This PR introduces a `codex-utils-cargo-bin` utility crate that wraps/replaces our use of `assert_cmd::Command` and `escargot::CargoBuild`. As you can infer from the introduction of `buck_project_root()` in this PR, I am attempting to make it possible to build Codex under [Buck2](https://buck2.build) as well as `cargo`. With Buck2, I hope to achieve faster incremental local builds (largely due to Buck2's [dice](https://buck2.build/docs/insights_and_knowledge/modern_dice/) build strategy, as well as benefits from its local build daemon) as well as faster CI builds if we invest in remote execution and caching. See https://buck2.build/docs/getting_started/what_is_buck2/#why-use-buck2-key-advantages for more details about the performance advantages of Buck2. Buck2 enforces stronger requirements in terms of build and test isolation. It discourages assumptions about absolute paths (which is key to enabling remote execution). Because the `CARGO_BIN_EXE_*` environment variables that Cargo provides are absolute paths (which `assert_cmd::Command` reads), this is a problem for Buck2, which is why we need this `codex-utils-cargo-bin` utility. My WIP-Buck2 setup sets the `CARGO_BIN_EXE_*` environment variables passed to a `rust_test()` build rule as relative paths. `codex-utils-cargo-bin` will resolve these values to absolute paths, when necessary. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/8496). * #8498 * __->__ #8496
363 lines
11 KiB
Rust
363 lines
11 KiB
Rust
#![cfg(unix)]
|
||
use codex_core::protocol::SandboxPolicy;
|
||
use codex_core::spawn::StdioPolicy;
|
||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||
use std::collections::HashMap;
|
||
use std::future::Future;
|
||
use std::io;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
use std::process::ExitStatus;
|
||
use tokio::fs::create_dir_all;
|
||
use tokio::process::Child;
|
||
|
||
#[cfg(target_os = "macos")]
|
||
async fn spawn_command_under_sandbox(
|
||
command: Vec<String>,
|
||
command_cwd: PathBuf,
|
||
sandbox_policy: &SandboxPolicy,
|
||
sandbox_cwd: &Path,
|
||
stdio_policy: StdioPolicy,
|
||
env: HashMap<String, String>,
|
||
) -> std::io::Result<Child> {
|
||
use codex_core::seatbelt::spawn_command_under_seatbelt;
|
||
spawn_command_under_seatbelt(
|
||
command,
|
||
command_cwd,
|
||
sandbox_policy,
|
||
sandbox_cwd,
|
||
stdio_policy,
|
||
env,
|
||
)
|
||
.await
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
async fn spawn_command_under_sandbox(
|
||
command: Vec<String>,
|
||
command_cwd: PathBuf,
|
||
sandbox_policy: &SandboxPolicy,
|
||
sandbox_cwd: &Path,
|
||
stdio_policy: StdioPolicy,
|
||
env: HashMap<String, String>,
|
||
) -> std::io::Result<Child> {
|
||
use codex_core::landlock::spawn_command_under_linux_sandbox;
|
||
let codex_linux_sandbox_exe = codex_utils_cargo_bin::cargo_bin("codex-exec")
|
||
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?;
|
||
spawn_command_under_linux_sandbox(
|
||
codex_linux_sandbox_exe,
|
||
command,
|
||
command_cwd,
|
||
sandbox_policy,
|
||
sandbox_cwd,
|
||
stdio_policy,
|
||
env,
|
||
)
|
||
.await
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn python_multiprocessing_lock_works_under_sandbox() {
|
||
core_test_support::skip_if_sandbox!();
|
||
#[cfg(target_os = "macos")]
|
||
let writable_roots = Vec::<AbsolutePathBuf>::new();
|
||
|
||
// From https://man7.org/linux/man-pages/man7/sem_overview.7.html
|
||
//
|
||
// > On Linux, named semaphores are created in a virtual filesystem,
|
||
// > normally mounted under /dev/shm.
|
||
#[cfg(target_os = "linux")]
|
||
let writable_roots: Vec<AbsolutePathBuf> = vec!["/dev/shm".try_into().unwrap()];
|
||
|
||
let policy = SandboxPolicy::WorkspaceWrite {
|
||
writable_roots,
|
||
network_access: false,
|
||
exclude_tmpdir_env_var: false,
|
||
exclude_slash_tmp: false,
|
||
};
|
||
|
||
let python_code = r#"import multiprocessing
|
||
from multiprocessing import Lock, Process
|
||
|
||
def f(lock):
|
||
with lock:
|
||
print("Lock acquired in child process")
|
||
|
||
if __name__ == '__main__':
|
||
lock = Lock()
|
||
p = Process(target=f, args=(lock,))
|
||
p.start()
|
||
p.join()
|
||
"#;
|
||
|
||
let command_cwd = std::env::current_dir().expect("should be able to get current dir");
|
||
let sandbox_cwd = command_cwd.clone();
|
||
let mut child = spawn_command_under_sandbox(
|
||
vec![
|
||
"python3".to_string(),
|
||
"-c".to_string(),
|
||
python_code.to_string(),
|
||
],
|
||
command_cwd,
|
||
&policy,
|
||
sandbox_cwd.as_path(),
|
||
StdioPolicy::Inherit,
|
||
HashMap::new(),
|
||
)
|
||
.await
|
||
.expect("should be able to spawn python under sandbox");
|
||
|
||
let status = child.wait().await.expect("should wait for child process");
|
||
assert!(status.success(), "python exited with {status:?}");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn python_getpwuid_works_under_sandbox() {
|
||
core_test_support::skip_if_sandbox!();
|
||
|
||
if std::process::Command::new("python3")
|
||
.arg("--version")
|
||
.status()
|
||
.is_err()
|
||
{
|
||
eprintln!("python3 not found in PATH, skipping test.");
|
||
return;
|
||
}
|
||
|
||
let policy = SandboxPolicy::ReadOnly;
|
||
let command_cwd = std::env::current_dir().expect("should be able to get current dir");
|
||
let sandbox_cwd = command_cwd.clone();
|
||
|
||
let mut child = spawn_command_under_sandbox(
|
||
vec![
|
||
"python3".to_string(),
|
||
"-c".to_string(),
|
||
"import pwd, os; print(pwd.getpwuid(os.getuid()))".to_string(),
|
||
],
|
||
command_cwd,
|
||
&policy,
|
||
sandbox_cwd.as_path(),
|
||
StdioPolicy::RedirectForShellTool,
|
||
HashMap::new(),
|
||
)
|
||
.await
|
||
.expect("should be able to spawn python under sandbox");
|
||
|
||
let status = child
|
||
.wait()
|
||
.await
|
||
.expect("should be able to wait for child process");
|
||
assert!(status.success(), "python exited with {status:?}");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn sandbox_distinguishes_command_and_policy_cwds() {
|
||
core_test_support::skip_if_sandbox!();
|
||
let temp = tempfile::tempdir().expect("should be able to create temp dir");
|
||
let sandbox_root = temp.path().join("sandbox");
|
||
let command_root = temp.path().join("command");
|
||
create_dir_all(&sandbox_root).await.expect("mkdir");
|
||
create_dir_all(&command_root).await.expect("mkdir");
|
||
let canonical_sandbox_root = tokio::fs::canonicalize(&sandbox_root)
|
||
.await
|
||
.expect("canonicalize sandbox root");
|
||
let canonical_allowed_path = canonical_sandbox_root.join("allowed.txt");
|
||
|
||
let disallowed_path = command_root.join("forbidden.txt");
|
||
|
||
// Note writable_roots is empty: verify that `canonical_allowed_path` is
|
||
// writable only because it is under the sandbox policy cwd, not because it
|
||
// is under a writable root.
|
||
let policy = SandboxPolicy::WorkspaceWrite {
|
||
writable_roots: vec![],
|
||
network_access: false,
|
||
exclude_tmpdir_env_var: true,
|
||
exclude_slash_tmp: true,
|
||
};
|
||
|
||
// Attempt to write inside the command cwd, which is outside of the sandbox policy cwd.
|
||
let mut child = spawn_command_under_sandbox(
|
||
vec![
|
||
"bash".to_string(),
|
||
"-lc".to_string(),
|
||
"echo forbidden > forbidden.txt".to_string(),
|
||
],
|
||
command_root.clone(),
|
||
&policy,
|
||
canonical_sandbox_root.as_path(),
|
||
StdioPolicy::Inherit,
|
||
HashMap::new(),
|
||
)
|
||
.await
|
||
.expect("should spawn command writing to forbidden path");
|
||
|
||
let status = child
|
||
.wait()
|
||
.await
|
||
.expect("should wait for forbidden command");
|
||
assert!(
|
||
!status.success(),
|
||
"sandbox unexpectedly allowed writing to command cwd: {status:?}"
|
||
);
|
||
let forbidden_exists = tokio::fs::try_exists(&disallowed_path)
|
||
.await
|
||
.expect("try_exists failed");
|
||
assert!(
|
||
!forbidden_exists,
|
||
"forbidden path should not have been created"
|
||
);
|
||
|
||
// Writing to the sandbox policy cwd after changing directories into it should succeed.
|
||
let mut child = spawn_command_under_sandbox(
|
||
vec![
|
||
"/usr/bin/touch".to_string(),
|
||
canonical_allowed_path.to_string_lossy().into_owned(),
|
||
],
|
||
command_root,
|
||
&policy,
|
||
canonical_sandbox_root.as_path(),
|
||
StdioPolicy::Inherit,
|
||
HashMap::new(),
|
||
)
|
||
.await
|
||
.expect("should spawn command writing to sandbox root");
|
||
|
||
let status = child.wait().await.expect("should wait for allowed command");
|
||
assert!(
|
||
status.success(),
|
||
"sandbox blocked allowed write: {status:?}"
|
||
);
|
||
let allowed_exists = tokio::fs::try_exists(&canonical_allowed_path)
|
||
.await
|
||
.expect("try_exists allowed failed");
|
||
assert!(allowed_exists, "allowed path should exist");
|
||
}
|
||
|
||
fn unix_sock_body() {
|
||
unsafe {
|
||
let mut fds = [0i32; 2];
|
||
let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr());
|
||
assert_eq!(
|
||
r,
|
||
0,
|
||
"socketpair(AF_UNIX, SOCK_DGRAM) failed: {}",
|
||
io::Error::last_os_error()
|
||
);
|
||
|
||
let msg = b"hello_unix";
|
||
// write() from one end (generic write is allowed)
|
||
let sent = libc::write(fds[0], msg.as_ptr() as *const libc::c_void, msg.len());
|
||
assert!(sent >= 0, "write() failed: {}", io::Error::last_os_error());
|
||
|
||
// recvfrom() on the other end. We don’t need the address for socketpair,
|
||
// so we pass null pointers for src address.
|
||
let mut buf = [0u8; 64];
|
||
let recvd = libc::recvfrom(
|
||
fds[1],
|
||
buf.as_mut_ptr() as *mut libc::c_void,
|
||
buf.len(),
|
||
0,
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
);
|
||
assert!(
|
||
recvd >= 0,
|
||
"recvfrom() failed: {}",
|
||
io::Error::last_os_error()
|
||
);
|
||
|
||
let recvd_slice = &buf[..(recvd as usize)];
|
||
assert_eq!(
|
||
recvd_slice,
|
||
&msg[..],
|
||
"payload mismatch: sent {} bytes, got {} bytes",
|
||
msg.len(),
|
||
recvd
|
||
);
|
||
|
||
// Also exercise AF_UNIX stream socketpair quickly to ensure AF_UNIX in general works.
|
||
let mut sfds = [0i32; 2];
|
||
let sr = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, sfds.as_mut_ptr());
|
||
assert_eq!(
|
||
sr,
|
||
0,
|
||
"socketpair(AF_UNIX, SOCK_STREAM) failed: {}",
|
||
io::Error::last_os_error()
|
||
);
|
||
let snt2 = libc::write(sfds[0], msg.as_ptr() as *const libc::c_void, msg.len());
|
||
assert!(
|
||
snt2 >= 0,
|
||
"write(stream) failed: {}",
|
||
io::Error::last_os_error()
|
||
);
|
||
let mut b2 = [0u8; 64];
|
||
let rcv2 = libc::recv(sfds[1], b2.as_mut_ptr() as *mut libc::c_void, b2.len(), 0);
|
||
assert!(
|
||
rcv2 >= 0,
|
||
"recv(stream) failed: {}",
|
||
io::Error::last_os_error()
|
||
);
|
||
|
||
// Clean up
|
||
let _ = libc::close(sfds[0]);
|
||
let _ = libc::close(sfds[1]);
|
||
let _ = libc::close(fds[0]);
|
||
let _ = libc::close(fds[1]);
|
||
}
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn allow_unix_socketpair_recvfrom() {
|
||
run_code_under_sandbox(
|
||
"allow_unix_socketpair_recvfrom",
|
||
&SandboxPolicy::ReadOnly,
|
||
|| async { unix_sock_body() },
|
||
)
|
||
.await
|
||
.expect("should be able to reexec");
|
||
}
|
||
|
||
const IN_SANDBOX_ENV_VAR: &str = "IN_SANDBOX";
|
||
|
||
#[expect(clippy::expect_used)]
|
||
pub async fn run_code_under_sandbox<F, Fut>(
|
||
test_selector: &str,
|
||
policy: &SandboxPolicy,
|
||
child_body: F,
|
||
) -> io::Result<Option<ExitStatus>>
|
||
where
|
||
F: FnOnce() -> Fut + Send + 'static,
|
||
Fut: Future<Output = ()> + Send + 'static,
|
||
{
|
||
if std::env::var(IN_SANDBOX_ENV_VAR).is_err() {
|
||
let exe = std::env::current_exe()?;
|
||
let mut cmds = vec![exe.to_string_lossy().into_owned(), "--exact".into()];
|
||
let mut stdio_policy = StdioPolicy::RedirectForShellTool;
|
||
// Allow for us to pass forward --nocapture / use the right stdio policy.
|
||
if std::env::args().any(|a| a == "--nocapture") {
|
||
cmds.push("--nocapture".into());
|
||
stdio_policy = StdioPolicy::Inherit;
|
||
}
|
||
cmds.push(test_selector.into());
|
||
|
||
// Your existing launcher:
|
||
let command_cwd = std::env::current_dir().expect("should be able to get current dir");
|
||
let sandbox_cwd = command_cwd.clone();
|
||
let mut child = spawn_command_under_sandbox(
|
||
cmds,
|
||
command_cwd,
|
||
policy,
|
||
sandbox_cwd.as_path(),
|
||
stdio_policy,
|
||
HashMap::from([("IN_SANDBOX".into(), "1".into())]),
|
||
)
|
||
.await?;
|
||
|
||
let status = child.wait().await?;
|
||
Ok(Some(status))
|
||
} else {
|
||
// Child branch: run the provided body.
|
||
child_body().await;
|
||
Ok(None)
|
||
}
|
||
}
|