mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
292 lines
9.8 KiB
Markdown
292 lines
9.8 KiB
Markdown
# PR #2309: Fix AF_UNIX, sockpair, recvfrom in linux sandbox
|
||
|
||
- URL: https://github.com/openai/codex/pull/2309
|
||
- Author: wpt-oai
|
||
- Created: 2025-08-14 19:10:37 UTC
|
||
- Updated: 2025-08-15 00:12:48 UTC
|
||
- Changes: +136/-4, Files changed: 4, Commits: 7
|
||
|
||
## Description
|
||
|
||
When using codex-tui on a linux system I was unable to run `cargo clippy` inside of codex due to:
|
||
```
|
||
[pid 3548377] socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, <unfinished ...>
|
||
[pid 3548370] close(8 <unfinished ...>
|
||
[pid 3548377] <... socketpair resumed>0x7ffb97f4ed60) = -1 EPERM (Operation not permitted)
|
||
```
|
||
And
|
||
```
|
||
3611300 <... recvfrom resumed>0x708b8b5cffe0, 8, 0, NULL, NULL) = -1 EPERM (Operation not permitted)
|
||
```
|
||
|
||
This PR:
|
||
* Fixes a bug that disallowed AF_UNIX to allow it on `socket()`
|
||
* Adds recvfrom() to the syscall allow list, this should be fine since we disable opening new sockets. But we should validate there is not a open socket inheritance issue.
|
||
* Allow socketpair to be called for AF_UNIX
|
||
* Adds tests for AF_UNIX components
|
||
* All of which allows running `cargo clippy` within the sandbox on linux, and possibly other tooling using a fork server model + AF_UNIX comms.
|
||
|
||
## Full Diff
|
||
|
||
```diff
|
||
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
|
||
index 41392633be..feec0b04b8 100644
|
||
--- a/codex-rs/Cargo.lock
|
||
+++ b/codex-rs/Cargo.lock
|
||
@@ -733,6 +733,7 @@ dependencies = [
|
||
"codex-common",
|
||
"codex-core",
|
||
"codex-ollama",
|
||
+ "libc",
|
||
"owo-colors",
|
||
"predicates",
|
||
"serde_json",
|
||
diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml
|
||
index aee480d7b4..b7c20df321 100644
|
||
--- a/codex-rs/exec/Cargo.toml
|
||
+++ b/codex-rs/exec/Cargo.toml
|
||
@@ -41,5 +41,6 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||
|
||
[dev-dependencies]
|
||
assert_cmd = "2"
|
||
+libc = "0.2"
|
||
predicates = "3"
|
||
tempfile = "3.13.0"
|
||
diff --git a/codex-rs/exec/tests/sandbox.rs b/codex-rs/exec/tests/sandbox.rs
|
||
index 8cc31bba58..22caf8abc6 100644
|
||
--- a/codex-rs/exec/tests/sandbox.rs
|
||
+++ b/codex-rs/exec/tests/sandbox.rs
|
||
@@ -4,7 +4,10 @@
|
||
use codex_core::protocol::SandboxPolicy;
|
||
use codex_core::spawn::StdioPolicy;
|
||
use std::collections::HashMap;
|
||
+use std::future::Future;
|
||
+use std::io;
|
||
use std::path::PathBuf;
|
||
+use std::process::ExitStatus;
|
||
use tokio::process::Child;
|
||
|
||
#[cfg(target_os = "macos")]
|
||
@@ -90,3 +93,128 @@ if __name__ == '__main__':
|
||
let status = child.wait().await.expect("should wait for child process");
|
||
assert!(status.success(), "python exited with {status:?}");
|
||
}
|
||
+
|
||
+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";
|
||
+
|
||
+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 mut child = spawn_command_under_sandbox(
|
||
+ cmds,
|
||
+ policy,
|
||
+ std::env::current_dir().expect("should be able to get current dir"),
|
||
+ 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)
|
||
+ }
|
||
+}
|
||
diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs
|
||
index e13e3c8b75..5bc96130dd 100644
|
||
--- a/codex-rs/linux-sandbox/src/landlock.rs
|
||
+++ b/codex-rs/linux-sandbox/src/landlock.rs
|
||
@@ -104,7 +104,9 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
|
||
deny_syscall(libc::SYS_sendto);
|
||
deny_syscall(libc::SYS_sendmsg);
|
||
deny_syscall(libc::SYS_sendmmsg);
|
||
- deny_syscall(libc::SYS_recvfrom);
|
||
+ // NOTE: allowing recvfrom allows some tools like: `cargo clippy` to run
|
||
+ // with their socketpair + child processes for sub-proc management
|
||
+ // deny_syscall(libc::SYS_recvfrom);
|
||
deny_syscall(libc::SYS_recvmsg);
|
||
deny_syscall(libc::SYS_recvmmsg);
|
||
deny_syscall(libc::SYS_getsockopt);
|
||
@@ -115,12 +117,12 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
|
||
let unix_only_rule = SeccompRule::new(vec![SeccompCondition::new(
|
||
0, // first argument (domain)
|
||
SeccompCmpArgLen::Dword,
|
||
- SeccompCmpOp::Eq,
|
||
+ SeccompCmpOp::Ne,
|
||
libc::AF_UNIX as u64,
|
||
)?])?;
|
||
|
||
- rules.insert(libc::SYS_socket, vec![unix_only_rule]);
|
||
- rules.insert(libc::SYS_socketpair, vec![]); // always deny (Unix can use socketpair but fine, keep open?)
|
||
+ rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]);
|
||
+ rules.insert(libc::SYS_socketpair, vec![unix_only_rule]); // always deny (Unix can use socketpair but fine, keep open?)
|
||
|
||
let filter = SeccompFilter::new(
|
||
rules,
|
||
```
|
||
|
||
## Review Comments
|
||
|
||
### codex-rs/exec/Cargo.toml
|
||
|
||
- Created: 2025-08-14 23:54:42 UTC | Link: https://github.com/openai/codex/pull/2309#discussion_r2277964769
|
||
|
||
```diff
|
||
@@ -43,3 +43,4 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||
assert_cmd = "2"
|
||
predicates = "3"
|
||
tempfile = "3.13.0"
|
||
+libc = "0.2.172"
|
||
```
|
||
|
||
> alpha?
|
||
>
|
||
> Still trying to find a way to enforce this in CI...
|
||
>
|
||
> We do have https://github.com/openai/codex/blob/main/.vscode/settings.json if you edit the file in VS Code, but `cargo add` skips that...
|
||
|
||
### codex-rs/exec/tests/sandbox.rs
|
||
|
||
- Created: 2025-08-14 23:57:58 UTC | Link: https://github.com/openai/codex/pull/2309#discussion_r2277970216
|
||
|
||
```diff
|
||
@@ -4,9 +4,53 @@
|
||
use codex_core::protocol::SandboxPolicy;
|
||
use codex_core::spawn::StdioPolicy;
|
||
use std::collections::HashMap;
|
||
+use std::future::Future;
|
||
+use std::io;
|
||
use std::path::PathBuf;
|
||
+use std::process::ExitStatus;
|
||
use tokio::process::Child;
|
||
|
||
+const IN_SANDBOX_ENV_VAR: &str = "IN_SANDBOX";
|
||
+
|
||
+pub async fn run_code_under_sandbox<F, Fut>(
|
||
```
|
||
|
||
> Can we put this at the end of the file? I try to have us keep the most important stuff at the top (module small-ish struct defs and consts). Arguably the `spawn_command_under_sandbox()` defs should go below the tests, too...
|
||
|
||
- Created: 2025-08-14 23:58:56 UTC | Link: https://github.com/openai/codex/pull/2309#discussion_r2277972679
|
||
|
||
```diff
|
||
@@ -90,3 +134,85 @@ if __name__ == '__main__':
|
||
let status = child.wait().await.expect("should wait for child process");
|
||
assert!(status.success(), "python exited with {status:?}");
|
||
}
|
||
+
|
||
+#[tokio::test]
|
||
+async fn allow_unix_socketpair_recvfrom() {
|
||
+ run_code_under_sandbox(
|
||
+ "allow_unix_socketpair_recvfrom",
|
||
+ &SandboxPolicy::ReadOnly,
|
||
+ || async {
|
||
```
|
||
|
||
> The one thing that's not great about this approach is the 2-3 extra levels of indenting...
|
||
>
|
||
> Do you want to put everything inside `unsafe` in its own function? |