mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
Isolate Linux sandbox IPC namespace
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user