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 {
|
||||
protected_destinations: BTreeSet<String>,
|
||||
nested_transient_overrides: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SandboxLayoutV1 {
|
||||
|
|
@ -120,6 +121,10 @@ impl fmt::Debug for SandboxLayoutV1 {
|
|||
formatter
|
||||
.debug_struct("SandboxLayoutV1")
|
||||
.field("protected_destinations", &self.protected_destinations)
|
||||
.field(
|
||||
"nested_transient_overrides",
|
||||
&self.nested_transient_overrides,
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -132,9 +137,21 @@ impl SandboxLayoutV1 {
|
|||
/// include `/workspace`.
|
||||
pub fn from_protected_destinations(
|
||||
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> {
|
||||
let mut protected_destinations = BTreeSet::new();
|
||||
for destination in destinations {
|
||||
for destination in exclusive {
|
||||
validate_sandbox_path(&destination)?;
|
||||
if !protected_destinations.insert(destination) {
|
||||
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") {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
|
|
@ -151,6 +178,7 @@ impl SandboxLayoutV1 {
|
|||
}
|
||||
Ok(Self {
|
||||
protected_destinations,
|
||||
nested_transient_overrides: nested,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +186,11 @@ impl SandboxLayoutV1 {
|
|||
pub fn protected_destinations(&self) -> &BTreeSet<String> {
|
||||
&self.protected_destinations
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn nested_transient_overrides(&self) -> &BTreeSet<String> {
|
||||
&self.nested_transient_overrides
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfinedPathPlan {
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ fn validate_owned_directory(descriptor: &OwnedFd, uid: u32) -> Result<(), Config
|
|||
pub(super) fn validate_persistent_destinations(
|
||||
destinations: &[String],
|
||||
protected: &BTreeSet<String>,
|
||||
nested_transient_overrides: &BTreeSet<String>,
|
||||
) -> Result<Vec<String>, ConfigurationError> {
|
||||
require_pairwise_non_ancestral(
|
||||
destinations.iter().map(String::as_str),
|
||||
|
|
@ -272,11 +273,32 @@ pub(super) fn validate_persistent_destinations(
|
|||
)?;
|
||||
for destination in destinations {
|
||||
validate_sandbox_path(destination)?;
|
||||
require_clear_of_protected(destination, protected)?;
|
||||
require_persistent_clear_of_protected(destination, protected, nested_transient_overrides)?;
|
||||
}
|
||||
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(
|
||||
destination: &str,
|
||||
protected: &BTreeSet<String>,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ pub fn load_trusted_configuration(
|
|||
let persistent_dirs = validate_persistent_destinations(
|
||||
&selected.persistent_dirs,
|
||||
request.sandbox_layout.protected_destinations(),
|
||||
request.sandbox_layout.nested_transient_overrides(),
|
||||
)?;
|
||||
let (effective_allow_prefixes, effective_deny_prefixes) =
|
||||
resolve_network(&configuration, selected)?;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ fn request(
|
|||
tree: &TestTree,
|
||||
config: PathBuf,
|
||||
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 {
|
||||
let workspace = tree.path().join("workspace");
|
||||
let runtime = tree.path().join("runtime");
|
||||
|
|
@ -89,7 +98,7 @@ fn request(
|
|||
xdg_data_home: None,
|
||||
xdg_runtime_dir: runtime,
|
||||
environment,
|
||||
sandbox_layout: layout(),
|
||||
sandbox_layout,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +205,64 @@ fn sandbox_layout_is_closed_normalized_and_requires_workspace() {
|
|||
.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]
|
||||
|
|
|
|||
|
|
@ -525,22 +525,46 @@ fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
|
|||
first.id_map = id_map_with_container_root();
|
||||
first.destinations = vec![
|
||||
PreparedDestination {
|
||||
path: "/workspace".into(),
|
||||
path: "/root".into(),
|
||||
kind: PreparedDestinationKind::Directory,
|
||||
},
|
||||
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,
|
||||
},
|
||||
PreparedDestination {
|
||||
path: "/workspace".into(),
|
||||
kind: PreparedDestinationKind::Directory,
|
||||
path: "/etc/hosts".into(),
|
||||
kind: PreparedDestinationKind::RegularFile,
|
||||
},
|
||||
PreparedDestination {
|
||||
path: "/etc/hostname".into(),
|
||||
kind: PreparedDestinationKind::RegularFile,
|
||||
},
|
||||
];
|
||||
let artifact = fixture.store.prepare(&first).unwrap();
|
||||
let root = fixture.artifact_root(&artifact);
|
||||
assert!(root.join("workspace").is_dir());
|
||||
assert!(root.join("run/lazaret/bin/entry").is_file());
|
||||
for destination in ["root", "usr", "var", "etc", "opt"] {
|
||||
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)]);
|
||||
reordered.id_map = id_map_with_container_root();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use crate::network::{
|
|||
NetworkAttestationContext, PreparedNetwork, enter_generation_filesystem_namespaces,
|
||||
enter_generation_network_namespace, prepare_network,
|
||||
};
|
||||
use crate::sandbox::RUNTIME_FILE_DESTINATIONS;
|
||||
|
||||
use super::runtime::{
|
||||
attached_runsc_standard_io, fail_network, fail_runtime_components, fail_with_runtime_cleanup,
|
||||
|
|
@ -134,6 +135,19 @@ fn prepare_state_anchor(
|
|||
mount
|
||||
.require_persistent_destinations(&state.persistent_destinations)
|
||||
.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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ default_allow = ["192.0.2.10/32"]
|
|||
|
||||
[profiles.default]
|
||||
image = "example.invalid/agent:v1"
|
||||
persistent_dirs = ["/root"]
|
||||
persistent_dirs = ["/root", "/usr", "/var", "/etc", "/opt"]
|
||||
env = {{ SELECTED_SECRET = "env:SELECTED_HOST_SECRET" }}
|
||||
volumes = [
|
||||
{{ 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(),
|
||||
format!("runsc-overlay/{container}"),
|
||||
"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(),
|
||||
format!("sandbox-control/{container}/credentials"),
|
||||
format!("sandbox-control/{container}/mailbox/requests"),
|
||||
|
|
@ -247,6 +251,15 @@ fn mounts(specification: &Value) -> BTreeMap<&str, &Value> {
|
|||
.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> {
|
||||
mount["options"]
|
||||
.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}"))
|
||||
);
|
||||
}
|
||||
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -28,16 +28,18 @@ pub const DEV_DESTINATION: &str = "/dev";
|
|||
/// # Errors
|
||||
/// Returns an error if the fixed destination contract is not a closed normalized layout.
|
||||
pub fn default_sandbox_layout() -> Result<SandboxLayoutV1, ConfigurationError> {
|
||||
SandboxLayoutV1::from_protected_destinations([
|
||||
WORKSPACE_DESTINATION.to_owned(),
|
||||
CONTROL_ROOT_DESTINATION.to_owned(),
|
||||
PROC_DESTINATION.to_owned(),
|
||||
SYS_DESTINATION.to_owned(),
|
||||
DEV_DESTINATION.to_owned(),
|
||||
RESOLV_CONF_DESTINATION.to_owned(),
|
||||
HOSTS_DESTINATION.to_owned(),
|
||||
HOSTNAME_DESTINATION.to_owned(),
|
||||
])
|
||||
SandboxLayoutV1::from_destination_policy(
|
||||
[
|
||||
WORKSPACE_DESTINATION.to_owned(),
|
||||
CONTROL_ROOT_DESTINATION.to_owned(),
|
||||
PROC_DESTINATION.to_owned(),
|
||||
SYS_DESTINATION.to_owned(),
|
||||
DEV_DESTINATION.to_owned(),
|
||||
],
|
||||
RUNTIME_FILE_DESTINATIONS
|
||||
.iter()
|
||||
.map(|(_, destination)| (*destination).to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -61,5 +63,14 @@ mod tests {
|
|||
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 {
|
||||
lower: OwnedFd,
|
||||
persistent_anchor: OwnedFd,
|
||||
upper: OwnedFd,
|
||||
runtime_anchor: OwnedFd,
|
||||
merged_label: PathBuf,
|
||||
merged_descriptor: OwnedFd,
|
||||
|
|
@ -214,6 +215,7 @@ impl RuntimeStateAnchorAuthority {
|
|||
inherited.persistent_anchor_label,
|
||||
(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 retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?;
|
||||
if !inherited.runtime_anchor_label.is_absolute() {
|
||||
|
|
@ -231,14 +233,13 @@ impl RuntimeStateAnchorAuthority {
|
|||
inherited.runtime_anchor_label,
|
||||
(retained.st_dev, retained.st_ino),
|
||||
)?;
|
||||
for name in ["upper", "work"] {
|
||||
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
||||
}
|
||||
drop(open_directory(&persistent_anchor, Path::new("work"))?);
|
||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||
Ok(Self {
|
||||
lower,
|
||||
persistent_anchor,
|
||||
upper,
|
||||
runtime_anchor,
|
||||
merged_label,
|
||||
merged_descriptor,
|
||||
|
|
@ -421,6 +422,54 @@ impl RuntimeStateAnchorMount {
|
|||
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.
|
||||
///
|
||||
/// # 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 {
|
||||
#[must_use]
|
||||
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,28 @@ fn run() -> io::Result<()> {
|
|||
},
|
||||
)
|
||||
.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)?;
|
||||
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
||||
|
||||
let first = acquire(&layout, &artifact)?;
|
||||
fs::write(
|
||||
|
|
|
|||
|
|
@ -285,13 +285,15 @@ the same component-wise `0700` creation rules.
|
|||
|
||||
Configuration resolution also requires a closed sandbox layout. Its
|
||||
closed protected-destination set contains `/workspace` and every product-owned
|
||||
sandbox mount destination selected by default bundle construction. Each
|
||||
persistent or volume destination is compared with every member in both ancestry
|
||||
directions. The configuration loader does not guess paths for control,
|
||||
credential, mailbox, proc, sys, dev, runtime, or later product mounts, and a
|
||||
layout cannot be constructed without `/workspace`. Bundle
|
||||
construction, rather than trusted-host configuration syntax, owns the remaining
|
||||
exact destination choices.
|
||||
sandbox mount destination selected by default bundle construction. Volumes and
|
||||
protected trees may not overlap in either ancestry direction. Persistent paths
|
||||
follow the same rule except that a bundle-classified exact transient mount may
|
||||
replace a child of a persistent parent. The child remains protected and may not
|
||||
itself contain or be selected as persistent state. The configuration loader does
|
||||
not guess paths for control, credential, mailbox, proc, sys, dev, runtime, or
|
||||
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
|
||||
network validation. A missing, non-UTF-8, or NUL-containing host value refuses
|
||||
|
|
|
|||
Loading…
Reference in a new issue