docs: consolidate security references

Jaime Fournier <jaimef@linbsd.org>

44b9fd4f92878adff2c192c32193df2ee24a9978

diff --git a/SECURITY.md b/SECURITY.md
index 178c96f..7c464a2 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -5,9 +5,11 @@ MCP server, LSP server, package tooling, native Rust backend, and multicall
 binary source. Treat it as security-sensitive even when a change looks like
 library or developer tooling work.
 
-Production claims require the gates in [`docs/release-security.md`](docs/release-security.md)
+Implemented security behavior is documented in
+[`docs/security-reference.md`](docs/security-reference.md). Production claims
+require the gates in [`docs/release-security.md`](docs/release-security.md)
 and the cross-repository tracker at `~/mine/jerboa-production-readiness.md`.
-The security roadmap and prioritized implementation handoff lives in
+The prioritized security backlog and implementation handoff lives in
 [`docs/kimi3-security-recommmendations.md`](docs/kimi3-security-recommmendations.md).
 
 ## Supported Status
diff --git a/docs/ai-threat.md b/docs/ai-threat.md
index 090b707..a821afb 100644
--- a/docs/ai-threat.md
+++ b/docs/ai-threat.md
@@ -1,6 +1,6 @@
 # Jerboa Security Assessment: AI-Assisted Adversarial Threat Model
 
-Updated 2026-03-21. Honest assessment — no hype, no stubs.
+Updated 2026-07-27. Honest assessment -- no hype, no stubs.
 
 The threat model: adversaries using AI tools (LLMs, AI-powered fuzzers,
 automated vulnerability scanners) to find and exploit bugs in applications
@@ -71,8 +71,10 @@ amount of clever prompting or input crafting will summon them. The allowlist
 approach is provably closed — unlike blocklists, which are always one oversight
 away from failure.
 
-The 113-binding allowlist in `(std security restrict)` is small enough to
-audit by hand. Future Chez Scheme additions cannot leak in.
+The `(std security restrict)` allowlist is small enough to audit by hand and is
+checked by `make audit` against the manifest in
+[`security-reference.md`](security-reference.md). Future Chez Scheme additions
+cannot leak in.
 
 ### Kernel-Enforced Protections Cannot Be Reasoned Around
 
@@ -164,12 +166,11 @@ from in-process evaluation and creates no temporary result file.
 The current exec-worker facade uses stdin/stdout/stderr pipes and returned
 output caps rather than a predictable temporary filename.
 
-### The Pre-Existing Import Conflict
+### Safe Prelude Import Resolution
 
-The safe prelude has a "multiple definitions" warning from overlapping symbol
-exports between imported modules. This isn't a security bug but it means symbol
-resolution order could surprise developers, potentially leading to calling the
-wrong version of a function (safe vs unsafe) in edge cases.
+The safe prelude import-conflict cleanup landed on 2026-07-27. Export
+resolution is tested so application scripts do not depend on ambiguous
+`sort`/`hash`/safe-wrapper ordering.
 
 ---
 
@@ -211,7 +212,7 @@ patterns. If the pattern doesn't exist, there's nothing to find.
 | Temp file race in `run-safe` | Medium | Obsolete: thunk execution and child result paths are retired |
 | Unsafe Scheme execution after raw fork | High | Fixed: `run-safe` fails closed; use an exec worker |
 | No memory limit in `run-safe-eval` | Medium | By design: use OS limits on an exec worker |
-| Import conflict in safe prelude | Low | Known; tracked because deterministic safe-symbol resolution matters |
+| Import conflict in safe prelude | Low | Fixed: deterministic safe-symbol resolution is regression-tested |
 | SQL injection heuristic bypasses | Medium | By design (use parameterized queries) |
 | Seccomp/Landlock x86_64-only | Low | By design (arch-specific syscalls) |
 | Silent degradation of requested process controls | Medium | Fixed: reject unless `allow-degraded? #t` is explicit |
diff --git a/docs/capability.md b/docs/capability.md
index 46188b4..947cdd5 100644
--- a/docs/capability.md
+++ b/docs/capability.md
@@ -51,7 +51,12 @@ An fs capability controls file system access.
 
 - `read?` — whether the holder may open files for reading.
 - `write?` — whether the holder may open files for writing.
