mirror of
https://github.com/openai/codex.git
synced 2026-03-08 07:33:21 +00:00
Compare commits
1 Commits
pr13453
...
dev/flaky-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0dbee9c61 |
@@ -673,32 +673,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_write_policy_with_carveouts_still_uses_platform_sandbox() {
|
||||
let blocked = AbsolutePathBuf::resolve_path_against_base(
|
||||
"blocked",
|
||||
std::env::current_dir().expect("current dir"),
|
||||
)
|
||||
.expect("blocked path");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: blocked },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Enabled, false),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_access_restricted_policy_still_uses_platform_sandbox_for_restricted_network() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
//! - seccomp + `PR_SET_NO_NEW_PRIVS` applied in-process, and
|
||||
//! - bubblewrap used to construct the filesystem view before exec.
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::error::Result;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
|
||||
/// Linux "platform defaults" that keep common system binaries and dynamic
|
||||
@@ -78,12 +76,6 @@ impl BwrapNetworkMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BwrapArgs {
|
||||
pub args: Vec<String>,
|
||||
pub preserved_files: Vec<File>,
|
||||
}
|
||||
|
||||
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
|
||||
/// with explicit writable roots and read-only subpaths layered afterward.
|
||||
///
|
||||
@@ -93,25 +85,22 @@ pub(crate) struct BwrapArgs {
|
||||
/// namespace restrictions apply while preserving full filesystem access.
|
||||
pub(crate) fn create_bwrap_command_args(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
) -> Result<Vec<String>> {
|
||||
if sandbox_policy.has_full_disk_write_access() {
|
||||
return if options.network_mode == BwrapNetworkMode::FullAccess {
|
||||
Ok(BwrapArgs {
|
||||
args: command,
|
||||
preserved_files: Vec::new(),
|
||||
})
|
||||
Ok(command)
|
||||
} else {
|
||||
Ok(create_bwrap_flags_full_filesystem(command, options))
|
||||
};
|
||||
}
|
||||
|
||||
create_bwrap_flags(command, file_system_sandbox_policy, cwd, options)
|
||||
create_bwrap_flags(command, sandbox_policy, cwd, options)
|
||||
}
|
||||
|
||||
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> BwrapArgs {
|
||||
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--new-session".to_string(),
|
||||
"--die-with-parent".to_string(),
|
||||
@@ -132,27 +121,20 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command);
|
||||
BwrapArgs {
|
||||
args,
|
||||
preserved_files: Vec::new(),
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
/// Build the bubblewrap flags (everything after `argv[0]`).
|
||||
fn create_bwrap_flags(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> Result<BwrapArgs> {
|
||||
let BwrapArgs {
|
||||
args: filesystem_args,
|
||||
preserved_files,
|
||||
} = create_filesystem_args(file_system_sandbox_policy, cwd)?;
|
||||
) -> Result<Vec<String>> {
|
||||
let mut args = Vec::new();
|
||||
args.push("--new-session".to_string());
|
||||
args.push("--die-with-parent".to_string());
|
||||
args.extend(filesystem_args);
|
||||
args.extend(create_filesystem_args(sandbox_policy, cwd)?);
|
||||
// Request a user namespace explicitly rather than relying on bubblewrap's
|
||||
// auto-enable behavior, which is skipped when the caller runs as uid 0.
|
||||
args.push("--unshare-user".to_string());
|
||||
@@ -168,35 +150,25 @@ fn create_bwrap_flags(
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command);
|
||||
Ok(BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
})
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
/// Build the bubblewrap filesystem mounts for a given filesystem policy.
|
||||
/// Build the bubblewrap filesystem mounts for a given sandbox policy.
|
||||
///
|
||||
/// The mount order is important:
|
||||
/// 1. Full-read policies, and restricted policies that explicitly read `/`,
|
||||
/// use `--ro-bind / /`; other restricted-read policies start from
|
||||
/// `--tmpfs /` and layer scoped `--ro-bind` mounts.
|
||||
/// 1. Full-read policies use `--ro-bind / /`; restricted-read policies start
|
||||
/// from `--tmpfs /` and layer scoped `--ro-bind` mounts.
|
||||
/// 2. `--dev /dev` mounts a minimal writable `/dev` with standard device nodes
|
||||
/// (including `/dev/urandom`) even under a read-only root.
|
||||
/// 3. `--bind <root> <root>` re-enables writes for allowed roots, including
|
||||
/// writable subpaths under `/dev` (for example, `/dev/shm`).
|
||||
/// 4. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
|
||||
/// those writable roots so protected subpaths win.
|
||||
/// 5. Explicit unreadable roots are masked last so deny carveouts still win
|
||||
/// even when the readable baseline includes `/`.
|
||||
fn create_filesystem_args(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> Result<BwrapArgs> {
|
||||
let writable_roots = file_system_sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
let unreadable_roots = file_system_sandbox_policy.get_unreadable_roots_with_cwd(cwd);
|
||||
fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<Vec<String>> {
|
||||
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
ensure_mount_targets_exist(&writable_roots)?;
|
||||
|
||||
let mut args = if file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
let mut args = if sandbox_policy.has_full_disk_read_access() {
|
||||
// Read-only root, then mount a minimal device tree.
|
||||
// In bubblewrap (`bubblewrap.c`, `SETUP_MOUNT_DEV`), `--dev /dev`
|
||||
// creates the standard minimal nodes: null, zero, full, random,
|
||||
@@ -219,12 +191,12 @@ fn create_filesystem_args(
|
||||
"/dev".to_string(),
|
||||
];
|
||||
|
||||
let mut readable_roots: BTreeSet<PathBuf> = file_system_sandbox_policy
|
||||
let mut readable_roots: BTreeSet<PathBuf> = sandbox_policy
|
||||
.get_readable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
if file_system_sandbox_policy.include_platform_defaults() {
|
||||
if sandbox_policy.include_platform_defaults() {
|
||||
readable_roots.extend(
|
||||
LINUX_PLATFORM_DEFAULT_READ_ROOTS
|
||||
.iter()
|
||||
@@ -234,8 +206,7 @@ fn create_filesystem_args(
|
||||
}
|
||||
|
||||
// A restricted policy can still explicitly request `/`, which is
|
||||
// the broad read baseline. Explicit unreadable carveouts are
|
||||
// re-applied later.
|
||||
// semantically equivalent to broad read access.
|
||||
if readable_roots.iter().any(|root| root == Path::new("/")) {
|
||||
args = vec![
|
||||
"--ro-bind".to_string(),
|
||||
@@ -257,7 +228,6 @@ fn create_filesystem_args(
|
||||
|
||||
args
|
||||
};
|
||||
let mut preserved_files = Vec::new();
|
||||
|
||||
for writable_root in &writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
@@ -301,44 +271,7 @@ fn create_filesystem_args(
|
||||
}
|
||||
}
|
||||
|
||||
if !unreadable_roots.is_empty() {
|
||||
// Apply explicit deny carveouts after all readable and writable mounts
|
||||
// so they win even when the broader baseline includes `/` or a writable
|
||||
// parent path.
|
||||
let null_file = File::open("/dev/null")?;
|
||||
let null_fd = null_file.as_raw_fd().to_string();
|
||||
for unreadable_root in unreadable_roots {
|
||||
let unreadable_root = unreadable_root.as_path();
|
||||
if unreadable_root.is_dir() {
|
||||
// Bubblewrap cannot bind `/dev/null` over a directory, so mask
|
||||
// denied directories by overmounting them with an empty tmpfs
|
||||
// and then remounting that tmpfs read-only.
|
||||
args.push("--perms".to_string());
|
||||
args.push("000".to_string());
|
||||
args.push("--tmpfs".to_string());
|
||||
args.push(path_to_string(unreadable_root));
|
||||
args.push("--remount-ro".to_string());
|
||||
args.push(path_to_string(unreadable_root));
|
||||
continue;
|
||||
}
|
||||
|
||||
// For files, bind a stable null-file payload over the original path
|
||||
// so later reads do not expose host contents. `--ro-bind-data`
|
||||
// expects a live fd number, so keep the backing file open until we
|
||||
// exec bubblewrap below.
|
||||
args.push("--perms".to_string());
|
||||
args.push("000".to_string());
|
||||
args.push("--ro-bind-data".to_string());
|
||||
args.push(null_fd.clone());
|
||||
args.push(path_to_string(unreadable_root));
|
||||
}
|
||||
preserved_files.push(null_file);
|
||||
}
|
||||
|
||||
Ok(BwrapArgs {
|
||||
args,
|
||||
preserved_files,
|
||||
})
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
/// Collect unique read-only subpaths across all writable roots.
|
||||
@@ -453,11 +386,6 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::FileSystemAccessMode;
|
||||
use codex_protocol::protocol::FileSystemPath;
|
||||
use codex_protocol::protocol::FileSystemSandboxEntry;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -469,7 +397,7 @@ mod tests {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command.clone(),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
@@ -478,7 +406,7 @@ mod tests {
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
|
||||
assert_eq!(args.args, command);
|
||||
assert_eq!(args, command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -486,7 +414,7 @@ mod tests {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
@@ -496,7 +424,7 @@ mod tests {
|
||||
.expect("create bwrap args");
|
||||
|
||||
assert_eq!(
|
||||
args.args,
|
||||
args,
|
||||
vec![
|
||||
"--new-session".to_string(),
|
||||
"--die-with-parent".to_string(),
|
||||
@@ -524,13 +452,9 @@ mod tests {
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
let args = create_filesystem_args(
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
Path::new("/"),
|
||||
)
|
||||
.expect("bwrap fs args");
|
||||
let args = create_filesystem_args(&sandbox_policy, Path::new("/")).expect("bwrap fs args");
|
||||
assert_eq!(
|
||||
args.args,
|
||||
args,
|
||||
vec![
|
||||
"--ro-bind".to_string(),
|
||||
"/".to_string(),
|
||||
@@ -538,11 +462,11 @@ mod tests {
|
||||
"--dev".to_string(),
|
||||
"/dev".to_string(),
|
||||
"--bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
"/dev".to_string(),
|
||||
"/dev".to_string(),
|
||||
"--bind".to_string(),
|
||||
"/dev".to_string(),
|
||||
"/dev".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -564,13 +488,12 @@ mod tests {
|
||||
network_access: false,
|
||||
};
|
||||
|
||||
let args = create_filesystem_args(&FileSystemSandboxPolicy::from(&policy), temp_dir.path())
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert_eq!(args.args[0..4], ["--tmpfs", "/", "--dev", "/dev"]);
|
||||
assert_eq!(args[0..4], ["--tmpfs", "/", "--dev", "/dev"]);
|
||||
|
||||
let readable_root_str = path_to_string(&readable_root);
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
assert!(args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--ro-bind",
|
||||
@@ -594,138 +517,15 @@ mod tests {
|
||||
// `ReadOnlyAccess::Restricted` always includes `cwd` as a readable
|
||||
// root. Using `"/"` here would intentionally collapse to broad read
|
||||
// access, so use a non-root cwd to exercise the restricted path.
|
||||
let args = create_filesystem_args(&FileSystemSandboxPolicy::from(&policy), temp_dir.path())
|
||||
.expect("filesystem args");
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(
|
||||
args.args
|
||||
.starts_with(&["--tmpfs".to_string(), "/".to_string()])
|
||||
);
|
||||
assert!(args.starts_with(&["--tmpfs".to_string(), "/".to_string()]));
|
||||
|
||||
if Path::new("/usr").exists() {
|
||||
assert!(
|
||||
args.args
|
||||
.windows(3)
|
||||
args.windows(3)
|
||||
.any(|window| window == ["--ro-bind", "/usr", "/usr"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_policy_reapplies_unreadable_carveouts_after_writable_binds() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let writable_root = temp_dir.path().join("workspace");
|
||||
let blocked = writable_root.join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked dir");
|
||||
let writable_root =
|
||||
AbsolutePathBuf::from_absolute_path(&writable_root).expect("absolute writable root");
|
||||
let blocked = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked dir");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: writable_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: blocked.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
let writable_root_str = path_to_string(writable_root.as_path());
|
||||
let blocked_str = path_to_string(blocked.as_path());
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--bind",
|
||||
writable_root_str.as_str(),
|
||||
writable_root_str.as_str(),
|
||||
]
|
||||
}));
|
||||
assert!(
|
||||
args.args.windows(3).any(|window| {
|
||||
window == ["--ro-bind", blocked_str.as_str(), blocked_str.as_str()]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_policy_masks_root_read_directory_carveouts() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let blocked = temp_dir.path().join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked dir");
|
||||
let blocked = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked dir");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: blocked.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
let blocked_str = path_to_string(blocked.as_path());
|
||||
|
||||
assert!(
|
||||
args.args
|
||||
.windows(3)
|
||||
.any(|window| window == ["--ro-bind", "/", "/"])
|
||||
);
|
||||
assert!(
|
||||
args.args
|
||||
.windows(4)
|
||||
.any(|window| { window == ["--perms", "000", "--tmpfs", blocked_str.as_str()] })
|
||||
);
|
||||
assert!(
|
||||
args.args
|
||||
.windows(2)
|
||||
.any(|window| window == ["--remount-ro", blocked_str.as_str()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_policy_masks_root_read_file_carveouts() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let blocked_file = temp_dir.path().join("blocked.txt");
|
||||
std::fs::write(&blocked_file, "secret").expect("create blocked file");
|
||||
let blocked_file =
|
||||
AbsolutePathBuf::from_absolute_path(&blocked_file).expect("absolute blocked file");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: blocked_file.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
let blocked_file_str = path_to_string(blocked_file.as_path());
|
||||
|
||||
assert_eq!(args.preserved_files.len(), 1);
|
||||
assert!(args.args.windows(5).any(|window| {
|
||||
window[0] == "--perms"
|
||||
&& window[1] == "000"
|
||||
&& window[2] == "--ro-bind-data"
|
||||
&& window[4] == blocked_file_str
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ pub fn run_main() -> ! {
|
||||
});
|
||||
run_bwrap_with_proc_fallback(
|
||||
&sandbox_policy_cwd,
|
||||
&file_system_sandbox_policy,
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
inner,
|
||||
!no_proc,
|
||||
@@ -261,7 +261,7 @@ fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_san
|
||||
|
||||
fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
@@ -270,12 +270,7 @@ 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;
|
||||
|
||||
if mount_proc
|
||||
&& !preflight_proc_mount_support(
|
||||
sandbox_policy_cwd,
|
||||
file_system_sandbox_policy,
|
||||
network_mode,
|
||||
)
|
||||
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy, network_mode)
|
||||
{
|
||||
eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc");
|
||||
mount_proc = false;
|
||||
@@ -285,13 +280,8 @@ fn run_bwrap_with_proc_fallback(
|
||||
mount_proc,
|
||||
network_mode,
|
||||
};
|
||||
let bwrap_args = build_bwrap_argv(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
options,
|
||||
);
|
||||
exec_vendored_bwrap(bwrap_args.args, bwrap_args.preserved_files);
|
||||
let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options);
|
||||
exec_vendored_bwrap(argv);
|
||||
}
|
||||
|
||||
fn bwrap_network_mode(
|
||||
@@ -309,56 +299,47 @@ fn bwrap_network_mode(
|
||||
|
||||
fn build_bwrap_argv(
|
||||
inner: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
) -> crate::bwrap::BwrapArgs {
|
||||
let mut bwrap_args = create_bwrap_command_args(
|
||||
inner,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
options,
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
|
||||
) -> Vec<String> {
|
||||
let mut args = create_bwrap_command_args(inner, sandbox_policy, sandbox_policy_cwd, options)
|
||||
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"));
|
||||
|
||||
let command_separator_index = bwrap_args
|
||||
.args
|
||||
let command_separator_index = args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
|
||||
bwrap_args.args.splice(
|
||||
args.splice(
|
||||
command_separator_index..command_separator_index,
|
||||
["--argv0".to_string(), "codex-linux-sandbox".to_string()],
|
||||
);
|
||||
|
||||
let mut argv = vec!["bwrap".to_string()];
|
||||
argv.extend(bwrap_args.args);
|
||||
crate::bwrap::BwrapArgs {
|
||||
args: argv,
|
||||
preserved_files: bwrap_args.preserved_files,
|
||||
}
|
||||
argv.extend(args);
|
||||
argv
|
||||
}
|
||||
|
||||
fn preflight_proc_mount_support(
|
||||
sandbox_policy_cwd: &Path,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> bool {
|
||||
let preflight_argv =
|
||||
build_preflight_bwrap_argv(sandbox_policy_cwd, file_system_sandbox_policy, network_mode);
|
||||
build_preflight_bwrap_argv(sandbox_policy_cwd, 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,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_mode: BwrapNetworkMode,
|
||||
) -> crate::bwrap::BwrapArgs {
|
||||
) -> Vec<String> {
|
||||
let preflight_command = vec![resolve_true_command()];
|
||||
build_bwrap_argv(
|
||||
preflight_command,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
@@ -387,7 +368,7 @@ fn resolve_true_command() -> String {
|
||||
/// - We capture stderr from that preflight to match known mount-failure text.
|
||||
/// We do not stream it because this is a one-shot probe with a trivial
|
||||
/// command, and reads are bounded to a fixed max size.
|
||||
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String {
|
||||
fn run_bwrap_in_child_capture_stderr(argv: Vec<String>) -> String {
|
||||
const MAX_PREFLIGHT_STDERR_BYTES: u64 = 64 * 1024;
|
||||
|
||||
let mut pipe_fds = [0; 2];
|
||||
@@ -416,7 +397,7 @@ fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> Str
|
||||
close_fd_or_panic(write_fd, "close write end in bubblewrap child");
|
||||
}
|
||||
|
||||
let exit_code = run_vendored_bwrap_main(&bwrap_args.args, &bwrap_args.preserved_files);
|
||||
let exit_code = run_vendored_bwrap_main(&argv);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,17 +35,15 @@ fn ignores_non_proc_mount_errors() {
|
||||
|
||||
#[test]
|
||||
fn inserts_bwrap_argv0_before_command_separator() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
},
|
||||
)
|
||||
.args;
|
||||
);
|
||||
assert_eq!(
|
||||
argv,
|
||||
vec![
|
||||
@@ -71,33 +69,29 @@ fn inserts_bwrap_argv0_before_command_separator() {
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::Isolated,
|
||||
},
|
||||
)
|
||||
.args;
|
||||
);
|
||||
assert!(argv.contains(&"--unshare-net".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::ProxyOnly,
|
||||
},
|
||||
)
|
||||
.args;
|
||||
);
|
||||
assert!(argv.contains(&"--unshare-net".to_string()));
|
||||
}
|
||||
|
||||
@@ -110,12 +104,7 @@ fn proxy_only_mode_takes_precedence_over_full_network_policy() {
|
||||
#[test]
|
||||
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("/"),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
mode,
|
||||
)
|
||||
.args;
|
||||
let argv = build_preflight_bwrap_argv(Path::new("/"), &SandboxPolicy::DangerFullAccess, mode);
|
||||
assert!(argv.iter().any(|arg| arg == "--"));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#[cfg(vendored_bwrap_available)]
|
||||
mod imp {
|
||||
use std::ffi::CString;
|
||||
use std::fs::File;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
unsafe extern "C" {
|
||||
@@ -28,10 +27,7 @@ mod imp {
|
||||
///
|
||||
/// On success, bubblewrap will `execve` into the target program and this
|
||||
/// function will never return. A return value therefore implies failure.
|
||||
pub(crate) fn run_vendored_bwrap_main(
|
||||
argv: &[String],
|
||||
_preserved_files: &[File],
|
||||
) -> libc::c_int {
|
||||
pub(crate) fn run_vendored_bwrap_main(argv: &[String]) -> libc::c_int {
|
||||
let cstrings = argv_to_cstrings(argv);
|
||||
|
||||
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
|
||||
@@ -43,21 +39,16 @@ mod imp {
|
||||
}
|
||||
|
||||
/// Execute the build-time bubblewrap `main` function with the given argv.
|
||||
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
|
||||
let exit_code = run_vendored_bwrap_main(&argv, &preserved_files);
|
||||
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>) -> ! {
|
||||
let exit_code = run_vendored_bwrap_main(&argv);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(vendored_bwrap_available))]
|
||||
mod imp {
|
||||
use std::fs::File;
|
||||
|
||||
/// Panics with a clear error when the build-time bwrap path is not enabled.
|
||||
pub(crate) fn run_vendored_bwrap_main(
|
||||
_argv: &[String],
|
||||
_preserved_files: &[File],
|
||||
) -> libc::c_int {
|
||||
pub(crate) fn run_vendored_bwrap_main(_argv: &[String]) -> libc::c_int {
|
||||
panic!(
|
||||
r#"build-time bubblewrap is not available in this build.
|
||||
codex-linux-sandbox should always compile vendored bubblewrap on Linux targets.
|
||||
@@ -69,8 +60,8 @@ Notes:
|
||||
}
|
||||
|
||||
/// Panics with a clear error when the build-time bwrap path is not enabled.
|
||||
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>, _preserved_files: Vec<File>) -> ! {
|
||||
let _ = run_vendored_bwrap_main(&[], &[]);
|
||||
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>) -> ! {
|
||||
let _ = run_vendored_bwrap_main(&[]);
|
||||
unreachable!("run_vendored_bwrap_main should always panic in this configuration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,8 @@ use codex_core::exec::process_exec_tool_call;
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -68,47 +63,13 @@ async fn run_cmd_output(
|
||||
.expect("sandboxed command should execute")
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
async fn run_cmd_result_with_writable_roots(
|
||||
cmd: &[&str],
|
||||
writable_roots: &[PathBuf],
|
||||
timeout_ms: u64,
|
||||
use_bwrap_sandbox: bool,
|
||||
network_access: bool,
|
||||
) -> Result<codex_core::exec::ExecToolCallOutput> {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: writable_roots
|
||||
.iter()
|
||||
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
|
||||
.collect(),
|
||||
read_only_access: Default::default(),
|
||||
network_access,
|
||||
// Exclude tmp-related folders from writable roots because we need a
|
||||
// folder that is writable by tests but that we intentionally disallow
|
||||
// writing to in the sandbox.
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
run_cmd_result_with_policies(
|
||||
cmd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
timeout_ms,
|
||||
use_bwrap_sandbox,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
async fn run_cmd_result_with_policies(
|
||||
cmd: &[&str],
|
||||
sandbox_policy: SandboxPolicy,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
timeout_ms: u64,
|
||||
use_bwrap_sandbox: bool,
|
||||
) -> Result<codex_core::exec::ExecToolCallOutput> {
|
||||
let cwd = std::env::current_dir().expect("cwd should exist");
|
||||
let sandbox_cwd = cwd.clone();
|
||||
@@ -123,14 +84,28 @@ async fn run_cmd_result_with_policies(
|
||||
justification: None,
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: writable_roots
|
||||
.iter()
|
||||
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
|
||||
.collect(),
|
||||
read_only_access: Default::default(),
|
||||
network_access,
|
||||
// Exclude tmp-related folders from writable roots because we need a
|
||||
// folder that is writable by tests but that we intentionally disallow
|
||||
// writing to in the sandbox.
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
|
||||
let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program));
|
||||
|
||||
process_exec_tool_call(
|
||||
params,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
use_bwrap_sandbox,
|
||||
@@ -504,110 +479,6 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() {
|
||||
assert_ne!(codex_output.exit_code, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let tmpdir = tempfile::tempdir().expect("tempdir");
|
||||
let blocked = tmpdir.path().join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked dir");
|
||||
let blocked_target = blocked.join("secret.txt");
|
||||
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")],
|
||||
read_only_access: Default::default(),
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir"),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
let output = expect_denied(
|
||||
run_cmd_result_with_policies(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
&format!("echo denied > {}", blocked_target.to_string_lossy()),
|
||||
],
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
"explicit split-policy carveout should be denied under bubblewrap",
|
||||
);
|
||||
|
||||
assert_ne!(output.exit_code, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_blocks_root_read_carveouts_under_bwrap() {
|
||||
if should_skip_bwrap_tests().await {
|
||||
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let tmpdir = tempfile::tempdir().expect("tempdir");
|
||||
let blocked = tmpdir.path().join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked dir");
|
||||
let blocked_target = blocked.join("secret.txt");
|
||||
std::fs::write(&blocked_target, "secret").expect("seed blocked file");
|
||||
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
let output = expect_denied(
|
||||
run_cmd_result_with_policies(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
&format!("cat {}", blocked_target.to_string_lossy()),
|
||||
],
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
"root-read carveout should be denied under bubblewrap",
|
||||
);
|
||||
|
||||
assert_ne!(output.exit_code, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_blocks_ssh() {
|
||||
// Force ssh to attempt a real TCP connection but fail quickly. `BatchMode`
|
||||
|
||||
@@ -123,25 +123,6 @@ impl Default for FileSystemSandboxPolicy {
|
||||
}
|
||||
|
||||
impl FileSystemSandboxPolicy {
|
||||
fn has_root_access(&self, predicate: impl Fn(FileSystemAccessMode) -> bool) -> bool {
|
||||
matches!(self.kind, FileSystemSandboxKind::Restricted)
|
||||
&& self.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
FileSystemPath::Special { value }
|
||||
if matches!(value, FileSystemSpecialPath::Root) && predicate(entry.access)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn has_explicit_deny_entries(&self) -> bool {
|
||||
matches!(self.kind, FileSystemSandboxKind::Restricted)
|
||||
&& self
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.access == FileSystemAccessMode::None)
|
||||
}
|
||||
|
||||
pub fn unrestricted() -> Self {
|
||||
Self {
|
||||
kind: FileSystemSandboxKind::Unrestricted,
|
||||
@@ -167,10 +148,13 @@ impl FileSystemSandboxPolicy {
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
match self.kind {
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
self.has_root_access(FileSystemAccessMode::can_read)
|
||||
&& !self.has_explicit_deny_entries()
|
||||
}
|
||||
FileSystemSandboxKind::Restricted => self.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
FileSystemPath::Special { value }
|
||||
if matches!(value, FileSystemSpecialPath::Root) && entry.access.can_read()
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,10 +162,14 @@ impl FileSystemSandboxPolicy {
|
||||
pub fn has_full_disk_write_access(&self) -> bool {
|
||||
match self.kind {
|
||||
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
self.has_root_access(FileSystemAccessMode::can_write)
|
||||
&& !self.has_explicit_deny_entries()
|
||||
}
|
||||
FileSystemSandboxKind::Restricted => self.entries.iter().any(|entry| {
|
||||
matches!(
|
||||
&entry.path,
|
||||
FileSystemPath::Special { value }
|
||||
if matches!(value, FileSystemSpecialPath::Root)
|
||||
&& entry.access.can_write()
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,24 +194,11 @@ impl FileSystemSandboxPolicy {
|
||||
}
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let mut readable_roots = Vec::new();
|
||||
if self.has_root_access(FileSystemAccessMode::can_read)
|
||||
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
|
||||
{
|
||||
readable_roots.push(absolute_root_path_for_cwd(cwd_absolute));
|
||||
}
|
||||
|
||||
dedup_absolute_paths(
|
||||
readable_roots
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_read())
|
||||
.filter_map(|entry| {
|
||||
resolve_file_system_path(&entry.path, cwd_absolute.as_ref())
|
||||
}),
|
||||
)
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_read())
|
||||
.filter_map(|entry| resolve_file_system_path(&entry.path, cwd_absolute.as_ref()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -237,24 +212,11 @@ impl FileSystemSandboxPolicy {
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let unreadable_roots = self.get_unreadable_roots_with_cwd(cwd);
|
||||
let mut writable_roots = Vec::new();
|
||||
if self.has_root_access(FileSystemAccessMode::can_write)
|
||||
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
|
||||
{
|
||||
writable_roots.push(absolute_root_path_for_cwd(cwd_absolute));
|
||||
}
|
||||
|
||||
dedup_absolute_paths(
|
||||
writable_roots
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_write())
|
||||
.filter_map(|entry| {
|
||||
resolve_file_system_path(&entry.path, cwd_absolute.as_ref())
|
||||
}),
|
||||
)
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_write())
|
||||
.filter_map(|entry| resolve_file_system_path(&entry.path, cwd_absolute.as_ref()))
|
||||
.collect(),
|
||||
)
|
||||
.into_iter()
|
||||
@@ -581,16 +543,6 @@ fn resolve_file_system_path(
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_root_path_for_cwd(cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
let root = cwd
|
||||
.as_path()
|
||||
.ancestors()
|
||||
.last()
|
||||
.unwrap_or_else(|| panic!("cwd must have a filesystem root"));
|
||||
AbsolutePathBuf::from_absolute_path(root)
|
||||
.unwrap_or_else(|err| panic!("cwd root must be an absolute path: {err}"))
|
||||
}
|
||||
|
||||
fn resolve_file_system_special_path(
|
||||
value: &FileSystemSpecialPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
|
||||
@@ -3352,56 +3352,6 @@ mod tests {
|
||||
assert!(writable.has_full_disk_write_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_treats_root_with_carveouts_as_scoped_access() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let cwd_absolute =
|
||||
AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute tempdir");
|
||||
let root = cwd_absolute
|
||||
.as_path()
|
||||
.ancestors()
|
||||
.last()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())
|
||||
.expect("filesystem root");
|
||||
let blocked = AbsolutePathBuf::resolve_path_against_base("blocked", cwd.path())
|
||||
.expect("resolve blocked");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: blocked.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!policy.has_full_disk_read_access());
|
||||
assert!(!policy.has_full_disk_write_access());
|
||||
assert_eq!(
|
||||
policy.get_readable_roots_with_cwd(cwd.path()),
|
||||
vec![root.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
policy.get_unreadable_roots_with_cwd(cwd.path()),
|
||||
vec![blocked.clone()]
|
||||
);
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
|
||||
assert_eq!(writable_roots.len(), 1);
|
||||
assert_eq!(writable_roots[0].root, root);
|
||||
assert!(
|
||||
writable_roots[0]
|
||||
.read_only_subpaths
|
||||
.iter()
|
||||
.any(|path| path.as_path() == blocked.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_derives_effective_paths() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -130,52 +130,36 @@ async fn collect_output_until_exit(
|
||||
}
|
||||
|
||||
async fn wait_for_python_repl_ready(
|
||||
writer: &tokio::sync::mpsc::Sender<Vec<u8>>,
|
||||
output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
|
||||
timeout_ms: u64,
|
||||
newline: &str,
|
||||
ready_marker: &str,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let mut collected = Vec::new();
|
||||
let marker = "__codex_pty_ready__";
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
|
||||
let probe_window = tokio::time::Duration::from_millis(if cfg!(windows) { 750 } else { 250 });
|
||||
|
||||
while tokio::time::Instant::now() < deadline {
|
||||
writer
|
||||
.send(format!("print('{marker}'){newline}").into_bytes())
|
||||
.await?;
|
||||
|
||||
let probe_deadline = tokio::time::Instant::now() + probe_window;
|
||||
loop {
|
||||
let now = tokio::time::Instant::now();
|
||||
if now >= deadline || now >= probe_deadline {
|
||||
break;
|
||||
}
|
||||
let remaining = std::cmp::min(
|
||||
deadline.saturating_duration_since(now),
|
||||
probe_deadline.saturating_duration_since(now),
|
||||
);
|
||||
match tokio::time::timeout(remaining, output_rx.recv()).await {
|
||||
Ok(Ok(chunk)) => {
|
||||
collected.extend_from_slice(&chunk);
|
||||
if String::from_utf8_lossy(&collected).contains(marker) {
|
||||
return Ok(collected);
|
||||
}
|
||||
let now = tokio::time::Instant::now();
|
||||
let remaining = deadline.saturating_duration_since(now);
|
||||
match tokio::time::timeout(remaining, output_rx.recv()).await {
|
||||
Ok(Ok(chunk)) => {
|
||||
collected.extend_from_slice(&chunk);
|
||||
if String::from_utf8_lossy(&collected).contains(ready_marker) {
|
||||
return Ok(collected);
|
||||
}
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
|
||||
anyhow::bail!(
|
||||
"PTY output closed while waiting for Python REPL readiness: {:?}",
|
||||
String::from_utf8_lossy(&collected)
|
||||
);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
|
||||
anyhow::bail!(
|
||||
"PTY output closed while waiting for Python REPL readiness: {:?}",
|
||||
String::from_utf8_lossy(&collected)
|
||||
);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"timed out waiting for Python REPL readiness in PTY: {:?}",
|
||||
"timed out waiting for Python REPL readiness marker {ready_marker:?} in PTY: {:?}",
|
||||
String::from_utf8_lossy(&collected)
|
||||
);
|
||||
}
|
||||
@@ -254,10 +238,17 @@ async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let ready_marker = "__codex_pty_ready__";
|
||||
let args = vec![
|
||||
"-i".to_string(),
|
||||
"-q".to_string(),
|
||||
"-c".to_string(),
|
||||
format!("print('{ready_marker}')"),
|
||||
];
|
||||
let env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
let spawned = spawn_pty_process(
|
||||
&python,
|
||||
&[],
|
||||
&args,
|
||||
Path::new("."),
|
||||
&env_map,
|
||||
&None,
|
||||
@@ -269,7 +260,7 @@ async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()> {
|
||||
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
|
||||
let startup_timeout_ms = if cfg!(windows) { 10_000 } else { 5_000 };
|
||||
let mut output =
|
||||
wait_for_python_repl_ready(&writer, &mut output_rx, startup_timeout_ms, newline).await?;
|
||||
wait_for_python_repl_ready(&mut output_rx, startup_timeout_ms, ready_marker).await?;
|
||||
writer
|
||||
.send(format!("print('hello from pty'){newline}").into_bytes())
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user