security: annotate worker native unsafe sites

ober

0f9bf57c1f79184b4e2b0dd191b52b63e0a47b44

diff --git a/docs/ffi-audit.md b/docs/ffi-audit.md
index 63404e9..5ce9568 100644
--- a/docs/ffi-audit.md
+++ b/docs/ffi-audit.md
@@ -1,6 +1,6 @@
 # FFI Audit
 
-Updated 2026-07-27.
+Updated 2026-07-28.
 
 This is the committed audit record for K3-P1-01, parser-hardening phase 5.
 Reproduce the current inventory with:
@@ -41,9 +41,9 @@ Latest summary from `tools/ffi-audit-report.ss`:
     (c-file-count 5)
     (no-mangle-exports 196)
     (unsafe-sites 438)
-    (unsafe-sites-with-nearby-safety-comment 5)
-    (unsafe-sites-without-nearby-safety-comment 433)
-    (unsafe-comment-review-sites 433)
+    (unsafe-sites-with-nearby-safety-comment 29)
+    (unsafe-sites-without-nearby-safety-comment 409)
+    (unsafe-comment-review-sites 409)
     (export-review-sites 196))
   (vendor
     (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated)))
@@ -72,6 +72,11 @@ caller reconciliation.
 The native Rust crate now denies `unsafe_op_in_unsafe_fn`, so an unsafe
 function body no longer grants implicit permission for unsafe operations. Each
 unsafe operation still has to live inside an explicit `unsafe { ... }` block.
+On 2026-07-28, the `aproc.rs` and `seccomp.rs` unsafe sites covering the
+worker pre-exec launch path, fd handoff/cleanup, FFI buffer copies, rlimit
+decoding, wait4 outputs, and seccomp `prctl` calls gained nearby `SAFETY:`
+invariant comments. The generated inventory now reports 29 annotated native
+unsafe sites and 409 remaining unsafe review sites.
 
 Remaining work before closing K3-P1-01:
 
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 7f07dee..bf894ba 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -621,7 +621,11 @@ not started."
   export. The 35 exports previously marked removal candidates had their C ABI
   export markers removed; the remaining zero-caller exports are the 7
   standalone C/binary-tooling helpers explicitly retained in the decision
-  file. Remaining work: unsafe invariant comments.
+  file. On 2026-07-28, `aproc.rs` and `seccomp.rs` gained `SAFETY:`
+  invariant comments for the worker launch/seccomp unsafe sites; the generated
+  report now shows 29 annotated native unsafe sites and 409 remaining unsafe
+  review sites. Remaining work: continue unsafe invariant comments across the
+  rest of `jerboa-native-rs`.
 
 ### K3-P1-02 — TOCTOU-safe filesystem capability checks
 **Serves:** G2. **Effort:** 1 week.
@@ -1109,7 +1113,7 @@ Track these in `docs/status.md` per release:
 | Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, and Linux syscall/ptrace seccomp pre-exec setup landed; Landlock path/net and Seatbelt/Capsicum parity pending | yes, tested (P0-02) |
 | Unclassified raw `read`/FASL/`load` sites | scanner-driven manifest classification gate closed 2026-07-27; trusted build, compiler, REPL/dev, and test surfaces are centrally justified by exact source line | 0 |
 | FFI bindings audited | scanner inventory and provisional verdicts in [ffi-audit.md](ffi-audit.md); native exports gated and justified | 100% with final verdicts |
-| Un-annotated Rust `unsafe` blocks | 433 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 |
+| Un-annotated Rust `unsafe` blocks | 409 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 |
 | Fuzz corpora / crash regressions | 0 / 0 | per-parser corpora + every crash a test |
 | Exploit-shaped regression tests | ~0 | ≥ 1 per historical finding |
 | Sandboxed-by-default app entry | no | yes (P0-06) |
diff --git a/docs/reviews/2026-07-27-native-export-review.sexp b/docs/reviews/2026-07-27-native-export-review.sexp
index db6d4f8..011e28c 100644
--- a/docs/reviews/2026-07-27-native-export-review.sexp
+++ b/docs/reviews/2026-07-27-native-export-review.sexp
@@ -19,11 +19,11 @@
     (export (symbol "jerboa_antidebug_check_tracer") (file "jerboa-native-rs/src/antidebug.rs") (line 30) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 38))))
     (export (symbol "jerboa_antidebug_ptrace") (file "jerboa-native-rs/src/antidebug.rs") (line 8) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 36))))
     (export (symbol "jerboa_antidebug_timing_check") (file "jerboa-native-rs/src/antidebug.rs") (line 119) (scheme-callers 1 (caller (file "lib/std/os/antidebug.ss") (line 44))))
-    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 632) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 155))))
-    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 604) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 151))))
-    (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 146) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 127)) (caller (file "lib/std/os/aproc.ss") (line 142))))
-    (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 358) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 142))))
-    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 646) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 159))))
+    (export (symbol "jerboa_aproc_killpg") (file "jerboa-native-rs/src/aproc.rs") (line 675) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 155))))
+    (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 645) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 151))))
+    (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 152) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 127)) (caller (file "lib/std/os/aproc.ss") (line 142))))
+    (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 374) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 142))))
+    (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 691) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 159))))
     (export (symbol "jerboa_argon2id_hash") (file "jerboa-native-rs/src/crypto.rs") (line 709) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 276)) (caller (file "lib/std/crypto/password.ss") (line 35))))
     (export (symbol "jerboa_argon2id_verify") (file "jerboa-native-rs/src/crypto.rs") (line 758) (scheme-callers 2 (caller (file "lib/std/crypto/native-rust.ss") (line 296)) (caller (file "lib/std/crypto/password.ss") (line 42))))
     (export (symbol "jerboa_chacha20_open") (file "jerboa-native-rs/src/crypto.rs") (line 526) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 204))))
@@ -109,9 +109,9 @@
     (export (symbol "jerboa_regex_is_match") (file "jerboa-native-rs/src/regex_native.rs") (line 49) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 30))))
     (export (symbol "jerboa_regex_replace_all") (file "jerboa-native-rs/src/regex_native.rs") (line 112) (scheme-callers 1 (caller (file "lib/std/regex-native.ss") (line 44))))
     (export (symbol "jerboa_scrypt") (file "jerboa-native-rs/src/crypto.rs") (line 600) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 226))))
-    (export (symbol "jerboa_seccomp_available") (file "jerboa-native-rs/src/seccomp.rs") (line 277) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 32))))
-    (export (symbol "jerboa_seccomp_lock") (file "jerboa-native-rs/src/seccomp.rs") (line 174) (scheme-callers 2 (caller (file "lib/std/os/seccomp.ss") (line 34)) (caller (file "lib/std/os/seccomp.ss") (line 36))))
-    (export (symbol "jerboa_seccomp_lock_strict") (file "jerboa-native-rs/src/seccomp.rs") (line 191) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 36))))
+    (export (symbol "jerboa_seccomp_available") (file "jerboa-native-rs/src/seccomp.rs") (line 285) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 32))))
+    (export (symbol "jerboa_seccomp_lock") (file "jerboa-native-rs/src/seccomp.rs") (line 179) (scheme-callers 2 (caller (file "lib/std/os/seccomp.ss") (line 34)) (caller (file "lib/std/os/seccomp.ss") (line 36))))
+    (export (symbol "jerboa_seccomp_lock_strict") (file "jerboa-native-rs/src/seccomp.rs") (line 196) (scheme-callers 1 (caller (file "lib/std/os/seccomp.ss") (line 36))))
     (export (symbol "jerboa_secure_alloc") (file "jerboa-native-rs/src/secure_mem.rs") (line 28) (scheme-callers 1 (caller (file "lib/std/crypto/secure-mem.ss") (line 35))))
     (export (symbol "jerboa_secure_dir_close") (file "jerboa-native-rs/src/secure_fs.rs") (line 463) (scheme-callers 2 (caller (file "lib/std/os/secure-output.ss") (line 33)) (caller (file "lib/std/os/secure-output.ss") (line 53))))
     (export (symbol "jerboa_secure_dir_list") (file "jerboa-native-rs/src/secure_fs.rs") (line 633) (scheme-callers 1 (caller (file "lib/std/os/secure-output.ss") (line 62))))
diff --git a/docs/status.md b/docs/status.md
index e267f1c..7e397ea 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -25,7 +25,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
 | Area | Current state | Remaining work |
 |---|---|---|
 | Kimi security handoff | [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) is the backlog. Dated evidence and review manifests live under [reviews/](reviews/). | Keep new security evidence in dated review records and summarize the current release state here. |
-| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. | Add `SAFETY:` invariant comments near Rust unsafe sites. |
+| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs` and `seccomp.rs` now has nearby `SAFETY:` comments, reducing the generated unannotated unsafe-site count to 409. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. |
 | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. |
 | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native Landlock path/net rules in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. |
 | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
diff --git a/jerboa-native-rs/src/aproc.rs b/jerboa-native-rs/src/aproc.rs
index 94d0259..20925ca 100644
--- a/jerboa-native-rs/src/aproc.rs
+++ b/jerboa-native-rs/src/aproc.rs
@@ -31,6 +31,8 @@ fn parse_nul_separated(buf: *const u8, len: usize, count: usize) -> Result<Vec<C
     if count == 0 || buf.is_null() {
         return Ok(Vec::new());
     }
+    // SAFETY: the FFI contract supplies `buf` with `len` readable bytes for
+    // this call. Bytes are copied into owned CStrings before returning.
     let slice = unsafe { std::slice::from_raw_parts(buf, len) };
     let mut out = Vec::with_capacity(count);
     let mut start = 0usize;
@@ -65,6 +67,8 @@ fn make_stdio(mode: i32, path: *const u8, path_len: usize) -> Result<Stdio, Stri
             if path.is_null() || path_len == 0 {
                 return Err("STDIO_FILE: empty path".into());
             }
+            // SAFETY: non-null path pointer and explicit byte length were
+            // checked above; bytes are copied into an owned OsString.
             let bytes = unsafe { std::slice::from_raw_parts(path, path_len) };
             let pb = PathBuf::from(OsString::from_vec(bytes.to_vec()));
             let f = std::fs::OpenOptions::new()
@@ -99,6 +103,8 @@ fn parse_rlimits(
             expected, count, len
         ));
     }
+    // SAFETY: non-null was checked above, and `len == count * 24` bounds every
+    // fixed-width decode below. Bytes are copied into owned Rust tuples.
     let slice = unsafe { std::slice::from_raw_parts(buf, len) };
     let mut out = Vec::with_capacity(count);
     for i in 0..count {
@@ -214,6 +220,8 @@ pub extern "C" fn jerboa_aproc_spawn(
         }
 
         if !cwd_buf.is_null() && cwd_len > 0 {
+            // SAFETY: non-null cwd pointer and explicit byte length are checked
+            // before constructing a temporary slice; path bytes are copied.
             let bytes = unsafe { std::slice::from_raw_parts(cwd_buf, cwd_len) };
             let pb = PathBuf::from(OsString::from_vec(bytes.to_vec()));
             cmd.current_dir(pb);
@@ -257,6 +265,9 @@ pub extern "C" fn jerboa_aproc_spawn(
         // — the raw pointer + count would not satisfy `'static` bounds.
         let inh_pairs: Vec<(i32, i32)> = if inherit_fd_count > 0 && !inherit_fds.is_null() {
             let mut v = Vec::with_capacity(inherit_fd_count);
+            // SAFETY: `inherit_fd_count` is a pair count from the Scheme
+            // caller; the ABI requires exactly two i32 values per pair, and the
+            // pairs are copied into an owned Vec for pre_exec.
             let slice = unsafe { std::slice::from_raw_parts(inherit_fds, inherit_fd_count * 2) };
             for i in 0..inherit_fd_count {
                 v.push((slice[i * 2], slice[i * 2 + 1]));
@@ -273,6 +284,9 @@ pub extern "C" fn jerboa_aproc_spawn(
             }
         };
 
+        // SAFETY: CommandExt::pre_exec runs this closure in the child after
+        // fork and before exec. The closure only performs fd/syscall setup
+        // using async-signal-safe libc calls and data copied into owned Vecs.
         unsafe {
             cmd.pre_exec(move || {
                 if new_pgroup {
@@ -332,6 +346,8 @@ pub extern "C" fn jerboa_aproc_spawn(
         // owns the pid + fds from here.
         std::mem::forget(child);
 
+        // SAFETY: result was checked non-null and the ABI requires room for
+        // RESULT_LEN i32 slots. Each add() stays within that fixed layout.
         unsafe {
             *result.add(0) = pid;
             *result.add(1) = stdin_fd;
@@ -388,6 +404,9 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         // openpty(3) gives us master + slave file descriptors.
         let mut master_fd: libc::c_int = -1;
         let mut slave_fd: libc::c_int = -1;
+        // SAFETY: openpty writes two c_int outputs and the optional name,
+        // termios, and winsize pointers are null. On success both fds are
+        // closed or handed to Scheme exactly once along every path below.
         let rc = unsafe {
             libc::openpty(
                 &mut master_fd,
@@ -410,8 +429,11 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         let want_stderr_pipe = stderr_mode == STDIO_PIPE;
         let (stderr_read, stderr_write) = if want_stderr_pipe {
             let mut fds = [0i32; 2];
+            // SAFETY: fds points to two valid c_int slots for pipe(2) output.
             let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
             if rc != 0 {
+                // SAFETY: master_fd and slave_fd were returned by openpty and
+                // are still owned on this error path.
                 unsafe {
                     libc::close(master_fd);
                     libc::close(slave_fd);
@@ -441,6 +463,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
             let envp = match parse_nul_separated(envp_buf, envp_buf_len, envc) {
                 Ok(v) => v,
                 Err(e) => {
+                    // SAFETY: these fds are owned by this function until
+                    // spawn succeeds; close each live descriptor before return.
                     unsafe {
                         libc::close(master_fd);
                         libc::close(slave_fd);
@@ -466,6 +490,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
             }
         }
         if !cwd_buf.is_null() && cwd_len > 0 {
+            // SAFETY: non-null cwd pointer and explicit byte length are checked
+            // before constructing a temporary slice; path bytes are copied.
             let bytes = unsafe { std::slice::from_raw_parts(cwd_buf, cwd_len) };
             let pb = PathBuf::from(OsString::from_vec(bytes.to_vec()));
             cmd.current_dir(pb);
@@ -487,6 +513,9 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         let slave = slave_fd;
         let stderr_w = stderr_write;
         let merge_stderr = stderr_mode == STDIO_MERGE_STDOUT;
+        // SAFETY: CommandExt::pre_exec runs in the child before exec. The
+        // closure uses only async-signal-safe fd/session operations on copied
+        // integers and does not allocate or call back into Rust-owned state.
         unsafe {
             cmd.pre_exec(move || {
                 // Become session leader so we can acquire the pty as controlling tty.
@@ -540,6 +569,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         let child = match cmd.spawn() {
             Ok(c) => c,
             Err(e) => {
+                // SAFETY: spawn failed before ownership transferred; close
+                // every fd opened by this function that remains live.
                 unsafe {
                     libc::close(master_fd);
                     libc::close(slave_fd);
@@ -555,10 +586,14 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
             }
         };
         // Parent doesn't keep the slave fd or stderr_write.
+        // SAFETY: after successful spawn, the parent still owns slave_fd and
+        // must close it; the child has its own duped/inherited descriptors.
         unsafe {
             libc::close(slave_fd);
         }
         if stderr_write >= 0 {
+            // SAFETY: stderr_write is owned by the parent after spawn and is no
+            // longer needed once the child inherited/duped its side.
             unsafe {
                 libc::close(stderr_write);
             }
@@ -567,6 +602,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
         let pid = child.id() as i32;
         std::mem::forget(child);
 
+        // SAFETY: result was checked non-null and the ABI requires room for
+        // RESULT_LEN i32 slots. Each add() stays within that fixed layout.
         unsafe {
             *result.add(0) = pid;
             *result.add(1) = master_fd;
@@ -581,6 +618,8 @@ pub extern "C" fn jerboa_aproc_spawn_pty(
 /// dup(2) — returns the new fd, or -1 on error.
 pub extern "C" fn jerboa_aproc_dup(fd: i32) -> i32 {
     ffi_wrap(|| {
+        // SAFETY: dup(2) validates the descriptor number; errors are surfaced
+        // through errno and jerboa_last_error.
         let r = unsafe { libc::dup(fd) };
         if r < 0 {
             set_last_error(format!(
@@ -595,6 +634,8 @@ pub extern "C" fn jerboa_aproc_dup(fd: i32) -> i32 {
 
 /// close(2) — returns 0 on success, -1 on error.
 pub extern "C" fn jerboa_aproc_close(fd: i32) -> i32 {
+    // SAFETY: close(2) validates the descriptor number. Ownership is governed
+    // by the Scheme caller; this wrapper forwards the syscall result.
     let r = unsafe { libc::close(fd) };
     r
 }
@@ -602,6 +643,8 @@ pub extern "C" fn jerboa_aproc_close(fd: i32) -> i32 {
 /// Set fd to non-blocking mode (O_NONBLOCK).
 #[no_mangle]
 pub extern "C" fn jerboa_aproc_set_nonblock(fd: i32, nonblock: i32) -> i32 {
+    // SAFETY: fcntl(2) validates fd and command arguments; this block performs
+    // only descriptor flag reads/writes and reports errno on failure.
     ffi_wrap(|| unsafe {
         let flags = libc::fcntl(fd, libc::F_GETFL);
         if flags < 0 {
@@ -630,6 +673,8 @@ pub extern "C" fn jerboa_aproc_set_nonblock(fd: i32, nonblock: i32) -> i32 {
 /// killpg(2) — send signal to the entire process group identified by pgid.
 #[no_mangle]
 pub extern "C" fn jerboa_aproc_killpg(pgid: i32, sig: i32) -> i32 {
+    // SAFETY: killpg(2) validates pgid and signal values; this wrapper exposes
+    // the kernel result directly to Scheme.
     let r = unsafe { libc::killpg(pgid, sig) };
     r
 }
@@ -654,11 +699,17 @@ pub extern "C" fn jerboa_aproc_wait4(
         return -1;
     }
     let mut status: libc::c_int = 0;
+    // SAFETY: libc::rusage is a plain C output struct; zeroed is a valid
+    // initial buffer for wait4 to fill.
     let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
+    // SAFETY: status and rusage point to live stack variables and wait4 owns no
+    // references beyond the syscall return.
     let r = unsafe { libc::wait4(pid, &mut status, options, &mut ru) };
     if r < 0 {
         return -1;
     }
+    // SAFETY: output pointers were checked non-null and the ABI requires eight
+    // i64 rusage slots plus one i32 status slot.
     unsafe {
         *status_out = status;
         *rusage_out.add(0) = ru.ru_utime.tv_sec as i64;
@@ -685,6 +736,8 @@ fn _unused_cstr(p: *const libc::c_char) -> Option<&'static CStr> {
     if p.is_null() {
         None
     } else {
+        // SAFETY: caller of this unused helper must pass a valid NUL-terminated
+        // C string whose lifetime outlives the returned reference.
         Some(unsafe { CStr::from_ptr(p) })
     }
 }
@@ -694,6 +747,8 @@ fn _unused_owned_from_raw(fd: i32) -> Option<OwnedFd> {
     if fd < 0 {
         None
     } else {
+        // SAFETY: caller of this unused helper must pass an owned fd; OwnedFd
+        // will close it on drop.
         Some(unsafe { OwnedFd::from_raw_fd(fd) })
     }
 }
diff --git a/jerboa-native-rs/src/seccomp.rs b/jerboa-native-rs/src/seccomp.rs
index 502af30..0f9765c 100644
--- a/jerboa-native-rs/src/seccomp.rs
+++ b/jerboa-native-rs/src/seccomp.rs
@@ -78,6 +78,8 @@ fn bpf_jump(code: u16, k: u32, jt: u8, jf: u8) -> SockFilter {
 }
 
 fn set_no_new_privs() -> Result<(), String> {
+    // SAFETY: prctl is called with the PR_SET_NO_NEW_PRIVS operation and
+    // integer arguments only; no borrowed memory crosses the syscall.
     let rc = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
     if rc < 0 {
         Err(format!(
@@ -94,6 +96,9 @@ fn install_filter(filter: &[SockFilter]) -> Result<(), String> {
         len: filter.len() as u16,
         filter: filter.as_ptr(),
     };
+    // SAFETY: prog points at `filter`, which stays alive for the duration of
+    // this synchronous prctl call. SockFilter/SockFprog mirror Linux classic
+    // BPF layouts with #[repr(C)].
     let rc = unsafe {
         libc::prctl(
             libc::PR_SET_SECCOMP,
@@ -114,52 +119,52 @@ fn install_filter(filter: &[SockFilter]) -> Result<(), String> {
 }
 
 pub(crate) fn install_debug_blocklist() -> Result<(), String> {
-        let audit_arch = match current_audit_arch() {
-            Some(arch) => arch,
-            None => {
-                return Err("seccomp lock is not implemented for this CPU architecture".to_string());
-            }
-        };
-        let blocked = match blocked_debug_syscalls() {
-            Some(syscalls) => syscalls,
-            None => {
-                return Err("seccomp lock is not implemented for this CPU architecture".to_string());
-            }
-        };
+    let audit_arch = match current_audit_arch() {
+        Some(arch) => arch,
+        None => {
+            return Err("seccomp lock is not implemented for this CPU architecture".to_string());
+        }
+    };
+    let blocked = match blocked_debug_syscalls() {
+        Some(syscalls) => syscalls,
+        None => {
+            return Err("seccomp lock is not implemented for this CPU architecture".to_string());
+        }
+    };
 
-        set_no_new_privs()?;
+    set_no_new_privs()?;
 
-        // BPF program:
-        //   0: load arch
-        //   1: if arch != current architecture, kill
-        //   2: load syscall nr
-        //   3..7: block ptrace/process_vm_readv/process_vm_writev/personality/memfd_create
-        //   8: allow
-        //   9: kill
-        let filter = [
-            // 0: Load architecture
-            bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_ARCH),
-            // 1: Verify architecture — if not, jump to kill (offset +7 -> instruction 9)
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, audit_arch, 0, 7),
-            // 2: Load syscall number
-            bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR),
-            // 3: Check ptrace — if match, jump to kill (+5 -> instruction 9)
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[0], 5, 0),
-            // 4: Check process_vm_readv
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[1], 4, 0),
-            // 5: Check process_vm_writev
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[2], 3, 0),
-            // 6: Check personality
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[3], 2, 0),
-            // 7: Check memfd_create — prevents code injection via memfd
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[4], 1, 0),
-            // 8: Allow
-            bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
-            // 9: Kill process
-            bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
-        ];
+    // BPF program:
+    //   0: load arch
+    //   1: if arch != current architecture, kill
+    //   2: load syscall nr
+    //   3..7: block ptrace/process_vm_readv/process_vm_writev/personality/memfd_create
+    //   8: allow
+    //   9: kill
+    let filter = [
+        // 0: Load architecture
+        bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_ARCH),
+        // 1: Verify architecture — if not, jump to kill (offset +7 -> instruction 9)
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, audit_arch, 0, 7),
+        // 2: Load syscall number
+        bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR),
+        // 3: Check ptrace — if match, jump to kill (+5 -> instruction 9)
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[0], 5, 0),
+        // 4: Check process_vm_readv
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[1], 4, 0),
+        // 5: Check process_vm_writev
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[2], 3, 0),
+        // 6: Check personality
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[3], 2, 0),
+        // 7: Check memfd_create — prevents code injection via memfd
+        bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, blocked[4], 1, 0),
+        // 8: Allow
+        bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+        // 9: Kill process
+        bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
+    ];
 
-        install_filter(&filter)
+    install_filter(&filter)
 }
 
 /// Install a seccomp-bpf filter that blocks debug-related syscalls:
@@ -217,6 +222,9 @@ pub extern "C" fn jerboa_seccomp_lock_strict(allowed: *const i32, allowed_count:
         }
 
         let syscalls = if allowed_count > 0 {
+            // SAFETY: non-null and bounded by MAX_STRICT_SYSCALLS above; the
+            // Scheme caller passes an immutable syscall-number vector consumed
+            // during this call only.
             unsafe { std::slice::from_raw_parts(allowed, allowed_count) }
         } else {
             &[]
@@ -277,6 +285,8 @@ pub extern "C" fn jerboa_seccomp_lock_strict(allowed: *const i32, allowed_count:
 pub extern "C" fn jerboa_seccomp_available() -> i32 {
     ffi_wrap(|| {
         // Try PR_GET_SECCOMP to check if seccomp is available
+        // SAFETY: prctl is called with PR_GET_SECCOMP and scalar arguments; it
+        // does not dereference caller-controlled pointers.
         let rc = unsafe { libc::prctl(libc::PR_GET_SECCOMP, 0, 0, 0, 0) };
         // Returns 0 (seccomp disabled for this process but supported),
         // 2 (filter mode active), or -1 with EINVAL (not supported)