Files
codex/prs/bolinfest/PR-2309.md
2025-09-02 15:17:45 -07:00

292 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 dont 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?