Restore persistent system path composition
This commit is contained in:
parent
84bb22a7db
commit
7aff8e542e
11 changed files with 338 additions and 30 deletions
|
|
@ -113,6 +113,7 @@ impl Ipv4Prefix {
|
||||||
|
|
||||||
pub struct SandboxLayoutV1 {
|
pub struct SandboxLayoutV1 {
|
||||||
protected_destinations: BTreeSet<String>,
|
protected_destinations: BTreeSet<String>,
|
||||||
|
nested_transient_overrides: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for SandboxLayoutV1 {
|
impl fmt::Debug for SandboxLayoutV1 {
|
||||||
|
|
@ -120,6 +121,10 @@ impl fmt::Debug for SandboxLayoutV1 {
|
||||||
formatter
|
formatter
|
||||||
.debug_struct("SandboxLayoutV1")
|
.debug_struct("SandboxLayoutV1")
|
||||||
.field("protected_destinations", &self.protected_destinations)
|
.field("protected_destinations", &self.protected_destinations)
|
||||||
|
.field(
|
||||||
|
"nested_transient_overrides",
|
||||||
|
&self.nested_transient_overrides,
|
||||||
|
)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,9 +137,21 @@ impl SandboxLayoutV1 {
|
||||||
/// include `/workspace`.
|
/// include `/workspace`.
|
||||||
pub fn from_protected_destinations(
|
pub fn from_protected_destinations(
|
||||||
destinations: impl IntoIterator<Item = String>,
|
destinations: impl IntoIterator<Item = String>,
|
||||||
|
) -> Result<Self, ConfigurationError> {
|
||||||
|
Self::from_destination_policy(destinations, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds protected destination classes supplied by sandbox-layout construction.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Destinations must be normalized, non-root, unique across classes, and include
|
||||||
|
/// `/workspace`; nested overrides must be exact product-owned transient mounts.
|
||||||
|
pub fn from_destination_policy(
|
||||||
|
exclusive: impl IntoIterator<Item = String>,
|
||||||
|
nested_transient_overrides: impl IntoIterator<Item = String>,
|
||||||
) -> Result<Self, ConfigurationError> {
|
) -> Result<Self, ConfigurationError> {
|
||||||
let mut protected_destinations = BTreeSet::new();
|
let mut protected_destinations = BTreeSet::new();
|
||||||
for destination in destinations {
|
for destination in exclusive {
|
||||||
validate_sandbox_path(&destination)?;
|
validate_sandbox_path(&destination)?;
|
||||||
if !protected_destinations.insert(destination) {
|
if !protected_destinations.insert(destination) {
|
||||||
return Err(ConfigurationError::new(
|
return Err(ConfigurationError::new(
|
||||||
|
|
@ -143,6 +160,16 @@ impl SandboxLayoutV1 {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut nested = BTreeSet::new();
|
||||||
|
for destination in nested_transient_overrides {
|
||||||
|
validate_sandbox_path(&destination)?;
|
||||||
|
if !nested.insert(destination.clone()) || !protected_destinations.insert(destination) {
|
||||||
|
return Err(ConfigurationError::new(
|
||||||
|
ConfigurationErrorKind::InvalidPath,
|
||||||
|
"duplicate protected sandbox destination",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
if !protected_destinations.contains("/workspace") {
|
if !protected_destinations.contains("/workspace") {
|
||||||
return Err(ConfigurationError::new(
|
return Err(ConfigurationError::new(
|
||||||
ConfigurationErrorKind::InvalidPath,
|
ConfigurationErrorKind::InvalidPath,
|
||||||
|
|
@ -151,6 +178,7 @@ impl SandboxLayoutV1 {
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
protected_destinations,
|
protected_destinations,
|
||||||
|
nested_transient_overrides: nested,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,6 +186,11 @@ impl SandboxLayoutV1 {
|
||||||
pub fn protected_destinations(&self) -> &BTreeSet<String> {
|
pub fn protected_destinations(&self) -> &BTreeSet<String> {
|
||||||
&self.protected_destinations
|
&self.protected_destinations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn nested_transient_overrides(&self) -> &BTreeSet<String> {
|
||||||
|
&self.nested_transient_overrides
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfinedPathPlan {
|
pub struct ConfinedPathPlan {
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@ fn validate_owned_directory(descriptor: &OwnedFd, uid: u32) -> Result<(), Config
|
||||||
pub(super) fn validate_persistent_destinations(
|
pub(super) fn validate_persistent_destinations(
|
||||||
destinations: &[String],
|
destinations: &[String],
|
||||||
protected: &BTreeSet<String>,
|
protected: &BTreeSet<String>,
|
||||||
|
nested_transient_overrides: &BTreeSet<String>,
|
||||||
) -> Result<Vec<String>, ConfigurationError> {
|
) -> Result<Vec<String>, ConfigurationError> {
|
||||||
require_pairwise_non_ancestral(
|
require_pairwise_non_ancestral(
|
||||||
destinations.iter().map(String::as_str),
|
destinations.iter().map(String::as_str),
|
||||||
|
|
@ -272,11 +273,32 @@ pub(super) fn validate_persistent_destinations(
|
||||||
)?;
|
)?;
|
||||||
for destination in destinations {
|
for destination in destinations {
|
||||||
validate_sandbox_path(destination)?;
|
validate_sandbox_path(destination)?;
|
||||||
require_clear_of_protected(destination, protected)?;
|
require_persistent_clear_of_protected(destination, protected, nested_transient_overrides)?;
|
||||||
}
|
}
|
||||||
Ok(destinations.to_vec())
|
Ok(destinations.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_persistent_clear_of_protected(
|
||||||
|
destination: &str,
|
||||||
|
protected: &BTreeSet<String>,
|
||||||
|
nested_transient_overrides: &BTreeSet<String>,
|
||||||
|
) -> Result<(), ConfigurationError> {
|
||||||
|
let conflict = protected.iter().any(|protected| {
|
||||||
|
destination == protected
|
||||||
|
|| sandbox_is_strict_descendant(destination, protected)
|
||||||
|
|| (sandbox_is_strict_descendant(protected, destination)
|
||||||
|
&& !nested_transient_overrides.contains(protected))
|
||||||
|
});
|
||||||
|
if conflict {
|
||||||
|
Err(ConfigurationError::new(
|
||||||
|
ConfigurationErrorKind::PathConflict,
|
||||||
|
"configured sandbox destination intersects a protected destination",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn require_clear_of_protected(
|
pub(super) fn require_clear_of_protected(
|
||||||
destination: &str,
|
destination: &str,
|
||||||
protected: &BTreeSet<String>,
|
protected: &BTreeSet<String>,
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ pub fn load_trusted_configuration(
|
||||||
let persistent_dirs = validate_persistent_destinations(
|
let persistent_dirs = validate_persistent_destinations(
|
||||||
&selected.persistent_dirs,
|
&selected.persistent_dirs,
|
||||||
request.sandbox_layout.protected_destinations(),
|
request.sandbox_layout.protected_destinations(),
|
||||||
|
request.sandbox_layout.nested_transient_overrides(),
|
||||||
)?;
|
)?;
|
||||||
let (effective_allow_prefixes, effective_deny_prefixes) =
|
let (effective_allow_prefixes, effective_deny_prefixes) =
|
||||||
resolve_network(&configuration, selected)?;
|
resolve_network(&configuration, selected)?;
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@ fn request(
|
||||||
tree: &TestTree,
|
tree: &TestTree,
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
environment: BTreeMap<String, std::ffi::OsString>,
|
environment: BTreeMap<String, std::ffi::OsString>,
|
||||||
|
) -> ConfigurationLoadRequest {
|
||||||
|
request_with_layout(tree, config, environment, layout())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_with_layout(
|
||||||
|
tree: &TestTree,
|
||||||
|
config: PathBuf,
|
||||||
|
environment: BTreeMap<String, std::ffi::OsString>,
|
||||||
|
sandbox_layout: SandboxLayoutV1,
|
||||||
) -> ConfigurationLoadRequest {
|
) -> ConfigurationLoadRequest {
|
||||||
let workspace = tree.path().join("workspace");
|
let workspace = tree.path().join("workspace");
|
||||||
let runtime = tree.path().join("runtime");
|
let runtime = tree.path().join("runtime");
|
||||||
|
|
@ -89,7 +98,7 @@ fn request(
|
||||||
xdg_data_home: None,
|
xdg_data_home: None,
|
||||||
xdg_runtime_dir: runtime,
|
xdg_runtime_dir: runtime,
|
||||||
environment,
|
environment,
|
||||||
sandbox_layout: layout(),
|
sandbox_layout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,6 +205,64 @@ fn sandbox_layout_is_closed_normalized_and_requires_workspace() {
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
SandboxLayoutV1::from_destination_policy(["/workspace".into()], ["/workspace".into()])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persistent_parents_allow_only_classified_transient_children() {
|
||||||
|
let layout = SandboxLayoutV1::from_destination_policy(
|
||||||
|
[
|
||||||
|
"/workspace".into(),
|
||||||
|
"/run/lazaret".into(),
|
||||||
|
"/proc".into(),
|
||||||
|
"/sys".into(),
|
||||||
|
"/dev".into(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/etc/resolv.conf".into(),
|
||||||
|
"/etc/hosts".into(),
|
||||||
|
"/etc/hostname".into(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tree = TestTree::new();
|
||||||
|
let body = valid_configuration(&tree).replace(
|
||||||
|
"persistent_dirs = [\"/root\"]",
|
||||||
|
"persistent_dirs = [\"/root\", \"/usr\", \"/var\", \"/etc\", \"/opt\"]",
|
||||||
|
);
|
||||||
|
let config = write_configuration(&tree, &body);
|
||||||
|
let environment = BTreeMap::from([("HOST_TOKEN".into(), "value".into())]);
|
||||||
|
let resolved =
|
||||||
|
load_trusted_configuration(request_with_layout(&tree, config, environment, layout))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resolved.fingerprint_record().persistent_dirs,
|
||||||
|
["/root", "/usr", "/var", "/etc", "/opt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
for persistent in ["/run", "/run/lazaret/state", "/etc/hosts"] {
|
||||||
|
let case = TestTree::new();
|
||||||
|
let body = valid_configuration(&case).replace(
|
||||||
|
"persistent_dirs = [\"/root\"]",
|
||||||
|
&format!("persistent_dirs = [\"{persistent}\"]"),
|
||||||
|
);
|
||||||
|
let config = write_configuration(&case, &body);
|
||||||
|
let environment = BTreeMap::from([("HOST_TOKEN".into(), "value".into())]);
|
||||||
|
let layout = SandboxLayoutV1::from_destination_policy(
|
||||||
|
["/workspace".into(), "/run/lazaret".into()],
|
||||||
|
["/etc/hosts".into()],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
load_trusted_configuration(request_with_layout(&case, config, environment, layout,))
|
||||||
|
.unwrap_err()
|
||||||
|
.kind(),
|
||||||
|
ConfigurationErrorKind::PathConflict
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -525,22 +525,46 @@ fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
|
||||||
first.id_map = id_map_with_container_root();
|
first.id_map = id_map_with_container_root();
|
||||||
first.destinations = vec![
|
first.destinations = vec![
|
||||||
PreparedDestination {
|
PreparedDestination {
|
||||||
path: "/workspace".into(),
|
path: "/root".into(),
|
||||||
kind: PreparedDestinationKind::Directory,
|
kind: PreparedDestinationKind::Directory,
|
||||||
},
|
},
|
||||||
PreparedDestination {
|
PreparedDestination {
|
||||||
path: "/run/lazaret/bin/entry".into(),
|
path: "/usr".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/var".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/etc".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/opt".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/etc/resolv.conf".into(),
|
||||||
kind: PreparedDestinationKind::RegularFile,
|
kind: PreparedDestinationKind::RegularFile,
|
||||||
},
|
},
|
||||||
PreparedDestination {
|
PreparedDestination {
|
||||||
path: "/workspace".into(),
|
path: "/etc/hosts".into(),
|
||||||
kind: PreparedDestinationKind::Directory,
|
kind: PreparedDestinationKind::RegularFile,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/etc/hostname".into(),
|
||||||
|
kind: PreparedDestinationKind::RegularFile,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let artifact = fixture.store.prepare(&first).unwrap();
|
let artifact = fixture.store.prepare(&first).unwrap();
|
||||||
let root = fixture.artifact_root(&artifact);
|
let root = fixture.artifact_root(&artifact);
|
||||||
assert!(root.join("workspace").is_dir());
|
for destination in ["root", "usr", "var", "etc", "opt"] {
|
||||||
assert!(root.join("run/lazaret/bin/entry").is_file());
|
assert!(root.join(destination).is_dir());
|
||||||
|
}
|
||||||
|
for destination in ["etc/resolv.conf", "etc/hosts", "etc/hostname"] {
|
||||||
|
assert!(root.join(destination).is_file());
|
||||||
|
}
|
||||||
|
|
||||||
let mut reordered = fixture.request(73, &[(bytes.clone(), OCI_TAR)]);
|
let mut reordered = fixture.request(73, &[(bytes.clone(), OCI_TAR)]);
|
||||||
reordered.id_map = id_map_with_container_root();
|
reordered.id_map = id_map_with_container_root();
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use crate::network::{
|
||||||
NetworkAttestationContext, PreparedNetwork, enter_generation_filesystem_namespaces,
|
NetworkAttestationContext, PreparedNetwork, enter_generation_filesystem_namespaces,
|
||||||
enter_generation_network_namespace, prepare_network,
|
enter_generation_network_namespace, prepare_network,
|
||||||
};
|
};
|
||||||
|
use crate::sandbox::RUNTIME_FILE_DESTINATIONS;
|
||||||
|
|
||||||
use super::runtime::{
|
use super::runtime::{
|
||||||
attached_runsc_standard_io, fail_network, fail_runtime_components, fail_with_runtime_cleanup,
|
attached_runsc_standard_io, fail_network, fail_runtime_components, fail_with_runtime_cleanup,
|
||||||
|
|
@ -134,6 +135,19 @@ fn prepare_state_anchor(
|
||||||
mount
|
mount
|
||||||
.require_persistent_destinations(&state.persistent_destinations)
|
.require_persistent_destinations(&state.persistent_destinations)
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
|
let transient_targets = RUNTIME_FILE_DESTINATIONS
|
||||||
|
.iter()
|
||||||
|
.map(|(_, destination)| PathBuf::from(destination.trim_start_matches('/')))
|
||||||
|
.filter(|target| {
|
||||||
|
state
|
||||||
|
.persistent_destinations
|
||||||
|
.iter()
|
||||||
|
.any(|persistent| target.starts_with(persistent) && target != persistent)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
mount
|
||||||
|
.require_transient_override_targets(&transient_targets)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
Ok(Some(mount))
|
Ok(Some(mount))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ default_allow = ["192.0.2.10/32"]
|
||||||
|
|
||||||
[profiles.default]
|
[profiles.default]
|
||||||
image = "example.invalid/agent:v1"
|
image = "example.invalid/agent:v1"
|
||||||
persistent_dirs = ["/root"]
|
persistent_dirs = ["/root", "/usr", "/var", "/etc", "/opt"]
|
||||||
env = {{ SELECTED_SECRET = "env:SELECTED_HOST_SECRET" }}
|
env = {{ SELECTED_SECRET = "env:SELECTED_HOST_SECRET" }}
|
||||||
volumes = [
|
volumes = [
|
||||||
{{ host = "{{profile_dir}}/read-only", container = "/root/read-only", kind = "directory", access = "read-only", create = true }},
|
{{ host = "{{profile_dir}}/read-only", container = "/root/read-only", kind = "directory", access = "read-only", create = true }},
|
||||||
|
|
@ -164,6 +164,10 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
|
||||||
"runsc".to_owned(),
|
"runsc".to_owned(),
|
||||||
format!("runsc-overlay/{container}"),
|
format!("runsc-overlay/{container}"),
|
||||||
"state-anchor/merged/root".to_owned(),
|
"state-anchor/merged/root".to_owned(),
|
||||||
|
"state-anchor/merged/usr".to_owned(),
|
||||||
|
"state-anchor/merged/var".to_owned(),
|
||||||
|
"state-anchor/merged/etc".to_owned(),
|
||||||
|
"state-anchor/merged/opt".to_owned(),
|
||||||
"state-anchor/runtime-files".to_owned(),
|
"state-anchor/runtime-files".to_owned(),
|
||||||
format!("sandbox-control/{container}/credentials"),
|
format!("sandbox-control/{container}/credentials"),
|
||||||
format!("sandbox-control/{container}/mailbox/requests"),
|
format!("sandbox-control/{container}/mailbox/requests"),
|
||||||
|
|
@ -247,6 +251,15 @@ fn mounts(specification: &Value) -> BTreeMap<&str, &Value> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mount_destinations(specification: &Value) -> Vec<&str> {
|
||||||
|
specification["mounts"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|mount| mount["destination"].as_str().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn options(mount: &Value) -> BTreeSet<&str> {
|
fn options(mount: &Value) -> BTreeSet<&str> {
|
||||||
mount["options"]
|
mount["options"]
|
||||||
.as_array()
|
.as_array()
|
||||||
|
|
@ -468,6 +481,25 @@ fn trusted_init_environment_is_empty_and_volume_modes_are_preserved() {
|
||||||
.ends_with(&format!("state-anchor/runtime-files/{name}"))
|
.ends_with(&format!("state-anchor/runtime-files/{name}"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let destinations = mount_destinations(&specification);
|
||||||
|
let persistent_etc = destinations
|
||||||
|
.iter()
|
||||||
|
.position(|path| *path == "/etc")
|
||||||
|
.unwrap();
|
||||||
|
for runtime_file in [
|
||||||
|
RESOLV_CONF_DESTINATION,
|
||||||
|
HOSTS_DESTINATION,
|
||||||
|
HOSTNAME_DESTINATION,
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
persistent_etc
|
||||||
|
< destinations
|
||||||
|
.iter()
|
||||||
|
.position(|path| *path == runtime_file)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,18 @@ pub const DEV_DESTINATION: &str = "/dev";
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error if the fixed destination contract is not a closed normalized layout.
|
/// Returns an error if the fixed destination contract is not a closed normalized layout.
|
||||||
pub fn default_sandbox_layout() -> Result<SandboxLayoutV1, ConfigurationError> {
|
pub fn default_sandbox_layout() -> Result<SandboxLayoutV1, ConfigurationError> {
|
||||||
SandboxLayoutV1::from_protected_destinations([
|
SandboxLayoutV1::from_destination_policy(
|
||||||
WORKSPACE_DESTINATION.to_owned(),
|
[
|
||||||
CONTROL_ROOT_DESTINATION.to_owned(),
|
WORKSPACE_DESTINATION.to_owned(),
|
||||||
PROC_DESTINATION.to_owned(),
|
CONTROL_ROOT_DESTINATION.to_owned(),
|
||||||
SYS_DESTINATION.to_owned(),
|
PROC_DESTINATION.to_owned(),
|
||||||
DEV_DESTINATION.to_owned(),
|
SYS_DESTINATION.to_owned(),
|
||||||
RESOLV_CONF_DESTINATION.to_owned(),
|
DEV_DESTINATION.to_owned(),
|
||||||
HOSTS_DESTINATION.to_owned(),
|
],
|
||||||
HOSTNAME_DESTINATION.to_owned(),
|
RUNTIME_FILE_DESTINATIONS
|
||||||
])
|
.iter()
|
||||||
|
.map(|(_, destination)| (*destination).to_owned()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -61,5 +63,14 @@ mod tests {
|
||||||
WORKSPACE_DESTINATION.to_owned(),
|
WORKSPACE_DESTINATION.to_owned(),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
default_sandbox_layout()
|
||||||
|
.unwrap()
|
||||||
|
.nested_transient_overrides(),
|
||||||
|
&RUNTIME_FILE_DESTINATIONS
|
||||||
|
.iter()
|
||||||
|
.map(|(_, destination)| (*destination).to_owned())
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ impl StateAnchorLaunchAuthority {
|
||||||
pub struct RuntimeStateAnchorAuthority {
|
pub struct RuntimeStateAnchorAuthority {
|
||||||
lower: OwnedFd,
|
lower: OwnedFd,
|
||||||
persistent_anchor: OwnedFd,
|
persistent_anchor: OwnedFd,
|
||||||
|
upper: OwnedFd,
|
||||||
runtime_anchor: OwnedFd,
|
runtime_anchor: OwnedFd,
|
||||||
merged_label: PathBuf,
|
merged_label: PathBuf,
|
||||||
merged_descriptor: OwnedFd,
|
merged_descriptor: OwnedFd,
|
||||||
|
|
@ -214,6 +215,7 @@ impl RuntimeStateAnchorAuthority {
|
||||||
inherited.persistent_anchor_label,
|
inherited.persistent_anchor_label,
|
||||||
(retained_persistent.st_dev, retained_persistent.st_ino),
|
(retained_persistent.st_dev, retained_persistent.st_ino),
|
||||||
)?;
|
)?;
|
||||||
|
let upper = open_directory(&persistent_anchor, Path::new("upper"))?;
|
||||||
let runtime_root = reopen_inherited_directory(inherited.runtime_root_fd)?;
|
let runtime_root = reopen_inherited_directory(inherited.runtime_root_fd)?;
|
||||||
let retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?;
|
let retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?;
|
||||||
if !inherited.runtime_anchor_label.is_absolute() {
|
if !inherited.runtime_anchor_label.is_absolute() {
|
||||||
|
|
@ -231,14 +233,13 @@ impl RuntimeStateAnchorAuthority {
|
||||||
inherited.runtime_anchor_label,
|
inherited.runtime_anchor_label,
|
||||||
(retained.st_dev, retained.st_ino),
|
(retained.st_dev, retained.st_ino),
|
||||||
)?;
|
)?;
|
||||||
for name in ["upper", "work"] {
|
drop(open_directory(&persistent_anchor, Path::new("work"))?);
|
||||||
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
|
||||||
}
|
|
||||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||||
let merged_label = inherited.runtime_anchor_label.join("merged");
|
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lower,
|
lower,
|
||||||
persistent_anchor,
|
persistent_anchor,
|
||||||
|
upper,
|
||||||
runtime_anchor,
|
runtime_anchor,
|
||||||
merged_label,
|
merged_label,
|
||||||
merged_descriptor,
|
merged_descriptor,
|
||||||
|
|
@ -421,6 +422,54 @@ impl RuntimeStateAnchorMount {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates exact transient child targets and proves they are absent from the upper layer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses changed mounts, invalid paths, non-files, links, nested mounts, or upper entries.
|
||||||
|
pub fn require_transient_override_targets(
|
||||||
|
&self,
|
||||||
|
targets: &[PathBuf],
|
||||||
|
) -> Result<(), StateAnchorError> {
|
||||||
|
if mount_identity(&self.merged)? != self.mount {
|
||||||
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
|
}
|
||||||
|
for target in targets {
|
||||||
|
if target.as_os_str().is_empty()
|
||||||
|
|| target
|
||||||
|
.components()
|
||||||
|
.any(|component| !matches!(component, std::path::Component::Normal(_)))
|
||||||
|
{
|
||||||
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
|
"transient override target is not normalized and relative",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let descriptor = openat2(
|
||||||
|
&self.merged,
|
||||||
|
target,
|
||||||
|
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
ResolveFlags::BENEATH
|
||||||
|
| ResolveFlags::NO_MAGICLINKS
|
||||||
|
| ResolveFlags::NO_SYMLINKS
|
||||||
|
| ResolveFlags::NO_XDEV,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
if !FileType::from_raw_mode(
|
||||||
|
rustix::fs::fstat(&descriptor)
|
||||||
|
.map_err(io::Error::from)?
|
||||||
|
.st_mode,
|
||||||
|
)
|
||||||
|
.is_file()
|
||||||
|
{
|
||||||
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
|
"transient override target is not a regular file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
require_upper_target_absent(&self.authority.upper, target)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Unmounts and reaps only the retained exact RS-owned State Anchor.
|
/// Unmounts and reaps only the retained exact RS-owned State Anchor.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -453,6 +502,38 @@ impl RuntimeStateAnchorMount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_upper_target_absent(upper: &OwnedFd, target: &Path) -> Result<(), StateAnchorError> {
|
||||||
|
require_upper_path_absent(upper, target)?;
|
||||||
|
let leaf = target.file_name().ok_or(StateAnchorError::InvalidLayout(
|
||||||
|
"transient override target has no final component",
|
||||||
|
))?;
|
||||||
|
let mut whiteout = std::ffi::OsString::from(".wh.");
|
||||||
|
whiteout.push(leaf);
|
||||||
|
require_upper_path_absent(
|
||||||
|
upper,
|
||||||
|
&target.parent().unwrap_or(Path::new("")).join(whiteout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_upper_path_absent(upper: &OwnedFd, target: &Path) -> Result<(), StateAnchorError> {
|
||||||
|
match openat2(
|
||||||
|
upper,
|
||||||
|
target,
|
||||||
|
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
ResolveFlags::BENEATH
|
||||||
|
| ResolveFlags::NO_MAGICLINKS
|
||||||
|
| ResolveFlags::NO_SYMLINKS
|
||||||
|
| ResolveFlags::NO_XDEV,
|
||||||
|
) {
|
||||||
|
Err(rustix::io::Errno::NOENT) => Ok(()),
|
||||||
|
Ok(_) => Err(StateAnchorError::InvalidLayout(
|
||||||
|
"transient override target is present in the upper layer",
|
||||||
|
)),
|
||||||
|
Err(error) => Err(io::Error::from(error).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl VerifiedStateAnchorReadiness {
|
impl VerifiedStateAnchorReadiness {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,28 @@ fn run() -> io::Result<()> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
|
let runtime_targets = [
|
||||||
|
PathBuf::from("etc/resolv.conf"),
|
||||||
|
PathBuf::from("etc/hosts"),
|
||||||
|
PathBuf::from("etc/hostname"),
|
||||||
|
];
|
||||||
|
runtime_mount
|
||||||
|
.require_persistent_destinations(&[PathBuf::from("usr"), PathBuf::from("etc")])
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
runtime_mount
|
||||||
|
.require_transient_override_targets(&runtime_targets)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
fs::write(layout.merged().join("etc/hosts"), b"stale")?;
|
||||||
|
if runtime_mount
|
||||||
|
.require_transient_override_targets(&runtime_targets)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"upper runtime-file entry survived transient override preflight",
|
||||||
|
));
|
||||||
|
}
|
||||||
runtime_mount.unmount().map_err(io::Error::other)?;
|
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||||
|
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
||||||
|
|
||||||
let first = acquire(&layout, &artifact)?;
|
let first = acquire(&layout, &artifact)?;
|
||||||
fs::write(
|
fs::write(
|
||||||
|
|
|
||||||
|
|
@ -285,13 +285,15 @@ the same component-wise `0700` creation rules.
|
||||||
|
|
||||||
Configuration resolution also requires a closed sandbox layout. Its
|
Configuration resolution also requires a closed sandbox layout. Its
|
||||||
closed protected-destination set contains `/workspace` and every product-owned
|
closed protected-destination set contains `/workspace` and every product-owned
|
||||||
sandbox mount destination selected by default bundle construction. Each
|
sandbox mount destination selected by default bundle construction. Volumes and
|
||||||
persistent or volume destination is compared with every member in both ancestry
|
protected trees may not overlap in either ancestry direction. Persistent paths
|
||||||
directions. The configuration loader does not guess paths for control,
|
follow the same rule except that a bundle-classified exact transient mount may
|
||||||
credential, mailbox, proc, sys, dev, runtime, or later product mounts, and a
|
replace a child of a persistent parent. The child remains protected and may not
|
||||||
layout cannot be constructed without `/workspace`. Bundle
|
itself contain or be selected as persistent state. The configuration loader does
|
||||||
construction, rather than trusted-host configuration syntax, owns the remaining
|
not guess paths for control, credential, mailbox, proc, sys, dev, runtime, or
|
||||||
exact destination choices.
|
later product mounts, and a layout cannot be constructed without `/workspace`.
|
||||||
|
Bundle construction, rather than trusted-host configuration syntax, owns the
|
||||||
|
exact destination choices and transient-child classification.
|
||||||
|
|
||||||
The loader resolves environment references only after structural, path, and
|
The loader resolves environment references only after structural, path, and
|
||||||
network validation. A missing, non-UTF-8, or NUL-containing host value refuses
|
network validation. A missing, non-UTF-8, or NUL-containing host value refuses
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue