security: add worker audit lifecycle records
ober
39692be15a438bc029dcf340ba62dbf8c4f1d16c
--- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -341,8 +341,9 @@ the code that ships. This violates the repo's own pre-commit rule. ### K3-P0-02 — Build the exec-based confined worker (`(std security worker)`) **Serves:** G2, G3, G4. **Effort:** 1–2 weeks. **Status:** initial `(std security worker)` facade landed 2026-07-27 with `tests/test-worker.ss`; -native pre-exec controls, egress proxy wiring, memory rlimit, and audit-log -events remain open. +audit-log start/end records landed 2026-07-27 and are asserted by +`tests/test-worker.ss`. Native pre-exec controls, egress proxy wiring, and +memory rlimit remain open. Every security doc routes adversarial work to "a bounded, separately exec'd worker". The initial facade exists; finish it as the assembly point for --- a/docs/security-reference.md +++ b/docs/security-reference.md @@ -249,10 +249,11 @@ returns `launched? = #f`, status `126`, and the refused axis list instead of silently running with weaker controls. Current limitation: the worker has a real exec boundary, pure environment, -parent deadline, process-group kill through `aproc`, and returned output caps. -The remaining pre-exec kernel-control backend is not complete, so rlimit, -Landlock/seccomp, Seatbelt/Capsicum installation, deny-default egress proxy -wiring, and audit-log start/end records remain tracked by the K3 handoff. +parent deadline, process-group kill through `aproc`, returned output caps, and +audit-log start/end records in `worker-result-diagnostics`. The remaining +pre-exec kernel-control backend is not complete, so rlimit, Landlock/seccomp, +Seatbelt/Capsicum installation, and deny-default egress proxy wiring remain +tracked by the K3 handoff. --- --- a/lib/std/security/worker.ss +++ b/lib/std/security/worker.ss @@ -42,6 +42,13 @@ (only (jerboa core) def defstruct) (only (std os aproc) aproc-run/status*) (only (std os limits sandbox) sandbox-capabilities sandbox-backend) + (only (std security audit-log) + audit-log + audit-log-now + audit-log-record! + audit-log-records + audit-process-start + audit-process-exit) (only (std security env) env-policy? env-policy-default @@ -188,12 +195,24 @@ (apply aproc-run/status* command args)]) (list stdout stderr status)))) - (def (worker-finish-result command pol caps captured) + (def (worker-finish-result command pol caps captured log start-ms) (let* ([stdout (car captured)] [stderr (cadr captured)] [status (caddr captured)] [stdout* (truncate-string stdout (worker-policy-stdout-cap-bytes pol))] - [stderr* (truncate-string stderr (worker-policy-stderr-cap-bytes pol))]) + [stderr* (truncate-string stderr (worker-policy-stderr-cap-bytes pol))] + [stdout-truncated? (truncated? stdout stdout*)] + [stderr-truncated? (truncated? stderr stderr*)]) + (audit-log-record! + log + (audit-process-exit + 'status: status + 'elapsed-ms: (max 0 (- (audit-log-now) start-ms)) + 'stdout-bytes: (string-length stdout) + 'stderr-bytes: (string-length stderr) + 'stdout-truncated?: stdout-truncated? + 'stderr-truncated?: stderr-truncated? + 'tag: 'worker)) (make-worker-result #t status @@ -202,9 +221,10 @@ '() `((backend . ,(sandbox-backend)) (capabilities . ,caps) - (stdout-truncated? . ,(truncated? stdout stdout*)) - (stderr-truncated? . ,(truncated? stderr stderr*)) - (env . ,(env-policy-audit-summary (worker-policy-env-policy pol)))) + (stdout-truncated? . ,stdout-truncated?) + (stderr-truncated? . ,stderr-truncated?) + (env . ,(env-policy-audit-summary (worker-policy-env-policy pol))) + (audit . ,(audit-log-records log))) command))) (def (worker-run-command* command pol stdin-data) @@ -227,11 +247,22 @@ [(eq? (car validation) 'error) (worker-refusal-result command pol caps '() (cdr validation))] [else - (worker-finish-result - command - pol - caps - (run-aproc-captured command pol env stdin-data))]))]))) + (let ([log (audit-log)] + [start-ms (audit-log-now)]) + (audit-log-record! + log + (audit-process-start + 'command: command + 'env: (env-policy-audit-summary (worker-policy-env-policy pol)) + 'cwd: (or (worker-policy-cwd pol) "") + 'tag: 'worker)) + (worker-finish-result + command + pol + caps + (run-aproc-captured command pol env stdin-data) + log + start-ms))]))]))) (def (worker-run-command argv . maybe-policy) (let* ([pol (if (null? maybe-policy) (worker-policy) (car maybe-policy))] --- a/tests/test-worker.ss +++ b/tests/test-worker.ss @@ -22,6 +22,10 @@ [(and (pair? al) (assq key al)) => cdr] [else default])) +(define (worker-audit-event-types result) + (map (lambda (rec) (alist-ref/default rec 'type #f)) + (alist-ref/default (worker-result-diagnostics result) 'audit '()))) + (define (test-pred name expr pred) (guard (exn [#t (set! fail (+ fail 1)) (printf "FAIL ~a: threw ~a~%" name @@ -75,6 +79,15 @@ (worker-run-eval "(+ 1 2)" (test-policy 3000)) ok-result?) +(test-pred "worker records audit start and exit" + (worker-run-eval "(+ 1 2)" (test-policy 3000)) + (lambda (r) + (let ([types (worker-audit-event-types r)]) + (and (worker-result? r) + (worker-result-launched? r) + (memq 'process_start types) + (memq 'process_exit types))))) + (test-pred "system unavailable in worker" (worker-run-eval "(system \"true\")" (test-policy 3000)) error-result?) @@ -88,7 +101,7 @@ error-result?) (test-pred "cpu spin times out" - (worker-run-eval "(let loop () (loop))" (test-policy 200)) + (worker-run-eval "(let loop () (loop))" (test-policy 50)) (lambda (r) (and (worker-result? r) (worker-result-launched? r)