mirror of
https://github.com/openai/codex.git
synced 2026-11-12 22:42:36 +00:00
Mount a minimal /dev in full-filesystem Bubblewrap sandboxes (#37349)
## Why Binding the full host filesystem into a network-isolated Bubblewrap sandbox also inherits the host's device tree. ## What changed - Overlay `/dev` with Bubblewrap's minimal device filesystem while preserving the root bind's `nodev` behavior. - Bind `/dev/shm` back into the sandbox when available so host and sandbox processes can continue to share memory files. ## Testing Add a managed-proxy integration test that verifies standard device nodes remain usable, `/dev` is not the host directory, and `/dev/shm` writes are shared with the host. GitOrigin-RevId: 1d91fe44affd792d2777adae8f2cce6d85d14103
This commit is contained in:
1 parent
4bb7ee3472
commit
912524d6fe
2 files changed
+90
No files matched your search
@@ -271,6 +271,13 @@ fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOption
|
|||||||
"--bind".to_string(),
|
"--bind".to_string(),
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
|
// Preserve nodev on the root bind while exposing only standard devices.
|
||||||
|
"--dev".to_string(),
|
||||||
|
"/dev".to_string(),
|
||||||
|
// Restore shared memory without exposing other host devices.
|
||||||
|
"--bind-try".to_string(),
|
||||||
|
"/dev/shm".to_string(),
|
||||||
|
"/dev/shm".to_string(),
|
||||||
// Always enter a fresh user namespace so root inside a container does
|
// Always enter a fresh user namespace so root inside a container does
|
||||||
// not need ambient CAP_SYS_ADMIN to create the remaining namespaces.
|
// not need ambient CAP_SYS_ADMIN to create the remaining namespaces.
|
||||||
"--unshare-user".to_string(),
|
"--unshare-user".to_string(),
|
||||||
@@ -1401,6 +1408,11 @@ mod tests {
|
|||||||
"--bind".to_string(),
|
"--bind".to_string(),
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
"/".to_string(),
|
"/".to_string(),
|
||||||
|
"--dev".to_string(),
|
||||||
|
"/dev".to_string(),
|
||||||
|
"--bind-try".to_string(),
|
||||||
|
"/dev/shm".to_string(),
|
||||||
|
"/dev/shm".to_string(),
|
||||||
"--unshare-user".to_string(),
|
"--unshare-user".to_string(),
|
||||||
"--unshare-pid".to_string(),
|
"--unshare-pid".to_string(),
|
||||||
"--unshare-net".to_string(),
|
"--unshare-net".to_string(),
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ use std::io::Read;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::process::Output;
|
use std::process::Output;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found";
|
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found";
|
||||||
@@ -169,6 +171,82 @@ fn linux_sandbox_command(
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn managed_proxy_full_filesystem_uses_minimal_dev_nodes() {
|
||||||
|
if let Some(skip_reason) = managed_proxy_skip_reason().await {
|
||||||
|
eprintln!("skipping managed proxy test: {skip_reason}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_dev = std::fs::metadata("/dev").expect("host /dev should exist");
|
||||||
|
let host_dev_id = format!("{}:{}", host_dev.dev(), host_dev.ino());
|
||||||
|
let shared_memory_file = match NamedTempFile::new_in("/dev/shm") {
|
||||||
|
Ok(file) => {
|
||||||
|
std::fs::write(file.path(), "host-before").expect("seed /dev/shm file");
|
||||||
|
Some(file)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("skipping /dev/shm interoperability check: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut env = create_env_from_core_vars();
|
||||||
|
strip_proxy_env(&mut env);
|
||||||
|
env.insert("HTTP_PROXY".to_string(), "http://127.0.0.1:9".to_string());
|
||||||
|
if let Some(file) = &shared_memory_file {
|
||||||
|
env.insert(
|
||||||
|
"CODEX_TEST_SHM_PATH".to_string(),
|
||||||
|
file.path().to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = run_linux_sandbox_direct(
|
||||||
|
&[
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
concat!(
|
||||||
|
"set -e; ",
|
||||||
|
"for node in null zero urandom; do test -c \"/dev/$node\"; done; ",
|
||||||
|
"printf test >/dev/null; ",
|
||||||
|
"head -c 1 /dev/zero >/dev/null; ",
|
||||||
|
"head -c 1 /dev/urandom >/dev/null; ",
|
||||||
|
"if command -v python3 >/dev/null 2>&1; then ",
|
||||||
|
"python3 -c 'import os; assert len(os.urandom(16)) == 16'; ",
|
||||||
|
"fi; ",
|
||||||
|
"if [ -n \"${CODEX_TEST_SHM_PATH:-}\" ]; then ",
|
||||||
|
"test \"$(cat \"$CODEX_TEST_SHM_PATH\")\" = host-before; ",
|
||||||
|
"printf sandbox-after >\"$CODEX_TEST_SHM_PATH\"; ",
|
||||||
|
"fi; ",
|
||||||
|
"stat -c '%d:%i' /dev",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
&PermissionProfile::Disabled,
|
||||||
|
/*allow_network_for_proxy*/ true,
|
||||||
|
env,
|
||||||
|
NETWORK_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output.status.success(),
|
||||||
|
true,
|
||||||
|
"standard devices should be usable; stderr={}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
String::from_utf8_lossy(&output.stdout).trim(),
|
||||||
|
host_dev_id,
|
||||||
|
"/dev should be synthetic rather than inherited from the host"
|
||||||
|
);
|
||||||
|
if let Some(file) = shared_memory_file {
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(file.path()).expect("read /dev/shm file"),
|
||||||
|
"sandbox-after"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn managed_proxy_bridges_release_command_output_after_exit() {
|
async fn managed_proxy_bridges_release_command_output_after_exit() {
|
||||||
if let Some(skip_reason) = managed_proxy_skip_reason().await {
|
if let Some(skip_reason) = managed_proxy_skip_reason().await {
|
||||||
|
|||||||
Reference in new issue
Block a user