security: report sandbox confinement state
Jaime Fournier <jaimef@linbsd.org>
a34356bd7748a5f0f3b7814d4dbcd1937207ba2f
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 9ac940e..c62daaf 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -733,8 +733,11 @@ Chez cannot heap-cap a thread; `run-safe-eval` rightly refuses
supported syscall tables: `(std security seccomp)` now exports
`http-server-filter`, `dns-server-filter`, and `worker-eval-filter`, and
`(std security sandbox)` accepts `'http-server`, `'dns-server`, and
- `'worker-eval` seccomp specs. Native per-platform CI parity and a dedicated
- runtime `sandbox-report` entry point remain open.
+ `'worker-eval` seccomp specs. `(std security sandbox)` now exports
+ `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.
### K3-P1-09 — Confused-deputy defenses: capability plans enforced at runtime
**Serves:** G2. **Effort:** 1 week.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index fbaa0c3..1a63115 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -584,6 +584,11 @@ continue without those controls. The flag does not install a weaker sandbox.
`make-sandbox-config` also accepts `capabilities`, `max-output-size`, and
`allow-degraded?`. Non-empty capabilities require an exec worker. The output
limit applies only to the formatted return value.
+`sandbox-report` returns an audit-friendly alist for the current default config
+or an explicit config; it records platform, requested process controls,
+degraded `run-safe-eval` state, active sandbox-child reservations, and
+configured/available/installed state for seccomp, Landlock, Seatbelt, Capsicum,
+and memory limits.
### Failure behavior
@@ -858,7 +863,7 @@ These are known gaps documented as current limitations, not implementation promi
- **No FIPS 140-3 validation.** The crypto uses ring (recommended) or OpenSSL (legacy), which can be FIPS-validated, but Jerboa itself has not undergone FIPS evaluation.
- **No covert channel analysis.** Chez Scheme's GC is a timing side channel. No mitigation exists for timing, storage, or resource-exhaustion covert channels.
- **No Common Criteria evaluation.** No Protection Profile, Security Target, or EAL evaluation has been performed.
-- **Seccomp architecture coverage is limited.** The BPF bytecode generator supports x86_64 and aarch64 syscall numbers.
+- **Seccomp architecture coverage is limited.** The BPF bytecode generator supports x86_64 and aarch64 syscall numbers. Use `sandbox-report` in audit logs or release evidence to record which confinement specs were requested, available, or installed for the current platform.
- **Landlock requires Linux 5.13+.** No equivalent on macOS, BSDs, or older Linux kernels. `landlock-available?` returns `#f` on unsupported systems.
- **Taint enforcement depends on the safe surface.** `(jerboa prelude safe)`
binds default file/shell/delete sink names to taint-checking wrappers, but raw
diff --git a/lib/std/security/sandbox.ss b/lib/std/security/sandbox.ss
index 099f642..cfa0e60 100644
--- a/lib/std/security/sandbox.ss
+++ b/lib/std/security/sandbox.ss
@@ -24,6 +24,7 @@
(export
run-safe
run-safe-eval
+ sandbox-report
make-sandbox-config
sandbox-config?
*sandbox-timeout*
@@ -172,6 +173,32 @@
(with-mutex %sandbox-children-mutex
(hashtable-delete! %active-sandbox-children token-or-pid))))
+ (def %installed-sandbox-protections (make-hashtable equal-hash equal?))
+ (def %installed-sandbox-protections-mutex (make-mutex))
+
+ (def (mark-protection-installed! name)
+ (with-mutex %installed-sandbox-protections-mutex
+ (hashtable-set! %installed-sandbox-protections name #t)))
+
+ (def (protection-installed? name)
+ (with-mutex %installed-sandbox-protections-mutex
+ (hashtable-ref %installed-sandbox-protections name #f)))
+
+ (def (active-sandbox-child-count)
+ (with-mutex %sandbox-children-mutex
+ (hashtable-size %active-sandbox-children)))
+
+ (def (requested-protection? spec)
+ (and spec (not (null? spec))))
+
+ (def (protection-report name spec supported? available? installed?)
+ (list (cons 'name name)
+ (cons 'requested? (and (requested-protection? spec) #t))
+ (cons 'spec spec)
+ (cons 'supported? supported?)
+ (cons 'available? available?)
+ (cons 'installed? installed?)))
+
(defstruct %sandbox-config (timeout seccomp landlock seatbelt capsicum capabilities max-output-size max-memory-size allow-degraded?))
(def %make-sandbox-config make-%sandbox-config)
(def sandbox-config? %sandbox-config?)
@@ -292,6 +319,52 @@
(lambda ()
(make-sandbox-config)))
+ (def (sandbox-report . maybe-config)
+ (when (> (length maybe-config) 1)
+ (error 'sandbox-report "expected at most one sandbox-config" maybe-config))
+ (let ([cfg (if (null? maybe-config) (default-config) (car maybe-config))])
+ (unless (sandbox-config? cfg)
+ (error 'sandbox-report "expected sandbox-config" cfg))
+ (let ([requested (requested-process-controls cfg)])
+ (list
+ (cons 'platform *current-platform*)
+ (cons 'active-children (active-sandbox-child-count))
+ (cons 'run-safe-eval
+ (list (cons 'process-controls-requested (map car requested))
+ (cons 'degraded? (and (pair? requested)
+ (%sandbox-config-allow-degraded? cfg)
+ #t))))
+ (cons 'seccomp
+ (protection-report 'seccomp
+ (%sandbox-config-seccomp cfg)
+ (eq? *current-platform* 'linux)
+ (seccomp-available?)
+ (protection-installed? 'seccomp)))
+ (cons 'landlock
+ (protection-report 'landlock
+ (%sandbox-config-landlock cfg)
+ (eq? *current-platform* 'linux)
+ (landlock-available?)
+ (protection-installed? 'landlock)))
+ (cons 'seatbelt
+ (protection-report 'seatbelt
+ (%sandbox-config-seatbelt cfg)
+ (eq? *current-platform* 'macos)
+ (seatbelt-available?)
+ (protection-installed? 'seatbelt)))
+ (cons 'capsicum
+ (protection-report 'capsicum
+ (%sandbox-config-capsicum cfg)
+ (eq? *current-platform* 'freebsd)
+ (capsicum-available?)
+ (protection-installed? 'capsicum)))
+ (cons 'memory-limit
+ (protection-report 'memory-limit
+ (%sandbox-config-max-memory-size cfg)
+ #t
+ #t
+ (protection-installed? 'memory-limit)))))))
+
(def (run-safe thunk . maybe-config)
(when (> (length maybe-config) 1)
(error 'run-safe "expected at most one sandbox-config" maybe-config))
@@ -533,23 +606,29 @@
;; Step 1: Install Landlock filesystem restrictions
(when landlock-rules
(if (landlock-available?)
- (landlock-install! landlock-rules)
+ (begin
+ (landlock-install! landlock-rules)
+ (mark-protection-installed! 'landlock))
(unavailable-protection! 'landlock 'landlock allow-degraded?)))
;; Step 2: Install seccomp syscall filter
(when seccomp-filter
(if (seccomp-available?)
- (seccomp-install! seccomp-filter)
+ (begin
+ (seccomp-install! seccomp-filter)
+ (mark-protection-installed! 'seccomp))
(unavailable-protection! 'seccomp 'seccomp allow-degraded?))))
(def (install-macos-protections! seatbelt-profile allow-degraded?)
;; Install Seatbelt sandbox profile
(when seatbelt-profile
(if (seatbelt-available?)
- (if (string? seatbelt-profile)
- ;; Raw SBPL string
- (seatbelt-install-profile! seatbelt-profile)
- ;; Named profile symbol
- (seatbelt-install! seatbelt-profile))
+ (begin
+ (if (string? seatbelt-profile)
+ ;; Raw SBPL string
+ (seatbelt-install-profile! seatbelt-profile)
+ ;; Named profile symbol
+ (seatbelt-install! seatbelt-profile))
+ (mark-protection-installed! 'seatbelt))
(unavailable-protection! 'seatbelt 'seatbelt allow-degraded?))))
(def (install-freebsd-protections! resolved-capsicum allow-degraded?)
@@ -559,7 +638,8 @@
(when resolved-capsicum
(if (capsicum-available?)
(when (list? resolved-capsicum)
- (capsicum-apply-preset! resolved-capsicum))
+ (capsicum-apply-preset! resolved-capsicum)
+ (mark-protection-installed! 'capsicum))
(unavailable-protection! 'capsicum 'capsicum allow-degraded?))))
(def (install-memory-limit! max-memory-size allow-degraded?)
@@ -569,7 +649,8 @@
(let ([results (limit-policy-install! policy)])
(let ([mem-result (cdr (assq 'mem results))])
(cond
- [(eq? mem-result 'installed) (void)]
+ [(eq? mem-result 'installed)
+ (mark-protection-installed! 'memory-limit)]
[(eq? mem-result 'degraded)
(unavailable-protection! 'limit 'memory-limit allow-degraded?)]
[else
diff --git a/tests/security/test-k3-regressions.ss b/tests/security/test-k3-regressions.ss
index 24dcce3..9c18719 100644
--- a/tests/security/test-k3-regressions.ss
+++ b/tests/security/test-k3-regressions.ss
@@ -105,6 +105,10 @@
(cond
[(assq key (capability-permissions cap)) => cdr]
[else #f]))
+(define (alist-ref/default al key default)
+ (cond
+ [(assq key al) => cdr]
+ [else default]))
(define (capability-attenuation-denies-escalation?)
(let* ([cap (make-fs-capability 'read: #t 'write: #f 'paths: '("/tmp"))]
@@ -230,6 +234,16 @@
(raises-pred? sandbox-error? (lambda () (run-safe-eval "\"abcdef\"" (eval-only-config 'max-output-size 4)))) => #t)
(check "K3-AI-14 run-safe-eval refuses process controls without allow-degraded"
(raises-pred? sandbox-error? (lambda () (run-safe-eval "(+ 1 2)" (eval-only-config 'max-memory-size 1024)))) => #t)
+(check "K3-P1-08 sandbox-report exposes degraded requested controls"
+ (let* ([cfg (eval-only-config 'max-memory-size 1024 'allow-degraded? #t)]
+ [report (sandbox-report cfg)]
+ [eval-report (alist-ref/default report 'run-safe-eval '())]
+ [memory-report (alist-ref/default report 'memory-limit '())])
+ (and (equal? (alist-ref/default eval-report 'process-controls-requested '())
+ '(memory-limit))
+ (eq? (alist-ref/default eval-report 'degraded? #f) #t)
+ (eq? (alist-ref/default memory-report 'requested? #f) #t)
+ (eq? (alist-ref/default memory-report 'installed? #t) #f))) => #t)
(check "K3-AI-04 empty network host policy denies connects"
(empty-host-capability-denies-network?) => #f)
(check "K3-AI-03 attenuate-capability cannot add write or paths"