security: add taint propagation benchmark
Jaime Fournier
5bcaf970089c5732a8c436e6a9362dcbd9110576
new file mode 100644 --- /dev/null +++ b/benchmarks/bench-taint.ss @@ -0,0 +1,68 @@ +#!/usr/bin/env scheme-script +#!chezscheme +;;; bench-taint.ss -- measure overhead of taint propagation wrappers. +;;; +;;; Run: bin/jerboa run benchmarks/bench-taint.ss [iterations] +;;; Default iterations: 200000 + +(import (scheme) + (std security taint)) + +(define (monotonic-ms) + (let ([now (current-time 'time-monotonic)]) + (+ (* (time-second now) 1000) + (quotient (time-nanosecond now) 1000000)))) + +(define (decimal-positive-integer s default) + (let ([len (string-length s)]) + (let loop ([i 0] [n 0]) + (cond + [(= i len) (if (> n 0) n default)] + [else + (let ([ch (string-ref s i)]) + (if (and (char>=? ch #\0) (char<=? ch #\9)) + (loop (+ i 1) (+ (* n 10) (- (char->integer ch) 48))) + default))])))) + +(define (iterations-from-argv default) + (let ([args (command-line)]) + (if (and (pair? args) (pair? (cdr args))) + (decimal-positive-integer (cadr args) default) + default))) + +(define (bench name iterations thunk) + (let ([started (monotonic-ms)] + [result (thunk iterations)]) + (let ([elapsed (- (monotonic-ms) started)]) + (printf "~a\n" name) + (printf " iterations: ~a\n" iterations) + (printf " elapsed-ms: ~a\n" elapsed) + (printf " result: ~a\n\n" result) + elapsed))) + +(define (clean-string-work iterations) + (let loop ([i 0] [sum 0]) + (if (= i iterations) + sum + (let ([s (string-append "prefix-" "payload" "-suffix")]) + (loop (+ i 1) (+ sum (string-length s))))))) + +(define (tainted-string-work iterations) + (let ([payload (taint-http "payload")]) + (let loop ([i 0] [sum 0]) + (if (= i iterations) + sum + (let ([s (tainted-string-append "prefix-" payload "-suffix")]) + (loop (+ i 1) (+ sum (tainted-string-length s)))))))) + +(define (print-ratio clean-ms tainted-ms) + (if (> clean-ms 0) + (printf "taint-overhead-ratio: ~,3f\n" + (/ (exact->inexact tainted-ms) + (exact->inexact clean-ms))) + (printf "taint-overhead-ratio: n/a (baseline below timer resolution)\n"))) + +(let* ([iterations (iterations-from-argv 200000)] + [clean-ms (bench "clean string append" iterations clean-string-work)] + [tainted-ms (bench "tainted string append" iterations tainted-string-work)]) + (print-ratio clean-ms tainted-ms)) --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -304,8 +304,8 @@ when" must be answerable from `dist/release-evidence/` in minutes. | Goal | Where we stand | The gap | |---|---|---| -| G1 shrink target | Managed core is memory-safe; parsers budgeted; safe prelude exists; FFI audit inventory and per-site provisional verdicts exist | FFI remediation remains: null/width/bounds/GC-safety scanner rules, unsafe invariant comments, native export shrinking/justification, and the `vendor/jsqlite` decision | -| G2 cap blast radius | Capabilities, taint, kernel sandbox, egress policy objects, worker facade, memory rlimit pre-exec path, egress proxy env wiring, and authenticated actor transport/envelopes all exist | **No native pre-exec worker backend yet for kernel sandbox controls before child input**; protocol/network taint source defaults and performance measurement remain open | +| G1 shrink target | Managed core is memory-safe; parsers budgeted; safe prelude exists; FFI audit inventory, per-site provisional verdicts, and FFI hazard scanner rules exist | FFI remediation remains: unsafe invariant comments, native export shrinking/justification, and the `vendor/jsqlite` decision | +| G2 cap blast radius | Capabilities, taint, kernel sandbox, egress policy objects, worker facade, memory rlimit pre-exec path, egress proxy env wiring, and authenticated actor transport/envelopes all exist | **No native pre-exec worker backend yet for kernel sandbox controls before child input**; protocol/network taint source defaults remain open | | G3 find it first | 13 harnesses, scanner w/ rule DB, lint | No corpora, no crash regression, no scheduled fuzzing, no standing AI-red-team, no exploit-shaped regression suite | | G4 fail closed | Raw-fork launchers retired correctly; `allow-degraded?` explicit | New controls must keep the invariant; degraded-mode warnings must be test-locked | | G5 recover fast | SBOM/repro/signing gates exist | TCB accounting manual; doc drift (stale tables, undocumented modules); independent-builder reproducibility not yet routine | @@ -603,11 +603,13 @@ not started." Rust sites. The reporter now skips comment-only mentions, emits per-site `hazards` and provisional `verdict` fields in `--full` output, and summarizes blocking review, pointer/width safety review, unsafe-comment review, and - export-review counts. `jerboa-native-rs` now denies + export-review counts. `data/security-rules.sexp` includes the P1-01 FFI + hazard rules for collect-safe bytevector pinning, pointer return null guards, + integer width ambiguity, and pointer arithmetic bounds review. + `jerboa-native-rs` now denies `unsafe_op_in_unsafe_fn`, so unsafe function bodies do not implicitly permit - unsafe operations. Remaining work: scanner rules for null/width/bounds/ - GC-safety hazards, unsafe invariant comments, export shrinking/ - justification, and the `vendor/jsqlite` CVE/replacement decision. + unsafe operations. Remaining work: unsafe invariant comments, export + shrinking/justification, and the `vendor/jsqlite` CVE/replacement decision. ### K3-P1-02 — TOCTOU-safe filesystem capability checks **Serves:** G2. **Effort:** 1 week. @@ -647,16 +649,16 @@ Taint is opt-in; native sinks don't check it. In the safe prelude, the now cover trim/split/join, UTF-8 string/bytevector conversion, bytevector copy, and bytevector element reads. Safe-prelude `getenv`, `read-file-string`, and `read-file-lines` now taint present environment - variables and file contents by default. + variables and file contents by default. `benchmarks/bench-taint.ss` gives a + reproducible local overhead measurement for taint propagation wrappers. - **Remaining:** (b) Taint sources by default for HTTP request fields and - network frames — wrapped at the protocol boundary. (c) Performance: measure; - if overhead matters, document - `*taint-enforce* #f` as an explicit, warned de-opt — never silent. + network frames — wrapped at the protocol boundary. - **Accept:** partially satisfied: tainted values reaching standard sink names in safe-prelude code raise `&taint-violation`; untainted flows are covered by existing safe-prelude tests; env/file-content reads are tainted by default; - `safety-guide.md` has a taint section. Full completion still requires - source-default work at protocol/network boundaries. + `safety-guide.md` has a taint section; taint overhead has a reproducible + benchmark. Full completion still requires source-default work at + protocol/network boundaries. ### K3-P1-04 — Decide `define-syntax` in the sandbox allowlist **Serves:** G1. **Effort:** 2–3 days. @@ -1161,9 +1163,9 @@ fake confidence happens. - No covert-channel analysis; Chez GC is a timing side channel (P3-07). - Seccomp tables cover x86_64/aarch64 only; Landlock needs Linux 5.13+; Seatbelt/Capsicum have thinner test coverage (P1-08). -- Safe-prelude file/shell/delete sink names now check taint, and env/file - content reads are source-tainted by default; protocol/network source marking - and performance measurement remain P1-03 follow-up. +- Safe-prelude file/shell/delete sink names now check taint, env/file content + reads are source-tainted by default, and taint overhead has a benchmark; + protocol/network source marking remains P1-03 follow-up. - Distributed actor authentication is available in `(std actor transport)` and `(std actor distributed)` authenticated envelopes; compatibility raw serialization remains unauthenticated. @@ -1178,9 +1180,10 @@ fake confidence happens. HMAC/AEAD/KDF/password APIs route to Rust native crypto. - Secure memory has a checked `secure-bytevector` API; raw secure-region pointers remain for explicit FFI integration. -- FFI audit phase 5 has a reproducible inventory and per-site provisional - verdicts; null/width/bounds/GC-safety remediation and the `vendor/jsqlite` - TCB decision remain open. +- FFI audit phase 5 has a reproducible inventory, per-site provisional + verdicts, and committed hazard scanner rules; unsafe invariant comments, + export shrinking/justification, and the `vendor/jsqlite` TCB decision remain + open. - No independent red-team evaluation yet (P2-04 starts the practice). - The confined exec worker facade exists as `(std security worker)`, with audit-log lifecycle events and native pre-exec memory rlimits on supported --- a/docs/safety-guide.md +++ b/docs/safety-guide.md @@ -292,7 +292,9 @@ In `(jerboa prelude safe)`, `getenv`, `read-file-string`, and are tagged as `env-input`, file contents are tagged as `file-input`, and missing environment-variable defaults are returned unchanged. Protocol and network modules still need explicit source marking unless their API documents a -tainted-by-default boundary. +tainted-by-default boundary. Measure local taint wrapper overhead with +`jerboa run benchmarks/bench-taint.ss 200000` before changing enforcement +policy for performance reasons. ### What the Contracts Check --- a/docs/security-reference.md +++ b/docs/security-reference.md @@ -359,6 +359,13 @@ The safe prelude also treats environment and file contents as taint sources: returns `file-input` taint, and `read-file-lines` taints each returned line. Missing environment-variable defaults are returned unchanged. +Use `benchmarks/bench-taint.ss` to measure taint-propagation overhead on the +current runtime and hardware: + +```sh +jerboa run benchmarks/bench-taint.ss 200000 +``` + ### Taint-propagating string operations `tainted-string-append`, `tainted-string-ref`, `tainted-substring`, @@ -918,7 +925,9 @@ These are known gaps documented as current limitations, not implementation promi Safe-prelude `getenv`, `read-file-string`, and `read-file-lines` mark environment and file contents by default, but raw Chez operations and unsafe imports do not check taint. Protocol and network sources still need explicit - `taint-*` marking until those modules wrap inputs by default. + `taint-*` marking until those modules wrap inputs by default. Track local + taint overhead with `benchmarks/bench-taint.ss` before accepting any + performance-based de-optimization. - **Distributed actor authentication is opt-in at the serialization layer.** `(std actor transport)` authenticates TCP traffic, and `(std actor distributed)` exposes HMAC'd envelopes with timestamp and monotonic sequence replay checks. Compatibility callers that use only `serialize-message` / `deserialize-message` still get parsing limits but no peer authentication. - **Filesystem capabilities are not process sandboxes.** They validate paths at the capability API boundary. Code with raw Chez file primitives or