mirror of
https://github.com/openai/codex.git
synced 2026-05-01 01:47:18 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user