Compare commits

...

1 Commits

Author SHA1 Message Date
Eva Wong
c3ff56bae9 Isolate Linux sandbox IPC namespace 2026-04-30 15:22:38 -07:00
4 changed files with 155 additions and 2 deletions

View File

@@ -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

View File

@@ -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());
@@ -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());
if options.network_mode.should_unshare_network() {
args.push("--unshare-net".to_string());
}
@@ -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(),

View File

@@ -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(),

View File

@@ -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();
@@ -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]
@@ -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;
}
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()) };
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();