security: install worker seccomp pre-exec
Jaime Fournier <jaimef@linbsd.org>
06936a274a6d0a466ea91479d4f89f3d54e777dd
diff --git a/docs/aproc.md b/docs/aproc.md
index 1e10220..2c89bbf 100644
--- a/docs/aproc.md
+++ b/docs/aproc.md
@@ -73,6 +73,12 @@ rlimits: '((resource soft hard) …) ; native pre-exec setrlimit triples
`pty: #t`. Resource numbers are platform `RLIMIT_*` constants; callers should
prefer `(std os limits)` `limit-policy->rlimits` over hard-coding them.
+`seccomp-lock?: #t` is also supported only on the argv/native path. On Linux,
+the child installs Jerboa's default seccomp debug/syscall blocklist after fd
+and rlimit setup and before `exec`; it is rejected with `pty: #t`. Higher-level
+code should prefer `(std security worker)` `sandbox: '(syscalls ptrace)` so
+fail-closed diagnostics remain structured.
+
### Low-level handle
```
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index d1fb263..7f07dee 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -217,7 +217,7 @@ when" must be answerable from `dist/release-evidence/` in minutes.
| Self-confinement (pledge/unveil-style `cage!`) | `(std security cage)` | exists; documented in the security module inventory | security-reference §12 |
| Privilege-separation pipe channels (no launcher) | `(std security privsep)` | channels only, by design | security-reference §5 |
| Launch policy planner + egress policy objects | `(std os limits sandbox)` | passive planner remains `pre-exec-refused`; `(std os limits)` can emit rlimit triples for the worker/aproc backend | [limits.md](limits.md) |
-| Exec restricted worker facade | `(std security worker)` | exists; pure env, deadline, process-group kill, output caps, audit records, fail-closed refused axes, explicit `sandbox:` axis requests, egress proxy env wiring, and memory rlimits installed pre-exec on supported platforms; native kernel sandbox installation pending | security-reference §5 |
+| Exec restricted worker facade | `(std security worker)` | exists; pure env, deadline, process-group kill, output caps, audit records, fail-closed refused axes, explicit `sandbox:` axis requests, egress proxy env wiring, memory rlimits installed pre-exec on supported platforms, and Linux syscall/ptrace seccomp pre-exec setup for requested axes; Landlock path/net and Seatbelt/Capsicum parity pending | security-reference §5 |
| Native async exec launcher (collect-safe) | `(std os aproc)` | exists — the primitive P0-02 should build on | [aproc.md](aproc.md) |
| Parser hardening (depth/size/backtrack budgets: reader, JSON, XML, YAML, DNS, HTTP/2, WS, zlib, base64, hex, CSV, pregexp, format) | various | phases 1–4 done, 42 tests; phase 5 FFI audit inventory has per-site provisional verdicts, with remediation rules still open | security-reference §7; [ffi-audit.md](ffi-audit.md) |
| Safe deserialization (tagged-JSON envelope, no native FASL on untrusted paths) | `(std safe-fasl)`, `(std fasl)` (trusted-only) | exists; raw `read`/FASL/`load` sites are classified by manifest and gated by `make audit` | [safety-guide.md](safety-guide.md) §10 |
@@ -347,7 +347,9 @@ audit-log start/end records landed 2026-07-27 and are asserted by
`tests/test-worker.ss`. Native pre-exec memory rlimit installation landed via
`(std os aproc)` `rlimits:` and `(std os limits)` `limit-policy->rlimits`.
Explicit worker `sandbox:` axis requests and `egress-policy:` allow-proxy env
-wiring landed 2026-07-27. Native Landlock/seccomp and Seatbelt/Capsicum setup
+wiring landed 2026-07-27. Native pre-exec syscall/ptrace seccomp setup for
+requested worker axes landed 2026-07-28 through `(std os aproc)`
+`seccomp-lock?:`. Native Landlock path/net setup and Seatbelt/Capsicum parity
remain open.
Every security doc routes adversarial work to "a bounded, separately exec'd
@@ -360,8 +362,8 @@ controls that already exist individually.
`(std security landlock)`, `(std security seccomp)`, `(std security seatbelt)`,
`(std security capsicum)`, `(std security audit-log)`. Optionally a tiny
pre-exec helper in `jerboa-native-rs` (Rust) for rlimit/namespace setup
- that must happen before Scheme boots. The rlimit path exists; extend that
- pattern for the remaining platform controls.
+ that must happen before Scheme boots. The rlimit and syscall/ptrace seccomp
+ paths exist; extend that pattern for the remaining platform controls.
- **Design (fail closed at every step):**
- Worker = fresh `jerboa run worker-main.ss` process started via argv
(never shell), with `env-pure:` from an `(std security env)` policy
@@ -773,7 +775,9 @@ Chez cannot heap-cap a thread; `run-safe-eval` rightly refuses
`sandbox-report`, an audit-friendly alist covering platform, requested
controls, degraded `run-safe-eval` state, active sandbox-child reservations,
and configured/available/installed state for seccomp, Landlock, Seatbelt,
- Capsicum, and memory limits. Native per-platform CI parity remains open.
+ Capsicum, and memory limits. `(std security worker)` now routes requested
+ `syscalls`/`ptrace` sandbox axes through native aproc pre-exec seccomp on
+ Linux. Native Landlock path/net setup and per-platform CI parity remain open.
### K3-P1-09 — Confused-deputy defenses: capability plans enforced at runtime
**Serves:** G2. **Effort:** 1 week.
@@ -1102,7 +1106,7 @@ Track these in `docs/status.md` per release:
| Metric | Baseline (2026-07-27) | Target |
|---|---|---|
| Build balance clean | `pattern.ss` repaired 2026-07-27; `source-balance` in `make audit` | always clean |
-| Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, and egress proxy env wiring landed 2026-07-27; native kernel sandbox installation pending | yes, tested (P0-02) |
+| 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 |
@@ -1208,7 +1212,8 @@ fake confidence happens.
audit-log lifecycle events and native pre-exec memory rlimits on supported
platforms. It now exposes explicit `sandbox:` fail-closed axis requests and
`egress-policy:` proxy env wiring. Native kernel sandbox installation is
- still pending (P0-02/P1-08).
+ still pending for Landlock path/net rules and Seatbelt/Capsicum parity
+ (P0-02/P1-08).
- The committed `pattern.ss` balance blocker named in P0-01 was repaired on
2026-07-27. The `pipeline.ss` and `test-pipeline.ss` reports were traced to
escaped-identifier false positives in the balance scanner; the scanner now
diff --git a/docs/reviews/2026-07-27-native-export-review.sexp b/docs/reviews/2026-07-27-native-export-review.sexp
index bad6245..db6d4f8 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 614) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 154))))
- (export (symbol "jerboa_aproc_set_nonblock") (file "jerboa-native-rs/src/aproc.rs") (line 586) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 150))))
- (export (symbol "jerboa_aproc_spawn") (file "jerboa-native-rs/src/aproc.rs") (line 132) (scheme-callers 2 (caller (file "lib/std/os/aproc.ss") (line 126)) (caller (file "lib/std/os/aproc.ss") (line 141))))
- (export (symbol "jerboa_aproc_spawn_pty") (file "jerboa-native-rs/src/aproc.rs") (line 340) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 141))))
- (export (symbol "jerboa_aproc_wait4") (file "jerboa-native-rs/src/aproc.rs") (line 628) (scheme-callers 1 (caller (file "lib/std/os/aproc.ss") (line 158))))
+ (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_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))))
@@ -82,7 +82,7 @@
(export (symbol "jerboa_landlock_add_path_rule") (file "jerboa-native-rs/src/landlock.rs") (line 120) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 83))))
(export (symbol "jerboa_landlock_create_ruleset") (file "jerboa-native-rs/src/landlock.rs") (line 91) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 80))))
(export (symbol "jerboa_landlock_enforce") (file "jerboa-native-rs/src/landlock.rs") (line 234) (scheme-callers 1 (caller (file "lib/std/os/landlock-native.ss") (line 89))))
- (export (symbol "jerboa_last_error") (file "jerboa-native-rs/src/panic.rs") (line 9) (scheme-callers 11 (caller (file "lib/std/compress/native-rust.ss") (line 42)) (caller (file "lib/std/crypto/native-rust.ss") (line 48)) (caller (file "lib/std/crypto/x509.ss") (line 24)) (caller (file "lib/std/native.ss") (line 56)) (caller (file "lib/std/net/tls-rustls.ss") (line 167)) (caller (file "lib/std/os/aproc.ss") (line 163)) (caller (file "lib/std/os/integrity.ss") (line 42)) (caller (file "lib/std/os/secure-output.ss") (line 47)) (caller (file "lib/std/pcap.ss") (line 66)) (caller (file "lib/std/regex-native.ss") (line 51)) (caller (file "lib/std/wasm/sandbox.ss") (line 196))))
+ (export (symbol "jerboa_last_error") (file "jerboa-native-rs/src/panic.rs") (line 9) (scheme-callers 11 (caller (file "lib/std/compress/native-rust.ss") (line 42)) (caller (file "lib/std/crypto/native-rust.ss") (line 48)) (caller (file "lib/std/crypto/x509.ss") (line 24)) (caller (file "lib/std/native.ss") (line 56)) (caller (file "lib/std/net/tls-rustls.ss") (line 167)) (caller (file "lib/std/os/aproc.ss") (line 164)) (caller (file "lib/std/os/integrity.ss") (line 42)) (caller (file "lib/std/os/secure-output.ss") (line 47)) (caller (file "lib/std/pcap.ss") (line 66)) (caller (file "lib/std/regex-native.ss") (line 51)) (caller (file "lib/std/wasm/sandbox.ss") (line 196))))
(export (symbol "jerboa_pbkdf2_derive") (file "jerboa-native-rs/src/crypto.rs") (line 642) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 246))))
(export (symbol "jerboa_pbkdf2_verify") (file "jerboa-native-rs/src/crypto.rs") (line 671) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 261))))
(export (symbol "jerboa_pcap_close") (file "jerboa-native-rs/src/pcap_capture.rs") (line 324) (scheme-callers 1 (caller (file "lib/std/pcap.ss") (line 58))))
@@ -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 283) (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 125) (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 197) (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 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_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/security-reference.md b/docs/security-reference.md
index 01c27a5..06d5f82 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -485,6 +485,12 @@ any unavailable or degraded requested axis refuses before `exec`; with
`fail-closed?: #f`, the launch continues and diagnostics record the requested
axes.
+When `sandbox:` includes `syscalls` or `ptrace` on Linux, the worker passes
+`seccomp-lock?: #t` to `(std os aproc)`. The native aproc child installs the
+debug/syscall seccomp blocklist after fd and rlimit setup and before `exec`.
+Diagnostics include `native-seccomp-requested?` and
+`native-seccomp-installed?`.
+
Use `egress-policy:` with a `(std net allow-proxy)` policy when the worker
should route HTTP-capable clients through the deny-default allow proxy:
@@ -529,10 +535,11 @@ result reports whether the limit was actually installed. Do not treat
Current limitation: the worker has a real exec boundary, pure environment,
parent deadline, process-group kill through `aproc`, returned output caps,
-audit-log start/end records, memory rlimits on supported platforms, explicit
-fail-closed sandbox-axis requests, and allow-proxy environment wiring. Native
-pre-exec Landlock/seccomp and Seatbelt/Capsicum installation remain tracked by
-the K3 handoff.
+audit-log start/end records, memory rlimits on supported platforms, Linux
+syscall/ptrace seccomp setup for requested axes, explicit fail-closed
+sandbox-axis requests, and allow-proxy environment wiring. Native pre-exec
+Landlock path/net setup and Seatbelt/Capsicum parity remain tracked by the K3
+handoff.
---
diff --git a/docs/status.md b/docs/status.md
index 1b23646..e267f1c 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -14,7 +14,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
| CLI | `jerboa`, `jerboa run`, `jerboa eval`, `jerboa test`, `jerboa build`, and `jerboa pkg` are the documented entry points. |
| Language | `(jerboa prelude)` is the default import for user code and resolves to the safe surface for direct scripts; use `--unsafe-prelude` or `(jerboa prelude unsafe)` only for explicit raw access. `.sls` files remain implementation internals. |
| Standard library | `lib/std` and `lib/jerboa` contain 647 `.ss` modules at this snapshot. See [libraries.md](libraries.md) and [api-index.md](api-index.md). |
-| Security | Landlock, seccomp, capability plans, taint-aware safe-prelude sinks, restricted evaluation, safe FASL, the exec worker facade, memory rlimit pre-exec setup, security regression tests, and release evidence are implemented. Native worker Landlock/seccomp and Seatbelt/Capsicum installation remain open hardening work. |
+| Security | Landlock, seccomp, capability plans, taint-aware safe-prelude sinks, restricted evaluation, safe FASL, the exec worker facade, memory rlimit pre-exec setup, worker syscall/ptrace seccomp setup on Linux, security regression tests, and release evidence are implemented. Native worker Landlock path/net setup and Seatbelt/Capsicum parity remain open hardening work. |
| Native Rust | `libjerboa_native` backs crypto, secure memory, compression, regex, selected databases, OS integrations, packet capture, and rustls TLS. |
| Packaging | `jpkg` is shipped through the Jerboa multicall binary and documented in [jpkg-guide.md](jpkg-guide.md). |
| Build and release | `make binary` is the local non-Linux release build, `make docker-build` is the Linux release build, and `make release-evidence` collects audit evidence. |
@@ -27,7 +27,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
| 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. |
| 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, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native kernel sandboxes in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. |
+| 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. |
## Compatibility Notes
diff --git a/jerboa-native-rs/src/aproc.rs b/jerboa-native-rs/src/aproc.rs
index 96f9da1..94d0259 100644
--- a/jerboa-native-rs/src/aproc.rs
+++ b/jerboa-native-rs/src/aproc.rs
@@ -17,6 +17,7 @@ const STDIO_MERGE_STDOUT: i32 = 4; // stderr only
const FLAG_NEW_PGROUP: i32 = 1 << 0;
const FLAG_USE_ENVP: i32 = 1 << 1; // apply envp entries
const FLAG_ENV_CLEAR: i32 = 1 << 2; // env_clear() before applying envp (replace mode)
+const FLAG_SECCOMP_LOCK: i32 = 1 << 3; // install debug/syscall seccomp blocklist pre-exec
// Result buffer layout (5 * i32, little-endian, native order):
// [0] = pid
@@ -117,6 +118,19 @@ fn parse_rlimits(
Ok(out)
}
+#[cfg(target_os = "linux")]
+fn install_seccomp_blocklist_pre_exec() -> std::io::Result<()> {
+ crate::seccomp::install_debug_blocklist().map_err(std::io::Error::other)
+}
+
+#[cfg(not(target_os = "linux"))]
+fn install_seccomp_blocklist_pre_exec() -> std::io::Result<()> {
+ Err(std::io::Error::new(
+ std::io::ErrorKind::Unsupported,
+ "seccomp lock is only available on Linux",
+ ))
+}
+
/// argv-style spawn via std::process::Command (posix_spawn fast path or
/// fork+execvp). Avoids /bin/sh -c, so the caller does not need to escape
/// args and there is no extra shell process in the tree.
@@ -237,6 +251,7 @@ pub extern "C" fn jerboa_aproc_spawn(
let new_pgroup = (flags & FLAG_NEW_PGROUP) != 0;
let merge_stderr = stderr_mode == STDIO_MERGE_STDOUT;
+ let seccomp_lock = (flags & FLAG_SECCOMP_LOCK) != 0;
// Copy inherit-fd pairs into a Vec we move into the pre_exec closure
// — the raw pointer + count would not satisfy `'static` bounds.
@@ -292,6 +307,9 @@ pub extern "C" fn jerboa_aproc_spawn(
return Err(std::io::Error::last_os_error());
}
}
+ if seccomp_lock {
+ install_seccomp_blocklist_pre_exec()?;
+ }
Ok(())
});
}
diff --git a/jerboa-native-rs/src/seccomp.rs b/jerboa-native-rs/src/seccomp.rs
index 42467fa..502af30 100644
--- a/jerboa-native-rs/src/seccomp.rs
+++ b/jerboa-native-rs/src/seccomp.rs
@@ -113,40 +113,21 @@ fn install_filter(filter: &[SockFilter]) -> Result<(), String> {
}
}
-/// Install a seccomp-bpf filter that blocks debug-related syscalls:
-/// - ptrace (101)
-/// - process_vm_readv (310)
-/// - process_vm_writev (311)
-/// - personality (135)
-/// All other syscalls are allowed.
-/// This is IRREVERSIBLE. Returns 0 on success, -1 on error.
-/// Requires Linux 3.5+ with CONFIG_SECCOMP_FILTER.
-#[no_mangle]
-pub extern "C" fn jerboa_seccomp_lock() -> i32 {
- ffi_wrap(|| {
+pub(crate) fn install_debug_blocklist() -> Result<(), String> {
let audit_arch = match current_audit_arch() {
Some(arch) => arch,
None => {
- set_last_error(
- "seccomp lock is not implemented for this CPU architecture".to_string(),
- );
- return -1;
+ return Err("seccomp lock is not implemented for this CPU architecture".to_string());
}
};
let blocked = match blocked_debug_syscalls() {
Some(syscalls) => syscalls,
None => {
- set_last_error(
- "seccomp lock is not implemented for this CPU architecture".to_string(),
- );
- return -1;
+ return Err("seccomp lock is not implemented for this CPU architecture".to_string());
}
};
- if let Err(e) = set_no_new_privs() {
- set_last_error(e);
- return -1;
- }
+ set_no_new_privs()?;
// BPF program:
// 0: load arch
@@ -178,11 +159,24 @@ pub extern "C" fn jerboa_seccomp_lock() -> i32 {
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
];
- if let Err(e) = install_filter(&filter) {
+ install_filter(&filter)
+}
+
+/// Install a seccomp-bpf filter that blocks debug-related syscalls:
+/// - ptrace (101)
+/// - process_vm_readv (310)
+/// - process_vm_writev (311)
+/// - personality (135)
+/// All other syscalls are allowed.
+/// This is IRREVERSIBLE. Returns 0 on success, -1 on error.
+/// Requires Linux 3.5+ with CONFIG_SECCOMP_FILTER.
+#[no_mangle]
+pub extern "C" fn jerboa_seccomp_lock() -> i32 {
+ ffi_wrap(|| {
+ if let Err(e) = install_debug_blocklist() {
set_last_error(e);
return -1;
}
-
0
})
}
diff --git a/lib/std/os/aproc.ss b/lib/std/os/aproc.ss
index 89d39de..a9c3bf1 100644
--- a/lib/std/os/aproc.ss
+++ b/lib/std/os/aproc.ss
@@ -71,6 +71,7 @@
aproc-run/status
aproc-run/status* ; argv-style
aproc-system
+ aproc-native-spawn-available?
;; Multi-process (P2.3)
aproc-pipeline
@@ -132,7 +133,7 @@
u8* size_t ; stderr path, len
u8* size_t ; inherit-fd pairs (i32 pairs), count
u8* size_t size_t ; rlimit triples (u64 resource soft hard), count
- int ; flags
+ int ; flags (pgroup/env/seccomp)
u8*) ; result buf (5*int)
int)
(catch (e) #f))))
@@ -315,9 +316,13 @@
(def STDIO_DEVNULL 2)
(def STDIO_FILE 3)
(def STDIO_MERGE_STDOUT 4)
- (def FLAG_NEW_PGROUP 1)
- (def FLAG_USE_ENVP 2)
- (def FLAG_ENV_CLEAR 4)
+ (def FLAG_NEW_PGROUP 1)
+ (def FLAG_USE_ENVP 2)
+ (def FLAG_ENV_CLEAR 4)
+ (def FLAG_SECCOMP_LOCK 8)
+
+ (def (aproc-native-spawn-available?)
+ (and c-jerboa-aproc-spawn #t))
(def (stdio-symbol->mode sym for-stderr?)
(case sym
@@ -351,7 +356,8 @@
[new-pgroup? #f]
[pty? #f]
[inherit-fds '()]
- [rlimits '()])
+ [rlimits '()]
+ [seccomp-lock? #f])
(let loop ([kw kwargs])
(cond
[(null? kw) (void)]
@@ -370,10 +376,13 @@
[(pty:) (set! pty? (and v #t))]
[(inherit-fd:) (set! inherit-fds v)]
[(rlimits:) (set! rlimits v)]
+ [(seccomp-lock?:) (set! seccomp-lock? (and v #t))]
[else (error 'aproc-spawn* "unknown keyword" k)])
(loop (cddr kw)))]))
(when (and pty? (pair? rlimits))
(error 'aproc-spawn* "rlimits are not supported with pty: #t"))
+ (when (and pty? seccomp-lock?)
+ (error 'aproc-spawn* "seccomp-lock?: #t is not supported with pty: #t"))
;; Pack argv
(let*-values
([(argv-buf argv-len argv-count) (pack-strings argv)]
@@ -409,7 +418,8 @@
[flags (bitwise-ior
(if new-pgroup? FLAG_NEW_PGROUP 0)
(if use-envp FLAG_USE_ENVP 0)
- (if env-clear? FLAG_ENV_CLEAR 0))]
+ (if env-clear? FLAG_ENV_CLEAR 0)
+ (if seccomp-lock? FLAG_SECCOMP_LOCK 0))]
[result-buf (make-bytevector 20 0)])
(let ([rc (if pty?
(call-pty-spawn argv-buf argv-len argv-count
@@ -862,7 +872,10 @@
[new-pgroup? (kw-ref kwargs 'new-pgroup:)]
[pty? (kw-ref kwargs 'pty:)]
[inherit-fd (kw-ref kwargs 'inherit-fd:)]
- [rlimits (kw-ref kwargs 'rlimits:)])
+ [rlimits (kw-ref kwargs 'rlimits:)]
+ [seccomp-lock? (kw-ref kwargs 'seccomp-lock?:)])
+ (when (and seccomp-lock? (not c-jerboa-aproc-spawn))
+ (error 'aproc-run/status* "seccomp-lock?: #t requires libjerboa_native"))
;; Build spawn kwargs
(let* ([spawn-kw (append
(if env (list 'env: env) '())
@@ -874,7 +887,8 @@
(if new-pgroup? (list 'new-pgroup: #t) '())
(if pty? (list 'pty: #t) '())
(if inherit-fd (list 'inherit-fd: inherit-fd) '())
- (if rlimits (list 'rlimits: rlimits) '()))]
+ (if rlimits (list 'rlimits: rlimits) '())
+ (if seccomp-lock? (list 'seccomp-lock?: #t) '()))]
[h (cond
[c-jerboa-aproc-spawn
(apply aproc-spawn* argv spawn-kw)]
diff --git a/lib/std/security/worker.ss b/lib/std/security/worker.ss
index ed8ddfb..e23b955 100644
--- a/lib/std/security/worker.ss
+++ b/lib/std/security/worker.ss
@@ -43,7 +43,7 @@
(import (chezscheme)
(only (jerboa core) def defstruct)
- (only (std os aproc) aproc-run/status*)
+ (only (std os aproc) aproc-run/status* aproc-native-spawn-available?)
(only (std os limits)
limit-policy
limit-policy-set!
@@ -241,6 +241,21 @@
(def (worker-sandbox-refused-axes pol caps)
(missing-required-axes (worker-policy-sandbox pol) caps))
+ (def (worker-native-seccomp-requested? pol)
+ (let ([axes (worker-policy-sandbox pol)])
+ (or (memq 'syscalls axes) (memq 'ptrace axes))))
+
+ (def (worker-native-seccomp-refused-axes pol)
+ (if (and (worker-native-seccomp-requested? pol)
+ (not (aproc-native-spawn-available?)))
+ (let lp ([xs '(syscalls ptrace)] [out '()])
+ (cond
+ [(null? xs) (reverse out)]
+ [(memq (car xs) (worker-policy-sandbox pol))
+ (lp (cdr xs) (cons (car xs) out))]
+ [else (lp (cdr xs) out)]))
+ '()))
+
(def (egress-policy-summary pol)
(let ([egress (worker-policy-egress-policy pol)])
(if egress
@@ -297,6 +312,8 @@
`((backend . ,(sandbox-backend))
(capabilities . ,caps)
(memory-limit-bytes . ,(worker-policy-memory-limit-bytes pol))
+ (native-seccomp-requested? . ,(and (worker-native-seccomp-requested? pol) #t))
+ (native-seccomp-available? . ,(aproc-native-spawn-available?))
(sandbox . ,(worker-policy-sandbox pol))
(egress . ,(egress-policy-summary pol))
(env . ,(env-policy-audit-summary (worker-policy-env-policy pol)))
@@ -315,6 +332,9 @@
(let ([rlimits (limit-policy->rlimits limits)])
(if (null? rlimits) '() (list 'rlimits: rlimits)))
'()))
+ (if (worker-native-seccomp-requested? pol)
+ (list 'seccomp-lock?: #t)
+ '())
(list 'timeout-ms: (worker-policy-timeout-ms pol)
'encoding: 'utf8-lossy
'new-pgroup: #t))])
@@ -352,6 +372,9 @@
(memory-limit-installed? . ,(and (worker-policy-memory-limit-bytes pol)
(memory-limit-installable? pol)))
(memory-limit-report . ,(worker-memory-limit-report pol))
+ (native-seccomp-requested? . ,(and (worker-native-seccomp-requested? pol) #t))
+ (native-seccomp-installed? . ,(and (worker-native-seccomp-requested? pol)
+ (aproc-native-spawn-available?)))
(sandbox . ,(worker-policy-sandbox pol))
(egress . ,(egress-policy-summary pol))
(stdout-truncated? . ,stdout-truncated?)
@@ -371,9 +394,12 @@
[required-missing (missing-required-axes (worker-policy-require pol) caps)]
[sandbox-missing (worker-sandbox-refused-axes pol caps)]
[memory-missing (memory-limit-refused-axes pol)]
+ [native-seccomp-missing (worker-native-seccomp-refused-axes pol)]
[missing (append-refused-axes
- (append-refused-axes required-missing sandbox-missing)
- memory-missing)])
+ (append-refused-axes
+ (append-refused-axes required-missing sandbox-missing)
+ memory-missing)
+ native-seccomp-missing)])
(cond
[(and (worker-policy-fail-closed? pol) (pair? missing))
(worker-refusal-result command pol caps missing 'required-axis-unavailable)]
diff --git a/tests/test-aproc.ss b/tests/test-aproc.ss
index 69d69fe..3b68af6 100644
--- a/tests/test-aproc.ss
+++ b/tests/test-aproc.ss
@@ -96,6 +96,14 @@
(cons out code))
'("0\n" . 0))
+(test-pred "seccomp-lock?: rejects pty"
+ (guard (exn [#t (and (message-condition? exn)
+ (condition-message exn))])
+ (aproc-spawn* '("true") 'pty: #t 'seccomp-lock?: #t)
+ #f)
+ (lambda (msg)
+ (equal? msg "seccomp-lock?: #t is not supported with pty: #t")))
+
;; ===== stdin from value =====
(test "stdin: string"
(let-values (((out err code)
diff --git a/tests/test-security2-parsers.ss b/tests/test-security2-parsers.ss
index 1794663..bef35b0 100644
--- a/tests/test-security2-parsers.ss
+++ b/tests/test-security2-parsers.ss
@@ -13,6 +13,7 @@
(std pregexp)
(std net dns)
(std net websocket)
+ (only (std security taint) tainted? untaint)
(jerboa reader))
(define test-count 0)
@@ -325,8 +326,11 @@
(let* ([payload (string->utf8 "hello")]
[frame (ws-text-frame payload)]
[encoded (ws-frame-encode frame)]
- [decoded (ws-frame-decode encoded)])
- (assert-equal "payload" payload (ws-frame-payload decoded)))))
+ [decoded (ws-frame-decode encoded)]
+ [decoded-payload (ws-frame-payload decoded)])
+ (unless (tainted? decoded-payload)
+ (error 'assert-equal "decoded payload should be tainted" decoded-payload))
+ (assert-equal "payload" payload (untaint decoded-payload)))))
(test "ws: too short for header"
(lambda ()
diff --git a/tests/test-worker.ss b/tests/test-worker.ss
index 06777e6..bbbd745 100644
--- a/tests/test-worker.ss
+++ b/tests/test-worker.ss
@@ -173,6 +173,24 @@
'sandbox 'missing)
'(definitely-unavailable-sandbox-axis)))))
+(test-pred "requested syscall sandbox installs native seccomp or refuses"
+ (worker-run-eval
+ "(+ 1 1)"
+ (worker-policy
+ 'command: worker-command
+ 'timeout-ms: 3000
+ 'sandbox: '(syscalls ptrace)
+ 'fail-closed?: #t))
+ (lambda (r)
+ (and (worker-result? r)
+ (let ([diag (worker-result-diagnostics r)])
+ (if (worker-result-launched? r)
+ (and (equal? (worker-result-status r) 0)
+ (eq? (alist-ref/default diag 'native-seccomp-requested? #f) #t)
+ (eq? (alist-ref/default diag 'native-seccomp-installed? #f) #t))
+ (and (equal? (worker-result-status r) 126)
+ (pair? (worker-result-refused-axes r))))))))
+
(test-pred "egress proxy policy reaches worker env"
(worker-run-command
'("/usr/bin/env")