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"