Restore persistent system path composition
This commit is contained in:
parent
84bb22a7db
commit
7aff8e542e
4 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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue