mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
fix: canonicalize symlinked Linux sandbox cwd (#14849)
## Problem On Linux, Codex can be launched from a workspace path that is a symlink (for example, a symlinked checkout or a symlinked parent directory). Our sandbox policy intentionally canonicalizes writable/readable roots to the real filesystem path before building the bubblewrap mounts. That part is correct and needed for safety. The remaining bug was that bubblewrap could still inherit the helper process's logical cwd, which might be the symlinked alias instead of the mounted canonical path. In that case, the sandbox starts in a cwd that does not exist inside the sandbox namespace even though the real workspace is mounted. This can cause sandboxed commands to fail in symlinked workspaces. ## Fix This PR keeps the sandbox policy behavior the same, but separates two concepts that were previously conflated: - the canonical cwd used to define sandbox mounts and permissions - the caller's logical cwd used when launching the command On the Linux bubblewrap path, we now thread the logical command cwd through the helper explicitly and only add `--chdir <canonical path>` when the logical cwd differs from the mounted canonical path. That means: - permissions are still computed from canonical paths - bubblewrap starts the command from a cwd that definitely exists inside the sandbox - we do not widen filesystem access or undo the earlier symlink hardening ## Why This Is Safe This is a narrow Linux-only launch fix, not a policy change. - Writable/readable root canonicalization stays intact. - Protected metadata carveouts still operate on canonical roots. - We only override bubblewrap's inherited cwd when the logical path would otherwise point at a symlink alias that is not mounted in the sandbox. ## Tests - kept the existing protocol/core regression coverage for symlink canonicalization - added regression coverage for symlinked cwd handling in the Linux bubblewrap builder/helper path Local validation: - `just fmt` - `cargo test -p codex-protocol` - `cargo test -p codex-core normalize_additional_permissions_canonicalizes_symlinked_write_paths` - `cargo clippy -p codex-linux-sandbox -p codex-protocol -p codex-core --tests -- -D warnings` - `cargo build --bin codex` ## Context This is related to #14694. The earlier writable-root symlink fix addressed the mount/permission side; this PR fixes the remaining symlinked-cwd launch mismatch in the Linux sandbox path.
This commit is contained in:
@@ -38,6 +38,7 @@ where
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
command,
|
||||
command_cwd.as_path(),
|
||||
sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -76,6 +77,7 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
||||
command: Vec<String>,
|
||||
command_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
@@ -93,10 +95,16 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
||||
.to_str()
|
||||
.unwrap_or_else(|| panic!("cwd must be valid UTF-8"))
|
||||
.to_string();
|
||||
let command_cwd = command_cwd
|
||||
.to_str()
|
||||
.unwrap_or_else(|| panic!("command cwd must be valid UTF-8"))
|
||||
.to_string();
|
||||
|
||||
let mut linux_cmd: Vec<String> = vec![
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
sandbox_policy_cwd,
|
||||
"--command-cwd".to_string(),
|
||||
command_cwd,
|
||||
"--sandbox-policy".to_string(),
|
||||
sandbox_policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
@@ -120,16 +128,26 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
||||
#[cfg(test)]
|
||||
pub(crate) fn create_linux_sandbox_command_args(
|
||||
command: Vec<String>,
|
||||
command_cwd: &Path,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Vec<String> {
|
||||
let command_cwd = command_cwd
|
||||
.to_str()
|
||||
.unwrap_or_else(|| panic!("command cwd must be valid UTF-8"))
|
||||
.to_string();
|
||||
let sandbox_policy_cwd = sandbox_policy_cwd
|
||||
.to_str()
|
||||
.unwrap_or_else(|| panic!("cwd must be valid UTF-8"))
|
||||
.to_string();
|
||||
|
||||
let mut linux_cmd: Vec<String> = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd];
|
||||
let mut linux_cmd: Vec<String> = vec![
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
sandbox_policy_cwd,
|
||||
"--command-cwd".to_string(),
|
||||
command_cwd,
|
||||
];
|
||||
if use_legacy_landlock {
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ use pretty_assertions::assert_eq;
|
||||
#[test]
|
||||
fn legacy_landlock_flag_is_included_when_requested() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let command_cwd = Path::new("/tmp/link");
|
||||
let cwd = Path::new("/tmp");
|
||||
|
||||
let default_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, false, false);
|
||||
let default_bwrap =
|
||||
create_linux_sandbox_command_args(command.clone(), command_cwd, cwd, false, false);
|
||||
assert_eq!(
|
||||
default_bwrap.contains(&"--use-legacy-landlock".to_string()),
|
||||
false
|
||||
);
|
||||
|
||||
let legacy_landlock = create_linux_sandbox_command_args(command, cwd, true, false);
|
||||
let legacy_landlock = create_linux_sandbox_command_args(command, command_cwd, cwd, true, false);
|
||||
assert_eq!(
|
||||
legacy_landlock.contains(&"--use-legacy-landlock".to_string()),
|
||||
true
|
||||
@@ -22,9 +24,10 @@ fn legacy_landlock_flag_is_included_when_requested() {
|
||||
#[test]
|
||||
fn proxy_flag_is_included_when_requested() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let command_cwd = Path::new("/tmp/link");
|
||||
let cwd = Path::new("/tmp");
|
||||
|
||||
let args = create_linux_sandbox_command_args(command, cwd, true, true);
|
||||
let args = create_linux_sandbox_command_args(command, command_cwd, cwd, true, true);
|
||||
assert_eq!(
|
||||
args.contains(&"--allow-network-for-proxy".to_string()),
|
||||
true
|
||||
@@ -34,6 +37,7 @@ fn proxy_flag_is_included_when_requested() {
|
||||
#[test]
|
||||
fn split_policy_flags_are_included() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let command_cwd = Path::new("/tmp/link");
|
||||
let cwd = Path::new("/tmp");
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
@@ -41,6 +45,7 @@ fn split_policy_flags_are_included() {
|
||||
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
command,
|
||||
command_cwd,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -59,6 +64,11 @@ fn split_policy_flags_are_included() {
|
||||
.any(|window| window[0] == "--network-sandbox-policy" && window[1] == "\"restricted\""),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
args.windows(2)
|
||||
.any(|window| window[0] == "--command-cwd" && window[1] == "/tmp/link"),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -672,6 +672,7 @@ impl SandboxManager {
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
let mut args = create_linux_sandbox_command_args_for_policies(
|
||||
command.clone(),
|
||||
spec.cwd.as_path(),
|
||||
&effective_policy,
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
|
||||
@@ -35,8 +35,15 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(unix)]
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn symlink_dir(original: &Path, link: &Path) -> std::io::Result<()> {
|
||||
std::os::unix::fs::symlink(original, link)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() {
|
||||
let manager = SandboxManager::new();
|
||||
@@ -217,6 +224,41 @@ fn normalize_additional_permissions_preserves_network() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn normalize_additional_permissions_canonicalizes_symlinked_write_paths() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let real_root = temp_dir.path().join("real");
|
||||
let link_root = temp_dir.path().join("link");
|
||||
let write_dir = real_root.join("write");
|
||||
std::fs::create_dir_all(&write_dir).expect("create write dir");
|
||||
symlink_dir(&real_root, &link_root).expect("create symlinked root");
|
||||
|
||||
let link_write_dir =
|
||||
AbsolutePathBuf::from_absolute_path(link_root.join("write")).expect("link write dir");
|
||||
let expected_write_dir = AbsolutePathBuf::from_absolute_path(
|
||||
write_dir.canonicalize().expect("canonicalize write dir"),
|
||||
)
|
||||
.expect("absolute canonical write dir");
|
||||
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![link_write_dir]),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(
|
||||
permissions.file_system,
|
||||
Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![expected_write_dir]),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_additional_permissions_drops_empty_nested_profiles() {
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
|
||||
@@ -96,7 +96,8 @@ pub(crate) struct BwrapArgs {
|
||||
pub(crate) fn create_bwrap_command_args(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
@@ -110,7 +111,13 @@ pub(crate) fn create_bwrap_command_args(
|
||||
};
|
||||
}
|
||||
|
||||
create_bwrap_flags(command, file_system_sandbox_policy, cwd, options)
|
||||
create_bwrap_flags(
|
||||
command,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> BwrapArgs {
|
||||
@@ -144,13 +151,15 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
||||
fn create_bwrap_flags(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
let BwrapArgs {
|
||||
args: filesystem_args,
|
||||
preserved_files,
|
||||
} = create_filesystem_args(file_system_sandbox_policy, cwd)?;
|
||||
} = create_filesystem_args(file_system_sandbox_policy, sandbox_policy_cwd)?;
|
||||
let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd);
|
||||
let mut args = Vec::new();
|
||||
args.push("--new-session".to_string());
|
||||
args.push("--die-with-parent".to_string());
|
||||
@@ -168,6 +177,14 @@ fn create_bwrap_flags(
|
||||
args.push("--proc".to_string());
|
||||
args.push("/proc".to_string());
|
||||
}
|
||||
if normalized_command_cwd.as_path() != command_cwd {
|
||||
// Bubblewrap otherwise inherits the helper's logical cwd, which can be
|
||||
// a symlink alias that disappears once the sandbox only mounts
|
||||
// canonical roots. Enter the canonical command cwd explicitly so
|
||||
// relative paths stay aligned with the mounted filesystem view.
|
||||
args.push("--chdir".to_string());
|
||||
args.push(path_to_string(normalized_command_cwd.as_path()));
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command);
|
||||
Ok(BwrapArgs {
|
||||
@@ -393,6 +410,12 @@ fn path_depth(path: &Path) -> usize {
|
||||
path.components().count()
|
||||
}
|
||||
|
||||
fn normalize_command_cwd_for_bwrap(command_cwd: &Path) -> PathBuf {
|
||||
command_cwd
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| command_cwd.to_path_buf())
|
||||
}
|
||||
|
||||
fn append_mount_target_parent_dir_args(args: &mut Vec<String>, mount_target: &Path, anchor: &Path) {
|
||||
let mount_target_dir = if mount_target.is_dir() {
|
||||
mount_target
|
||||
@@ -607,6 +630,7 @@ mod tests {
|
||||
command.clone(),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
@@ -624,6 +648,7 @@ mod tests {
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::ProxyOnly,
|
||||
@@ -650,6 +675,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn restricted_policy_chdirs_to_canonical_command_cwd() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let real_root = temp_dir.path().join("real");
|
||||
let real_subdir = real_root.join("subdir");
|
||||
let link_root = temp_dir.path().join("link");
|
||||
std::fs::create_dir_all(&real_subdir).expect("create real subdir");
|
||||
std::os::unix::fs::symlink(&real_root, &link_root).expect("create symlinked root");
|
||||
|
||||
let sandbox_policy_cwd = AbsolutePathBuf::from_absolute_path(&link_root)
|
||||
.expect("absolute symlinked root")
|
||||
.to_path_buf();
|
||||
let command_cwd = link_root.join("subdir");
|
||||
let canonical_command_cwd = real_subdir
|
||||
.canonicalize()
|
||||
.expect("canonicalize command cwd");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_bwrap_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
&command_cwd,
|
||||
BwrapOptions::default(),
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
let canonical_command_cwd = path_to_string(&canonical_command_cwd);
|
||||
let link_command_cwd = path_to_string(&command_cwd);
|
||||
|
||||
assert!(
|
||||
args.args
|
||||
.windows(2)
|
||||
.any(|window| { window == ["--chdir", canonical_command_cwd.as_str()] })
|
||||
);
|
||||
assert!(
|
||||
!args
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|window| { window == ["--chdir", link_command_cwd.as_str()] })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mounts_dev_before_writable_dev_binds() {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
|
||||
@@ -31,6 +31,15 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "sandbox-policy-cwd")]
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
|
||||
/// The logical working directory for the command being sandboxed.
|
||||
///
|
||||
/// This can intentionally differ from `sandbox_policy_cwd` when the
|
||||
/// command runs from a symlinked alias of the policy workspace. Keep it
|
||||
/// explicit so bubblewrap can preserve the caller's logical cwd when that
|
||||
/// alias would otherwise disappear inside the sandbox namespace.
|
||||
#[arg(long = "command-cwd", hide = true)]
|
||||
pub command_cwd: Option<PathBuf>,
|
||||
|
||||
/// Legacy compatibility policy.
|
||||
///
|
||||
/// Newer callers pass split filesystem/network policies as well so the
|
||||
@@ -91,6 +100,7 @@ pub struct LandlockCommand {
|
||||
pub fn run_main() -> ! {
|
||||
let LandlockCommand {
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -177,6 +187,7 @@ pub fn run_main() -> ! {
|
||||
};
|
||||
let inner = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd,
|
||||
command_cwd: command_cwd.as_deref(),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -186,6 +197,7 @@ pub fn run_main() -> ! {
|
||||
});
|
||||
run_bwrap_with_proc_fallback(
|
||||
&sandbox_policy_cwd,
|
||||
command_cwd.as_deref(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
inner,
|
||||
@@ -387,6 +399,7 @@ fn ensure_legacy_landlock_mode_supports_policy(
|
||||
|
||||
fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: Option<&Path>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
inner: Vec<String>,
|
||||
@@ -395,10 +408,12 @@ fn run_bwrap_with_proc_fallback(
|
||||
) -> ! {
|
||||
let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy);
|
||||
let mut mount_proc = mount_proc;
|
||||
let command_cwd = command_cwd.unwrap_or(sandbox_policy_cwd);
|
||||
|
||||
if mount_proc
|
||||
&& !preflight_proc_mount_support(
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
file_system_sandbox_policy,
|
||||
network_mode,
|
||||
)
|
||||
@@ -416,6 +431,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
);
|
||||
exec_vendored_bwrap(bwrap_args.args, bwrap_args.preserved_files);
|
||||
@@ -438,12 +454,14 @@ fn build_bwrap_argv(
|
||||
inner: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> crate::bwrap::BwrapArgs {
|
||||
let mut bwrap_args = create_bwrap_command_args(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
options,
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
|
||||
@@ -468,17 +486,23 @@ fn build_bwrap_argv(
|
||||
|
||||
fn preflight_proc_mount_support(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> bool {
|
||||
let preflight_argv =
|
||||
build_preflight_bwrap_argv(sandbox_policy_cwd, file_system_sandbox_policy, network_mode);
|
||||
let preflight_argv = build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
file_system_sandbox_policy,
|
||||
network_mode,
|
||||
);
|
||||
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
|
||||
!is_proc_mount_failure(stderr.as_str())
|
||||
}
|
||||
|
||||
fn build_preflight_bwrap_argv(
|
||||
sandbox_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> crate::bwrap::BwrapArgs {
|
||||
@@ -487,6 +511,7 @@ fn build_preflight_bwrap_argv(
|
||||
preflight_command,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode,
|
||||
@@ -591,6 +616,7 @@ fn is_proc_mount_failure(stderr: &str) -> bool {
|
||||
|
||||
struct InnerSeccompCommandArgs<'a> {
|
||||
sandbox_policy_cwd: &'a Path,
|
||||
command_cwd: Option<&'a Path>,
|
||||
sandbox_policy: &'a SandboxPolicy,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
@@ -603,6 +629,7 @@ struct InnerSeccompCommandArgs<'a> {
|
||||
fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String> {
|
||||
let InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -631,6 +658,12 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
current_exe.to_string_lossy().to_string(),
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
sandbox_policy_cwd.to_string_lossy().to_string(),
|
||||
];
|
||||
if let Some(command_cwd) = command_cwd {
|
||||
inner.push("--command-cwd".to_string());
|
||||
inner.push(command_cwd.to_string_lossy().to_string());
|
||||
}
|
||||
inner.extend([
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
@@ -638,7 +671,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
"--apply-seccomp-then-exec".to_string(),
|
||||
];
|
||||
]);
|
||||
if allow_network_for_proxy {
|
||||
inner.push("--allow-network-for-proxy".to_string());
|
||||
let proxy_route_spec = proxy_route_spec
|
||||
|
||||
@@ -44,6 +44,7 @@ fn inserts_bwrap_argv0_before_command_separator() {
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
@@ -80,6 +81,7 @@ fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::Isolated,
|
||||
@@ -96,6 +98,7 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::ProxyOnly,
|
||||
@@ -163,6 +166,7 @@ fn root_write_read_only_carveout_requires_direct_runtime_enforcement() {
|
||||
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
|
||||
let argv = build_preflight_bwrap_argv(
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
mode,
|
||||
@@ -176,6 +180,7 @@ fn managed_proxy_inner_command_includes_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
@@ -193,6 +198,7 @@ fn inner_command_includes_split_policy_flags() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
@@ -203,6 +209,10 @@ fn inner_command_includes_split_policy_flags() {
|
||||
|
||||
assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy"));
|
||||
assert!(args.iter().any(|arg| arg == "--network-sandbox-policy"));
|
||||
assert!(
|
||||
args.windows(2)
|
||||
.any(|window| { window == ["--command-cwd", "/tmp/link"] })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -210,6 +220,7 @@ fn non_managed_inner_command_omits_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
@@ -227,6 +238,7 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
|
||||
@@ -1229,6 +1229,88 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn current_working_directory_special_path_canonicalizes_symlinked_cwd() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_root = cwd.path().join("real");
|
||||
let link_root = cwd.path().join("link");
|
||||
let blocked = real_root.join("blocked");
|
||||
let agents_dir = real_root.join(".agents");
|
||||
let codex_dir = real_root.join(".codex");
|
||||
|
||||
fs::create_dir_all(&blocked).expect("create blocked");
|
||||
fs::create_dir_all(&agents_dir).expect("create .agents");
|
||||
fs::create_dir_all(&codex_dir).expect("create .codex");
|
||||
symlink_dir(&real_root, &link_root).expect("create symlinked cwd");
|
||||
|
||||
let link_blocked =
|
||||
AbsolutePathBuf::from_absolute_path(link_root.join("blocked")).expect("link blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_agents = AbsolutePathBuf::from_absolute_path(
|
||||
agents_dir.canonicalize().expect("canonicalize .agents"),
|
||||
)
|
||||
.expect("absolute canonical .agents");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link_blocked },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
policy.get_readable_roots_with_cwd(&link_root),
|
||||
vec![expected_root.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
policy.get_unreadable_roots_with_cwd(&link_root),
|
||||
vec![expected_blocked.clone()]
|
||||
);
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(&link_root);
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
assert_eq!(writable_roots[0].root, expected_root);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.contains(&expected_blocked)
|
||||
);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.contains(&expected_agents)
|
||||
);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.contains(&expected_codex)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn writable_roots_preserve_symlinked_protected_subpaths() {
|
||||
|
||||
Reference in New Issue
Block a user