Compare commits

...

1 Commits

Author SHA1 Message Date
Jeremy Rose
f89641fced test widened sandbox 2025-09-12 14:06:12 -07:00
3 changed files with 143 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import os
import sys
import time
import select
import ctypes
def main() -> int:
# Fork the child that will exit shortly.
pid = os.fork()
if pid == 0:
time.sleep(0.2)
sys.exit(0)
# Query process group via sysctl MIB: {CTL_KERN, KERN_PROC, KERN_PROC_PGRP, pgid}.
libSystem = ctypes.CDLL("/usr/lib/libSystem.B.dylib")
sysctl = libSystem.sysctl
sysctl.argtypes = [
ctypes.POINTER(ctypes.c_int),
ctypes.c_uint,
ctypes.c_void_p,
ctypes.POINTER(ctypes.c_size_t),
ctypes.c_void_p,
ctypes.c_size_t,
]
sysctl.restype = ctypes.c_int
CTL_KERN = 1
KERN_PROC = 14
KERN_PROC_PGRP = 2
pgid = os.getpgid(0)
mib = (ctypes.c_int * 4)(CTL_KERN, KERN_PROC, KERN_PROC_PGRP, pgid)
sz = ctypes.c_size_t(0)
rc = sysctl(mib, 4, None, ctypes.byref(sz), None, 0)
if rc != 0 or sz.value == 0:
return 1
buf = (ctypes.c_char * sz.value)()
rc2 = sysctl(mib, 4, buf, ctypes.byref(sz), None, 0)
if rc2 != 0 or sz.value == 0:
return 1
# Register kqueue EVFILT_PROC NOTE_EXIT for child pid and wait.
kq = select.kqueue()
kev = select.kevent(
pid,
filter=select.KQ_FILTER_PROC,
flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE,
fflags=select.KQ_NOTE_EXIT,
)
kq.control([kev], 0, 0)
events = kq.control(None, 1, None)
ok_ev = len(events) == 1 and (events[0].fflags & select.KQ_NOTE_EXIT) != 0
try:
os.waitpid(pid, 0)
except Exception:
pass
return 0 if ok_ev else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
sleep 5 & pid=$!
sleep 0.05
# Capability probe
kill -0 "$pid" 2>/dev/null || exit 1
# Send a terminating signal and ensure the child exits
kill -TERM "$pid" || exit 1
wait "$pid" || true
exit 0

View File

@@ -194,6 +194,70 @@ async fn python_getpwuid_works_under_seatbelt() {
assert!(status.success(), "python exited with {status:?}");
}
/// Exercises (allow process-info* (target same-sandbox)) and
/// (sysctl-read
/// (sysctl-name-prefix "kern.proc.pgrp.")
/// (sysctl-name-prefix "kern.proc.pid."))
#[tokio::test]
async fn process_info_allowed_within_same_sandbox() {
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
return;
}
let policy = SandboxPolicy::ReadOnly;
let py_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/seatbelt_kqueue_sysctl.py");
let mut child = spawn_command_under_seatbelt(
vec!["python3".to_string(), py_path.to_string_lossy().to_string()],
&policy,
std::env::current_dir().expect("should be able to get current dir"),
StdioPolicy::RedirectForShellTool,
HashMap::new(),
)
.await
.expect("should be able to spawn python under seatbelt");
let status = child
.wait()
.await
.expect("should be able to wait for child process");
assert!(status.success(), "python exited with {status:?}");
}
/// Exercises (allow signal (target same-sandbox))
#[tokio::test]
async fn signals_parent_to_child_allowed_within_same_sandbox() {
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
return;
}
let policy = SandboxPolicy::ReadOnly;
let sh_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/seatbelt_signal_parent_child.sh");
let mut child = spawn_command_under_seatbelt(
vec![
"/bin/bash".to_string(),
sh_path.to_string_lossy().to_string(),
],
&policy,
std::env::current_dir().expect("should be able to get current dir"),
StdioPolicy::RedirectForShellTool,
HashMap::new(),
)
.await
.expect("should be able to spawn bash under seatbelt");
let status = child
.wait()
.await
.expect("should be able to wait for child process");
assert!(status.success(), "bash exited with {status:?}");
}
#[expect(clippy::expect_used)]
fn create_test_scenario(tmp: &TempDir) -> TestScenario {
let repo_parent = tmp.path().to_path_buf();