Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion codex-rs/linux-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ commands that would enter the bubblewrap path.
writable roots are blocked by mounting `/dev/null` on the symlink or first
missing component.
- When bubblewrap is active, the helper explicitly isolates the user namespace via
`--unshare-user` and the PID namespace via `--unshare-pid`.
`--unshare-user`, the PID namespace via `--unshare-pid`, and the IPC namespace
via `--unshare-ipc`.
- When bubblewrap is active and network is restricted without proxy routing, the helper also
isolates the network namespace via `--unshare-net`.
- In managed proxy mode, the helper uses `--unshare-net` plus an internal
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/linux-sandbox/src/bwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
// not need ambient CAP_SYS_ADMIN to create the remaining namespaces.
"--unshare-user".to_string(),
"--unshare-pid".to_string(),
"--unshare-ipc".to_string(),
];
if options.network_mode.should_unshare_network() {
args.push("--unshare-net".to_string());
Expand Down Expand Up @@ -322,6 +323,7 @@ fn create_bwrap_flags(
// auto-enable behavior, which is skipped when the caller runs as uid 0.
args.push("--unshare-user".to_string());
args.push("--unshare-pid".to_string());
args.push("--unshare-ipc".to_string());
Comment thread
evawong-oai marked this conversation as resolved.
if options.network_mode.should_unshare_network() {
args.push("--unshare-net".to_string());
}
Expand Down Expand Up @@ -1402,6 +1404,7 @@ mod tests {
"/".to_string(),
"--unshare-user".to_string(),
"--unshare-pid".to_string(),
"--unshare-ipc".to_string(),
"--unshare-net".to_string(),
"--proc".to_string(),
"/proc".to_string(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/linux-sandbox/src/linux_run_main_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ fn inserts_bwrap_argv0_before_command_separator() {
"/dev".to_string(),
"--unshare-user".to_string(),
"--unshare-pid".to_string(),
"--unshare-ipc".to_string(),
"--proc".to_string(),
"/proc".to_string(),
"--argv0".to_string(),
Expand Down
150 changes: 149 additions & 1 deletion codex-rs/linux-sandbox/tests/suite/landlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,51 @@ const NETWORK_TIMEOUT_MS: u64 = 10_000;
const NETWORK_TIMEOUT_MS: u64 = 10_000;

const BWRAP_UNAVAILABLE_ERR: &str = "build-time bubblewrap is not available in this build.";
const SYSV_IPC_PROBE_SHMID_ENV: &str = "CODEX_SYSV_IPC_PROBE_SHMID";
const SYSV_IPC_PROBE_SECRET_ENV: &str = "CODEX_SYSV_IPC_PROBE_SECRET";

struct HostSysvSharedMemory {
shmid: libc::c_int,
addr: *mut libc::c_void,
}

impl HostSysvSharedMemory {
fn new(contents: &[u8]) -> std::io::Result<Self> {
let shmid =
unsafe { libc::shmget(libc::IPC_PRIVATE, contents.len(), libc::IPC_CREAT | 0o600) };
if shmid == -1 {
return Err(std::io::Error::last_os_error());
}

let addr = unsafe { libc::shmat(shmid, std::ptr::null(), 0) };
if shmat_failed(addr) {
let err = std::io::Error::last_os_error();
unsafe {
libc::shmctl(shmid, libc::IPC_RMID, std::ptr::null_mut());
}
return Err(err);
}

let shared_bytes =
unsafe { std::slice::from_raw_parts_mut(addr.cast::<u8>(), contents.len()) };
shared_bytes.copy_from_slice(contents);

Ok(Self { shmid, addr })
}
}

impl Drop for HostSysvSharedMemory {
fn drop(&mut self) {
unsafe {
libc::shmdt(self.addr);
libc::shmctl(self.shmid, libc::IPC_RMID, std::ptr::null_mut());
}
}
}

fn shmat_failed(addr: *mut libc::c_void) -> bool {
addr == (-1_isize) as *mut libc::c_void
}

fn create_env_from_core_vars() -> HashMap<String, String> {
let policy = ShellEnvironmentPolicy::default();
Expand Down Expand Up @@ -245,7 +290,7 @@ fn expect_denied(

#[tokio::test]
async fn test_root_read() {
run_cmd(&["ls", "-l", "/bin"], &[], SHORT_TIMEOUT_MS).await;
run_cmd(&["ls", "-l", "/bin"], &[], LONG_TIMEOUT_MS).await;
}

#[tokio::test]
Expand Down Expand Up @@ -349,6 +394,109 @@ async fn bwrap_preserves_writable_dev_shm_bind_mount() {
);
}

#[test]
fn sysv_ipc_probe_helper() {
let Ok(shmid) = std::env::var(SYSV_IPC_PROBE_SHMID_ENV) else {
return;
};
let expected_secret =
std::env::var(SYSV_IPC_PROBE_SECRET_ENV).expect("expected probe secret env var");
let shmid = shmid
.parse::<libc::c_int>()
.expect("probe shmid should be an integer");

let addr = unsafe { libc::shmat(shmid, std::ptr::null(), 0) };
if shmat_failed(addr) {
let err = std::io::Error::last_os_error();
assert_eq!(
err.raw_os_error(),
Some(libc::EINVAL),
"expected private IPC namespace to hide host SysV shared memory segment {shmid}, got {err}"
);
return;
Comment thread
evawong-oai marked this conversation as resolved.
}

let mut segment_metadata = unsafe { std::mem::zeroed::<libc::shmid_ds>() };
let shmctl_result = unsafe { libc::shmctl(shmid, libc::IPC_STAT, &mut segment_metadata) };
if shmctl_result == -1 {
let err = std::io::Error::last_os_error();
unsafe {
libc::shmdt(addr);
}
panic!("failed to stat SysV shared memory segment {shmid}: {err}");
}
let segment_size = segment_metadata.shm_segsz;
if expected_secret.len() > segment_size {
unsafe {
libc::shmdt(addr);
}
panic!(
"SysV shared memory segment {shmid} size {segment_size} is smaller than probe secret size {}",
expected_secret.len()
);
}

let observed = unsafe { std::slice::from_raw_parts(addr.cast::<u8>(), expected_secret.len()) };
Comment thread
evawong-oai marked this conversation as resolved.
let observed_secret = String::from_utf8_lossy(observed).into_owned();
unsafe {
libc::shmdt(addr);
}

panic!(
"sandboxed command attached to host SysV shared memory segment {shmid} and read {observed_secret:?}"
);
}

#[tokio::test]
async fn bwrap_uses_private_ipc_namespace() {
if should_skip_bwrap_tests().await {
eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable");
return;
}

let secret = b"codex-sysv-ipc-host-secret";
let host_segment = match HostSysvSharedMemory::new(secret) {
Ok(host_segment) => host_segment,
Err(err) => {
eprintln!("skipping bwrap test: failed to create SysV shared memory segment: {err}");
return;
}
};

let test_exe = std::env::current_exe()
.expect("current test executable")
.to_string_lossy()
.into_owned();
let shmid_env = format!("{SYSV_IPC_PROBE_SHMID_ENV}={}", host_segment.shmid);
let secret_env = format!(
"{SYSV_IPC_PROBE_SECRET_ENV}={}",
String::from_utf8_lossy(secret)
);
let output = run_cmd_result_with_writable_roots(
&[
"env",
shmid_env.as_str(),
secret_env.as_str(),
test_exe.as_str(),
"sysv_ipc_probe_helper",
"--exact",
"--nocapture",
],
&[],
NETWORK_TIMEOUT_MS,
/*use_legacy_landlock*/ false,
/*network_access*/ true,
)
.await
.expect("sandboxed SysV IPC probe should execute");

assert_eq!(
output.exit_code, 0,
"sandboxed SysV IPC probe failed\nstdout:\n{}\nstderr:\n{}",
output.stdout.text, output.stderr.text
);
}

#[tokio::test]
async fn test_writable_root() {
let tmpdir = tempfile::tempdir().unwrap();
Expand Down
Loading