fix: preserve split filesystem semantics in linux sandbox (#14173)

## Stack

   fix: fail closed for unsupported split windows sandboxing #14172
-> fix: preserve split filesystem semantics in linux sandbox #14173
   fix: align core approvals with split sandbox policies #14171
   refactor: centralize filesystem permissions precedence #14174

## Summary
## Summary
- Preserve Linux split filesystem carveouts in bubblewrap by applying
mount masks in the right order, so narrower rules still win under
broader writable roots.
- Preserve unreadable ancestors of writable roots by masking them first
and then rebinding the narrower writable descendants.
- Stop rejecting legacy-plus-split Linux configs that are
sandbox-equivalent after `cwd` resolution by comparing semantics instead
of raw legacy structs.
- Fail closed when callers provide partial split policies, mismatched
legacy-plus-split policies, or force `--use-legacy-landlock` for
split-only shapes that legacy Landlock cannot enforce.
- Add Linux regressions for overlapping writable, read-only, and denied
paths, and document the supported split-policy enforcement path.

## Example
Given a split filesystem policy like:

```toml
[permissions.dev.filesystem]
":root" = "read"
"/code" = "write"
"/code/.git" = "read"
"/code/secrets" = "none"
"/code/secrets/tmp" = "write"
```

this PR makes Linux enforce the intended result under bubblewrap:

- `/code` stays writable
- `/code/.git` stays read-only
- `/code/secrets` stays denied
- `/code/secrets/tmp` can still be reopened as writable if explicitly
allowed

Before this, Linux could lose one of those carveouts depending on mount
order or legacy-policy fallback. This PR keeps the split-policy
semantics intact and rejects configurations that legacy Landlock cannot
represent safely.
This commit is contained in:
viyatb-oai
2026-03-12 10:56:32 -07:00
committed by GitHub
parent 4e99c0f179
commit 774965f1e8
4 changed files with 745 additions and 110 deletions

View File

@@ -1,5 +1,6 @@
use clap::Parser;
use std::ffi::CString;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::os::fd::FromRawFd;
@@ -114,6 +115,13 @@ pub fn run_main() -> ! {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
)
.unwrap_or_else(|err| panic!("{err}"));
ensure_legacy_landlock_mode_supports_policy(
use_legacy_landlock,
&file_system_sandbox_policy,
network_sandbox_policy,
&sandbox_policy_cwd,
);
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
@@ -207,12 +215,56 @@ struct EffectiveSandboxPolicies {
network_sandbox_policy: NetworkSandboxPolicy,
}
#[derive(Debug, PartialEq, Eq)]
enum ResolveSandboxPoliciesError {
PartialSplitPolicies,
SplitPoliciesRequireDirectRuntimeEnforcement(String),
FailedToDeriveLegacyPolicy(String),
MismatchedLegacyPolicy {
provided: SandboxPolicy,
derived: SandboxPolicy,
},
MissingConfiguration,
}
impl fmt::Display for ResolveSandboxPoliciesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PartialSplitPolicies => {
write!(
f,
"file-system and network sandbox policies must be provided together"
)
}
Self::SplitPoliciesRequireDirectRuntimeEnforcement(err) => {
write!(
f,
"split sandbox policies require direct runtime enforcement and cannot be paired with legacy sandbox policy: {err}"
)
}
Self::FailedToDeriveLegacyPolicy(err) => {
write!(
f,
"failed to derive legacy sandbox policy from split policies: {err}"
)
}
Self::MismatchedLegacyPolicy { provided, derived } => {
write!(
f,
"legacy sandbox policy must match split sandbox policies: provided={provided:?}, derived={derived:?}"
)
}
Self::MissingConfiguration => write!(f, "missing sandbox policy configuration"),
}
}
}
fn resolve_sandbox_policies(
sandbox_policy_cwd: &Path,
sandbox_policy: Option<SandboxPolicy>,
file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
network_sandbox_policy: Option<NetworkSandboxPolicy>,
) -> EffectiveSandboxPolicies {
) -> Result<EffectiveSandboxPolicies, ResolveSandboxPoliciesError> {
// Accept either a fully legacy policy, a fully split policy pair, or all
// three views together. Reject partial split-policy input so the helper
// never runs with mismatched filesystem/network state.
@@ -221,47 +273,118 @@ fn resolve_sandbox_policies(
Some((file_system_sandbox_policy, network_sandbox_policy))
}
(None, None) => None,
_ => panic!("file-system and network sandbox policies must be provided together"),
_ => return Err(ResolveSandboxPoliciesError::PartialSplitPolicies),
};
match (sandbox_policy, split_policies) {
(Some(sandbox_policy), Some((file_system_sandbox_policy, network_sandbox_policy))) => {
EffectiveSandboxPolicies {
if file_system_sandbox_policy
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd)
{
return Ok(EffectiveSandboxPolicies {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
});
}
let derived_legacy_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
.map_err(|err| {
ResolveSandboxPoliciesError::SplitPoliciesRequireDirectRuntimeEnforcement(
err.to_string(),
)
})?;
if !legacy_sandbox_policies_match_semantics(
&sandbox_policy,
&derived_legacy_policy,
sandbox_policy_cwd,
) {
return Err(ResolveSandboxPoliciesError::MismatchedLegacyPolicy {
provided: sandbox_policy,
derived: derived_legacy_policy,
});
}
Ok(EffectiveSandboxPolicies {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
}
})
}
(Some(sandbox_policy), None) => EffectiveSandboxPolicies {
(Some(sandbox_policy), None) => Ok(EffectiveSandboxPolicies {
file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&sandbox_policy,
sandbox_policy_cwd,
),
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
sandbox_policy,
},
}),
(None, Some((file_system_sandbox_policy, network_sandbox_policy))) => {
let sandbox_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
.unwrap_or_else(|err| {
panic!("failed to derive legacy sandbox policy from split policies: {err}")
});
EffectiveSandboxPolicies {
.map_err(|err| {
ResolveSandboxPoliciesError::FailedToDeriveLegacyPolicy(err.to_string())
})?;
Ok(EffectiveSandboxPolicies {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
}
})
}
(None, None) => panic!("missing sandbox policy configuration"),
(None, None) => Err(ResolveSandboxPoliciesError::MissingConfiguration),
}
}
fn legacy_sandbox_policies_match_semantics(
provided: &SandboxPolicy,
derived: &SandboxPolicy,
sandbox_policy_cwd: &Path,
) -> bool {
NetworkSandboxPolicy::from(provided) == NetworkSandboxPolicy::from(derived)
&& file_system_sandbox_policies_match_semantics(
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(provided, sandbox_policy_cwd),
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(derived, sandbox_policy_cwd),
sandbox_policy_cwd,
)
}
fn file_system_sandbox_policies_match_semantics(
provided: &FileSystemSandboxPolicy,
derived: &FileSystemSandboxPolicy,
sandbox_policy_cwd: &Path,
) -> bool {
provided.has_full_disk_read_access() == derived.has_full_disk_read_access()
&& provided.has_full_disk_write_access() == derived.has_full_disk_write_access()
&& provided.include_platform_defaults() == derived.include_platform_defaults()
&& provided.get_readable_roots_with_cwd(sandbox_policy_cwd)
== derived.get_readable_roots_with_cwd(sandbox_policy_cwd)
&& provided.get_writable_roots_with_cwd(sandbox_policy_cwd)
== derived.get_writable_roots_with_cwd(sandbox_policy_cwd)
&& provided.get_unreadable_roots_with_cwd(sandbox_policy_cwd)
== derived.get_unreadable_roots_with_cwd(sandbox_policy_cwd)
}
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_legacy_landlock: bool) {
if apply_seccomp_then_exec && use_legacy_landlock {
panic!("--apply-seccomp-then-exec is incompatible with --use-legacy-landlock");
}
}
fn ensure_legacy_landlock_mode_supports_policy(
use_legacy_landlock: bool,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox_policy_cwd: &Path,
) {
if use_legacy_landlock
&& file_system_sandbox_policy
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd)
{
panic!(
"split sandbox policies requiring direct runtime enforcement are incompatible with --use-legacy-landlock"
);
}
}
fn run_bwrap_with_proc_fallback(
sandbox_policy_cwd: &Path,
file_system_sandbox_policy: &FileSystemSandboxPolicy,