-- `paths` — a list of path prefixes that the capability covers, or `#f` for unrestricted access. Access is checked via string prefix matching: a path is allowed if it begins with any element of the list.
+- `paths` — a list of allowed path roots, or `#f` for unrestricted access.
+  Current `(std security capability)` checks resolve existing paths through an
+  opened fd with `O_NOFOLLOW` where the platform exposes it, then compare the
+  actual resolved location against the allowed roots. Missing create-target
+  leaves use lexical normalization so a capability can still authorize creating
+  a new file under an allowed directory.
 
 ```scheme
 ;; Capability covering all files (read and write)
@@ -163,7 +168,8 @@ Raises an error if:
 - The capability is invalid or revoked.
 - The capability is not an fs capability.
 - Write mode is requested but the capability is read-only.
-- The path is not covered by the capability's path list.
+- The path is not covered by the capability's path roots after canonicalization
+  or safe lexical normalization for a missing create target.
 
 **`cap-file-read`** — opens the file for reading, reads its entire contents as a string, and closes the port.
 
@@ -419,7 +425,10 @@ Note: `revoke-capability!` is defined internally but not exported. To use revoca
 
 **Limitations:**
 
-- Path checking is prefix-based string matching. It does not resolve symlinks or normalize `..` components. A path like `/data/../etc/passwd` would pass a `/data/` prefix check if not pre-normalized.
+- Filesystem capability checks are path-root checks, not a whole-process
+  sandbox. Code that keeps raw file-opening primitives in scope can still
+  bypass this layer; use the safe prelude, import auditing, and an exec worker
+  with OS confinement for hostile inputs.
 - `with-sandbox` cannot forcibly terminate a Chez Scheme thread. When a timeout fires, the worker thread continues to run in the background; only the error is raised to the caller.
 - `memory-bytes:` is accepted as an option but is not currently enforced. There is no Chez API for per-thread memory limits.
 - The `cap-connect` function returns a connection specification but does not itself open a TCP socket. The actual socket layer must separately enforce the capability check, or must accept only the result of `cap-connect`.
diff --git a/docs/index.md b/docs/index.md
index e209c83..5f7934f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
 # Jerboa Documentation
 
-Updated 2026-06-25.
+Updated 2026-07-27.
 
 These are the maintained docs for the current Jerboa toolchain. Public examples
 use the `jerboa` CLI: run programs with `jerboa run file.ss`, open the REPL with
@@ -57,8 +57,9 @@ Start with [quickstart.md](quickstart.md), then use
 
 ## Security
 
-- [security-reference.md](security-reference.md) - security module reference and limitation ledger
-- [safety-guide.md](safety-guide.md) - practical secure-application guide
+- [security-reference.md](security-reference.md) - canonical reference for implemented security features, gates, and current limitations
+- [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) - prioritized security backlog and implementation handoff
+- [safety-guide.md](safety-guide.md) - practical secure-application guide for Jerboa programs
 - [release-security.md](release-security.md) - release gates and security evidence
 - [capability.md](capability.md) - object-capability model
 - [limits.md](limits.md) - resource limits, sandboxing, and capability primitives
@@ -66,9 +67,8 @@ Start with [quickstart.md](quickstart.md), then use
 - [harden-usage.md](harden-usage.md) - antidebug, seccomp, integrity, and related APIs
 - [hide.md](hide.md) - reducing information leakage from binaries
 - [unification.md](unification.md) - security-first system architecture
- - [ai-threat.md](ai-threat.md) - AI-assisted adversary assessment
- - [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) - K3-resistant security roadmap and implementation handoff
- - [uhoh.md](uhoh.md) - cryptographic failure lessons
+- [ai-threat.md](ai-threat.md) - AI-assisted adversary assessment
+- [uhoh.md](uhoh.md) - cryptographic failure lessons
 
 ## Build, Packaging, And Release
 
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 1126c9c..af34aa6 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -203,7 +203,7 @@ when" must be answerable from `dist/release-evidence/` in minutes.
 
 | Layer | Module(s) | Verified state | Doc |
 |---|---|---|---|
-| Safe-by-default prelude (no FFI, no `fork-thread`, no `eval`) | `(jerboa prelude safe)`, `(std safe)` | exists; has a known cosmetic import-conflict warning | [safety-guide.md](safety-guide.md) §1 |
+| Safe-by-default prelude (no FFI, no `fork-thread`, no `eval`) | `(jerboa prelude safe)`, `(std safe)` | exists; duplicate-export/import-conflict warning fixed 2026-07-27 | [safety-guide.md](safety-guide.md) §1 |
 | Allowlist sandbox (closed env, bounded `jerboa-read`) | `(std security restrict)` | exists; `safe-bindings` at `restrict.ss:24` | [security-reference.md](security-reference.md) §2 |
 | In-process bounded eval (engine timeout, result cap, fails closed on process controls) | `(std security sandbox)` `run-safe-eval` | exists; no memory limit, no FFI preemption (documented) | security-reference §6 |
 | Capability tokens (sealed, CSPRNG nonce, monotone attenuation, revocation) | `(std security capability)` | exists; path checks reject symlink escapes and keep create-target compatibility | [security-reference.md](security-reference.md#3-capability-based-security) |
@@ -479,18 +479,14 @@ what a 23-minute adversary counts on users skipping.
   raw bindings with a stderr warning. `make audit` now includes an
   application import-policy gate for `examples/`. `tests/test-safe-prelude.ss`
   passes 28/28.
-- **Do:** (a) `jerboa run file.ss` and the script path default to resolving
-  `(jerboa prelude)` as `(jerboa prelude safe)`; opting into the raw
-  prelude requires an explicit `(jerboa prelude unsafe)` (rename with
-  alias kept) or `--unsafe-prelude` flag, which prints a one-line warning
-  to stderr. (b) Fix the safe prelude import conflict (the "multiple
-  definitions" warning) so symbol resolution order is deterministic and
-  tested — a wrong-symbol resolution in a security wrapper is a real bug,
-  not cosmetics. (c) Extend `(std security import-audit)` so direct
-  `(chezscheme)` imports in application code fail `make audit` outside an
-  allowlisted set of stdlib-internal files. (d) Update `quickstart.md`,
-  `safety-guide.md`, `JERBOA-LANG.md`, `model-coding-guidance.md` and the
-  MCP cookbook to teach safe-prelude-first.
+- **Done:** `jerboa run file.ss` and the script path default to resolving
+  `(jerboa prelude)` as `(jerboa prelude safe)`; opting into the raw prelude
+  requires `(jerboa prelude unsafe)` or `--unsafe-prelude`, which prints a
+  one-line warning to stderr. The safe-prelude import conflict is fixed and
+  tested. `(std security import-audit)` rejects direct `(chezscheme)` imports
+  in application code outside the allowlisted stdlib/tooling surface.
+  `quickstart.md`, `safety-guide.md`, `JERBOA-LANG.md`,
+  `model-coding-guidance.md`, and the MCP cookbook teach safe-prelude-first.
 - **Accept:** done for the app-entry surface: `tests/test-safe-prelude.ss`
   now proves a fresh script using `(jerboa prelude)` cannot reach
   `foreign-procedure`, `system`, `eval`, or `fork-thread`; the escape hatch
@@ -601,15 +597,21 @@ not started."
 not `O_NOFOLLOW` + fd-relative verification after open. A symlink race
 defeats the capability.
 
-- **Where:** `(std security capability)` path enforcement; Rust
-  `secure_fs.rs` already exists — extend it.
-- **Do:** open with `O_NOFOLLOW`/`openat2(RESOLVE_NO_SYMLINKS)` where
-  available, verify the opened fd via `/proc/self/fd/N` against the
-  canonicalized allowed set, then operate on the fd (not the path). macOS/
-  BSD: `open(..., O_NOFOLLOW | O_CLOEXEC)` + `fstat` checks; document the
-  residual race where the platform can't fully close it.
-- **Accept:** race PoC test (symlink swapped between check and open) fails
-  pre-patch, passes post-patch; `capability.md` limitation removed.
+- **Status:** complete for the current capability API. `(std security
+  capability)` opens existing paths with `O_NOFOLLOW` where available, resolves
+  the opened fd via `/proc/self/fd/N` when present, rejects existing symlink
+  escapes, and keeps lexical checks only for missing create-target leaves.
+  `jerboa-native-rs/src/secure_fs.rs` carries symlink replacement race tests,
+  and `tests/security/test-k3-regressions.ss` locks the Scheme capability
+  symlink escape rejection. `capability.md` and `security-reference.md` now
+  describe the implemented boundary.
+- **Evidence:** `(std security capability)` path enforcement is the public
+  Scheme boundary. Rust `secure_fs.rs` provides the fd-relative secure file
+  writer and race-regression coverage for package/build use cases.
+- **Accept:** satisfied for the existing API: symlink escape regressions are
+  tested, and the old `capability.md` limitation has been removed. Future file
+  APIs that perform the actual open should accept a pinned fd or secure-fs
+  handle rather than revalidating a string path later.
 
 ### K3-P1-03 — Taint enforcement on by default in the safe prelude
 **Serves:** G2. **Effort:** 1 week.
@@ -759,10 +761,10 @@ making string-built SQL unrepresentable in safe code.
 ### K3-P1-12 — Eliminate the safe-prelude import conflict
 **Serves:** G1, G4. **Effort:** 2 days. (Folded into P0-06 if done there.)
 
-Tracked separately because "which `sort`/`hash` did I get?" ambiguity in
-security wrappers is a latent vulnerability. Make exports disjoint, add a
-test asserting no duplicate-export warnings at build, and remove the
-"cosmetic" language from `ai-threat.md` once fixed.
+- **Status:** complete; folded into K3-P0-06 on 2026-07-27. The safe-prelude
+  import-conflict cleanup landed with a regression test asserting deterministic
+  export resolution and no duplicate-export warnings. Current docs treat this
+  as a fixed security ambiguity, not a cosmetic warning.
 
 ---
 
@@ -823,6 +825,10 @@ fix must add its scanner rule in the same commit (write it into
 ### K3-P2-06 — Documentation consistency pass
 **Serves:** G5. **Effort:** 2 days.
 
+- **Status:** partially complete. `docs/security-reference.md` is the canonical
+  implemented-security reference; this file is only the roadmap/handoff; review
+  snapshots live under `docs/reviews/`; `docs/index.md` and `SECURITY.md` point
+  readers to that hierarchy.
 - Refresh `ai-threat.md`'s comparative table (supply chain now:
   TUF + signatures + transparency + OSV; update the "Behind" cells and the
   "Known Vulnerabilities" table statuses).
@@ -977,9 +983,9 @@ Security-relevant docs as of 2026-07-27, with their role in this plan:
 | Doc | Role |
 |---|---|
 | [`Philosophy.md`](Philosophy.md) | The *why*: authority-centric doctrine; principles every item above cites |
-| [`ai-threat.md`](ai-threat.md) | Existing AI-adversary threat model; needs table refresh (P2-06) |
-| [`security-reference.md`](security-reference.md) | Feature reference + limitation ledger (§13); add worker/cage/secret/env sections |
-| [`safety-guide.md`](safety-guide.md) | User-facing secure-app guide; update §2 to the real worker API |
+| [`ai-threat.md`](ai-threat.md) | Existing AI-adversary threat model; update only when comparative posture changes |
+| [`security-reference.md`](security-reference.md) | Canonical implemented-security reference + limitation ledger (§13) |
+| [`safety-guide.md`](safety-guide.md) | User-facing secure-app guide; points users at the safe prelude and worker boundary |
 | [`release-security.md`](release-security.md) | Release gates and threat model for the distribution |
 | [`capability.md`](capability.md) | ocap implementation + documented limitations (TOCTOU fixed by P1-02) |
 | [`chez-hardening.md`](chez-hardening.md) | CPU/runtime mitigations; P2-08 and P3-01..03 source |
diff --git a/docs/safety-guide.md b/docs/safety-guide.md
index 8d0cbe6..01776c0 100644
--- a/docs/safety-guide.md
+++ b/docs/safety-guide.md
@@ -54,10 +54,10 @@ state can be left inconsistent and failures are intermittent. The binding is
 retained so existing callers fail closed instead of silently losing isolation.
 
 `run-safe-eval` is a different, narrower API. It evaluates a string against the
-113-binding restricted allowlist in the current process, with a Scheme engine
-timeout and a cap on the formatted result. It does **not** create a process,
-install kernel protections, set a memory limit, or isolate the evaluator from
-the application.
+manifest-checked restricted allowlist in the current process, with a Scheme
+engine timeout and a cap on the formatted result. It does **not** create a
+process, install kernel protections, set a memory limit, or isolate the
+evaluator from the application.
 
 Production code that needs a process boundary should use `(std security
 worker)`. It starts a fresh process through argv execution, uses a pure
@@ -89,7 +89,7 @@ running degraded.
     'capabilities '()
     'max-memory-size #f))
 
-;; Bounded evaluation in the current process (113-binding allowlist):
+;; Bounded evaluation in the current process (manifest-checked allowlist):
 (run-safe-eval
   "(map (lambda (x) (* x x)) '(1 2 3))"
   restricted-eval-config)  ;; => (1 4 9)
@@ -172,9 +172,9 @@ all process controls when selecting in-process restricted evaluation:
 
 ### What It Protects Against
 
-- **Ambient API access**: `run-safe-eval` uses a 113-binding allowlist. `system`,
-  `eval`, `load`, `foreign-procedure` are not in the allowlist. No amount of
-  clever input can summon them.
+- **Ambient API access**: `run-safe-eval` uses a manifest-checked allowlist.
+  `system`, `eval`, `load`, `foreign-procedure` are not in the allowlist. No
+  amount of clever input can summon them.
 - **Runaway Scheme computation**: the engine timeout bounds interruptible
   Scheme evaluation.
 - **Oversized return values**: `max-output-size` caps the formatted result.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 78ab6c7..3e5d4d5 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -232,10 +232,10 @@ Access rights are unforgeable tokens (sealed, opaque records with CSPRNG nonces)
 
 - **Sealed records**: cannot be subtyped or inspected via `record-type-descriptor`
 - **CSPRNG nonces**: each capability carries a unique random nonce from `/dev/urandom`
-- **Path canonicalization**: loads libc through the native-loader policy,
-  resolves existing paths with fd/`realpath(3)` checks, rejects existing
-  symlink escapes, and keeps lexical checks only for missing create-target
-  leaves
+- **Path canonicalization**: loads libc through the native-loader policy, opens
+  existing paths with no-follow fd checks where available, resolves the opened
+  fd back to its actual location, rejects existing symlink escapes, and keeps
+  lexical checks only for missing create-target leaves
 - **Default deny for hosts**: empty host list means no hosts allowed (not all allowed)
 - **Intersection**: `with-capabilities` intersects child capabilities against parent per-permission (ANDs booleans, set-intersects lists)
 - **Thread-safe**: nonce generation is mutex-protected; capability context is a thread parameter
@@ -441,9 +441,10 @@ thunk is never invoked. Arbitrary Scheme execution after raw `fork(2)` is not a
 safe Chez runtime boundary, including in apparently single-threaded programs.
 
 `run-safe-eval` performs bounded restricted evaluation in the calling process.
-It uses the 113-binding allowlist, an interruptible Scheme engine timeout, and a
-formatted-result size cap. It provides no process isolation, kernel sandbox,
-memory limit, stdout/stderr capture, or protection from blocking foreign calls.
+It uses the manifest-checked allowlist, an interruptible Scheme engine timeout,
+and a formatted-result size cap. It provides no process isolation, kernel
+sandbox, memory limit, stdout/stderr capture, or protection from blocking
+foreign calls.
 
 Production confinement requires a bounded, separately exec'd worker. Install
 Landlock/seccomp, Seatbelt, Capsicum, resource limits, and capability policy in
@@ -741,7 +742,10 @@ These are known gaps documented as current limitations, not implementation promi
 - **Landlock requires Linux 5.13+.** No equivalent on macOS, BSDs, or older Linux kernels. `landlock-available?` returns `#f` on unsupported systems.
 - **Taint tracking is opt-in.** Only the `safe-*` wrappers enforce taint checks. Native Chez operations (`open-input-file`, `system`, etc.) do not check taint. No static analysis enforcement exists.
 - **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.
-- **No TOCTOU-safe path checking.** `canonicalize-path` uses `realpath(3)` before access, not `O_NOFOLLOW` + `/proc/self/fd/N` after open.
+- **Filesystem capabilities are not process sandboxes.** They validate paths at
+  the capability API boundary. Code with raw Chez file primitives or
+  unauthorized imports can bypass them unless the safe prelude, import-audit
+  gate, and OS worker confinement are also in force.
 - **`define-syntax` remains in the sandbox allowlist.** Macro definition in sandboxed code is possible. Whether this is a risk depends on the use case.
 - **Restricted-eval output is not captured.** `max-output-size` bounds only the formatted return value. It is not a stdout/stderr capture system.
 - **No Argon2id.** Password hashing uses PBKDF2 (via OpenSSL or Rust ring) rather than Argon2id (requires separate library).