docs: maximum-hardening plan for vendored Chez Scheme

ober

fc9dbd22a1a2033b927fc0d7aa88637207cf34b9

diff --git a/docs/chez-hardening.md b/docs/chez-hardening.md
new file mode 100644
index 0000000..dc2fa60
--- /dev/null
+++ b/docs/chez-hardening.md
@@ -0,0 +1,498 @@
+# Maximum-Hardening Plan for the Vendored Chez Scheme
+
+Proposal for landing every CPU-exposed mitigation that applies to the
+vendored Chez runtime (`vendor/ChezScheme/`). This is the missing
+piece of `secure.md` §"Path 1: Harden the Chez Binary" — most of the
+flags that secure.md says are "DONE" are applied to Jerboa's own C
+shim and to the final link line, but **not** to `libkernel.a` itself,
+which is the ~1 MB block of native code that contributes nearly all
+ROP gadgets in the binary.
+
+This document is the engineering plan to close that gap, broken into
+phases ranked by effort, with the hazards (notably `call/cc` ×
+shadow-stack interaction) called out as research items rather than
+buried.
+
+---
+
+## 1. Scope
+
+In scope:
+- `vendor/ChezScheme/c/*.c` — the kernel (`gc.c`, `scheme.c`, `fasl.c`,
+  `prim5.c`, etc.). Compiled into `libkernel.a`.
+- `vendor/ChezScheme/s/x86_64.ss` and `s/arm64.ss` — the JIT
+  back-ends that emit native function prologues at runtime.
+- The Jerboa build wiring that drives Chez's `./configure`
+  (`Makefile`, `Dockerfile`, cross-build paths).
+
+Out of scope:
+- Jerboa's C shim and link line. Already hardened via
+  `lib/jerboa/build/musl.sls` and `jerboa-native-rs/.cargo/config.toml`
+  (per `secure.md` §"Standard Hardening CFLAGS").
+- Application-level mitigations (anti-debug, seccomp, Landlock,
+  encrypted boot files). Covered by `docs/harden.md`.
+- WASM-sandboxed parsers. Separate concern, covered by §"Path 2" of
+  `secure.md`.
+
+---
+
+## 2. Current State
+
+The Chez fork at `vendor/ChezScheme/` **already ships the patches** to
+make hardening work end-to-end. See `docs/chez-fork.md` §"Security
+hardening":
+
+| Fork commit | What it adds                                                |
+|-------------|-------------------------------------------------------------|
+| `829bc806`  | ENDBR64 emission at JIT function entries (Intel CET/IBT)    |
+| `beefa2a6`  | `--enable-harden` configure flag + ARM64 BTI landing pads   |
+| `b3f35405`  | FASL deserialization / FFI hardening                        |
+| `aaa82190`  | vfasl bounds checks + `path_append` hardening               |
+| `c3c3d6f3`  | `--static` + `--foreign-libs` for hermetic builds           |
+
+What `--enable-harden` does today (verified in
+`vendor/ChezScheme/configure` lines 899–924):
+
+```sh
+CFLAGS  += -fstack-protector-strong -fstack-clash-protection
+CFLAGS  += -D_FORTIFY_SOURCE=2
+case arch in
+  a6le)    CFLAGS += -fcf-protection=full            # CET (SHSTK + IBT)
+  a6*)     # CET unavailable on non-Linux x86_64
+  arm64*)  CFLAGS += -mbranch-protection=standard    # BTI + PAC-ret
+esac
+LDFLAGS += -Wl,-z,relro,-z,now
+# if not --static: CFLAGS += -fPIC ; mdlinkflags += -pie
+```
+
+**The gap is in Jerboa, not Chez:**
+
+| Build path                         | Passes `--enable-harden`? |
+|------------------------------------|:-------------------------:|
+| `Makefile` `$(CHEZ_INSTALL_FLAGS)` | ✗                         |
+| `Makefile` `chez-cross` target     | ✗                         |
+| `Dockerfile` (musl pipeline)       | ✗                         |
+
+Net effect today: `libkernel.a` is built with `-O2` only. Every
+hardening claim in `secure.md` about Chez is technically aspirational.
+
+---
+
+## 3. CPU Protection Inventory
+
+Mitigations grouped by what the hardware can enforce, with current
+status and which Chez subsystem they touch.
+
+### 3.1 x86_64 (Intel + AMD)
+
+| Feature           | Hardware                | Flag                       | Chez-side work       | Status                        |
+|-------------------|-------------------------|----------------------------|----------------------|-------------------------------|
+| CET SHSTK         | Intel 11th gen, Zen 4+  | `-fcf-protection=return`   | None for C; **call/cc hazard** | Compile-side ready; runtime untested |
+| CET IBT           | Intel 11th gen, Zen 4+  | `-fcf-protection=branch`   | JIT ENDBR64 done (`829bc806`)  | Ready                                 |
+| Full CET          | as above                | `-fcf-protection=full`     | combined                       | Ready (in `--enable-harden`)          |
+| Stack canaries    | universal               | `-fstack-protector-strong` | None                           | In `--enable-harden`                  |
+| Stack-clash probe | universal               | `-fstack-clash-protection` | None                           | In `--enable-harden`                  |
+| FORTIFY_SOURCE    | universal (libc)        | `-D_FORTIFY_SOURCE=2`      | None                           | In `--enable-harden`                  |
+| Register clearing | universal               | `-fzero-call-used-regs=…`  | None                           | **Not in `--enable-harden`** (Phase 2)|
+| Auto-var-init     | universal               | `-ftrivial-auto-var-init=zero` | None                       | **Not in `--enable-harden`** (Phase 2)|
+| Full RELRO + Now  | universal               | `-Wl,-z,relro,-z,now`      | None                           | In `--enable-harden`                  |
+| PIE + ASLR        | universal               | `-fPIE -pie`               | None                           | In `--enable-harden` (non-static)     |
+| MPK / PKU         | Intel Skylake+, Zen 3+  | runtime `pkey_*` syscalls  | JIT page management            | **Not started** (Phase 4)             |
+| LAM               | Intel Sapphire Rapids+  | runtime                    | sanitizer-style, niche         | Out of scope                          |
+
+### 3.2 aarch64 (ARM)
+
+| Feature      | Hardware              | Flag                              | Chez-side work        | Status                              |
+|--------------|-----------------------|-----------------------------------|-----------------------|-------------------------------------|
+| PAC-ret      | ARMv8.3 (Apple M1+, Graviton 2+) | `-mbranch-protection=pac-ret` | None for C; **call/cc hazard** | In `--enable-harden` via `=standard` |
+| BTI          | ARMv8.5 (Graviton 3+, M-series) | `-mbranch-protection=bti`     | JIT landing pads done (`beefa2a6`) | In `--enable-harden` via `=standard` |
+| GCS          | ARMv9.4 (very new)    | `-mbranch-protection=standard+gcs`| JIT + call/cc | **Not in `--enable-harden`** (Phase 4)|
+| MTE          | ARMv8.5 + memtag      | `-march=…+memtag`, runtime opt-in | GC integration        | **Not started** (Phase 4)             |
+| Stack canaries / FORTIFY / RELRO / PIE | universal | (same as x86_64)            | None                  | In `--enable-harden`                  |
+
+### 3.3 Other Architectures
+
+`ppc32`, `riscv64`, `loongarch64`, `pb` (portable bytecode) — the
+universal flags (canaries, FORTIFY, RELRO, PIE) work; architecture-
+specific CFI does not. `--enable-harden` will fall through the case
+statement and apply only the cross-arch flags. No changes needed.
+
+---
+
+## 4. Phase 1 — Wire Up `--enable-harden`
+
+**Effort:** ~1 hour. **Risk:** very low. **Impact:** unlocks the
+already-shipped hardening end-to-end.
+
+### 4.1 Changes
+
+Three call sites pass flags to `./configure`. All three need
+`--enable-harden`:
+
+**`Makefile` (native host build), `CHEZ_INSTALL_FLAGS`** (~line 12):
+add `--enable-harden` to the list.
+
+**`Makefile` (cross build), `CROSS_CHEZ_CONFIGURE_FLAGS`** (~line 154):
+append `--enable-harden`. CET is x86_64-Linux-only; the configure
+case statement gates it automatically per `flagsmuni`, so this is
+safe for all targets including `tarm64le`, `ta6osx`, `ta6fb`.
+
+**`Dockerfile`** (~lines 71, 84, 89): the three `./configure`
+invocations in the Chez build stages all need `--enable-harden`. The
+musl-static stage (line 89) is the highest-value one — that's the
+production binary.
+
+### 4.2 Verification
+
+After landing, the workarea Makefile should show the flags:
+
+```bash
+$ grep -E '^(C|LD)FLAGS' build/chez/ta6le/Mf-config
+CFLAGS = -O2 -fstack-protector-strong -fstack-clash-protection \
+         -D_FORTIFY_SOURCE=2 -fcf-protection=full -fPIC
+LDFLAGS = -Wl,-z,relro,-z,now
+```
+
+(For static builds, `-fPIC` drops out and `-pie` is not added — see
+Phase 2.2 for the static-PIE path.)
+
+Confirm CET landing pads at JIT entries:
+
+```bash
+$ objdump -d .chez/bin/scheme | grep -c 'endbr64'
+# expect: hundreds (JIT-emitted) on x86_64
+```
+
+### 4.3 Regression Surface
+
+Run the Chez upstream test suite (in `vendor/ChezScheme/`):
+
+```bash
+cd build/chez && make test-some-fast    # ~5 min, representative
+cd build/chez && make test              # ~1 hour, full
+```
+
+Then Jerboa's own suite:
+
+```bash
+make test
+```
+
+The call/cc-heavy mats — `mats/cont.ms`, `mats/misc.ms`,
+`mats/thread.ms` — are the ones to watch on CET-capable CPUs (see
+Phase 3 below). On a non-CET CPU the SHSTK bits are no-ops and only
+the canary/FORTIFY paths exercise.
+
+---
+
+## 5. Phase 2 — Extra CFLAGS Not Yet in `--enable-harden`
+
+**Effort:** ~1 day (mostly testing). **Risk:** low.
+
+`--enable-harden` covers the standard mitigations but omits four
+worthwhile ones:
+
+### 5.1 Register Clearing — `-fzero-call-used-regs=used-gpr`
+
+Clears caller-saved registers on function return. Defeats
+data-oriented attacks that read stale register values across the
+ABI boundary and reduces gadget utility (a gadget that depends on a
+register the previous frame zeroed out is dead). GCC 11+, Clang 15+.
+
+Cost: small (a few `xor` instructions per return). No correctness
+impact. The `=used-gpr` variant is the cheap one — only the GPRs
+actually clobbered by the function get zeroed.
+
+### 5.2 Stack Auto-Initialization — `-ftrivial-auto-var-init=zero`
+
+Initializes uninitialized stack variables to zero. Defeats
+uninitialized-read information leaks. GCC 12+, Clang 8+.
+
+Hazard: changes behaviour if any code in `c/` relies on uninitialized
+reads (which would be a bug). Run the Chez test suite to verify.
+
+### 5.3 Format-String Hardening — `-Wformat -Wformat-security -Werror=format-security`
+
+Compile-time error on tainted format strings. Catches a small class
+of bugs that don't exist in audited code but are cheap to enforce.
+
+### 5.4 Static-PIE Compatibility
+
+The current `--enable-harden` adds `-pie` only when not `--static`.
+The Jerboa Docker build uses `--static`, so libkernel ends up with no
+PIE flag — but the final Jerboa binary is `-static-pie` (per
+`secure.md` §"Static PIE"). For static-PIE to actually randomize
+libkernel's address, libkernel's object files need `-fPIE` (not
+`-fPIC`, but `-fPIE` is what static-PIE wants).
+
+Add to `configure`: when `--static` AND `--enable-harden`, emit
+`-fPIE` without `-pie`. The `-pie` belongs on the final link, not on
+intermediate `.o` files.
+
+### 5.5 Proposed `--enable-harden` Patch (Fork-Side)
+
+These flags should be added to `vendor/ChezScheme/configure` under
+the `if [ "$enableharden" = "yes" ]` block. This becomes commit N+1
+in the fork's "Security hardening" theme.
+
+```sh
+CFLAGS="${CFLAGS} -fzero-call-used-regs=used-gpr"
+CFLAGS="${CFLAGS} -ftrivial-auto-var-init=zero"
+CFLAGS="${CFLAGS} -Wformat -Wformat-security -Werror=format-security"
+if [ "$staticbuild" = "yes" ] ; then
+    CFLAGS="${CFLAGS} -fPIE"
+fi
+```
+
+Compiler-version guards: `-fzero-call-used-regs` and
+`-ftrivial-auto-var-init` are GCC 11+/12+ respectively. The
+configure script should detect compiler version and skip on older
+toolchains rather than fail. (musl-cross-make ships GCC 11+ for all
+supported architectures, so this is non-issue for the production
+pipeline.)
+
+---
+
+## 6. Phase 3 — Runtime Enablement and the `call/cc` Hazard
+
+**Effort:** unknown (research). **Risk:** medium to high.
+
+Compile-time hardening flags are necessary but not sufficient. CET
+SHSTK and ARM PAC require **runtime activation** on Linux to actually
+enforce protection — and Chez's first-class continuations make
+shadow-stack management non-trivial.
+
+### 6.1 The Compile/Runtime Gap
+
+Compile-time:
+- `-fcf-protection=return` compiles every function epilogue to emit
+  the SHSTK-aware `ret` (which the CPU silently no-ops if SHSTK is
+  not active).
+- `-mbranch-protection=pac-ret` compiles function prologues/epilogues
+  to issue `paciasp` / `autiasp` (which the CPU silently no-ops if
+  PAC is not active).
+
+Runtime:
+- SHSTK must be enabled per-thread via
+  `arch_prctl(ARCH_SHSTK_ENABLE)` on Linux 6.6+.
+- PAC is enabled by default in userspace once compiled in.
+- Both need to survive thread creation, `fork`, `exec`, and — for
+  Chez specifically — `call/cc` invocation.
+
+### 6.2 Why `call/cc` Breaks Shadow-Stack-Style Mitigations
+
+Chez's first-class continuations capture and restore control flow at
+arbitrary points in the C stack. SHSTK assumes a strict LIFO discipline
+on return addresses; a continuation invocation can:
+
+- **Long-jump backward** (re-invoke a captured continuation from
+  shallower stack): need to *push* shadow-stack entries to match the
+  restored real stack.
+- **Long-jump forward** (multi-shot continuation captured deeper,
+  invoked from shallower): need to *unwind* shadow stack via
+  `INCSSP` to match.
+- **Cross-thread continuation invocation**: each thread has its own
+  shadow stack; transferring control across threads requires
+  `RSTORSSP` plus a saved-shadow-stack-pointer in the continuation
+  object.
+
+ARM PAC has an equivalent issue: return addresses are signed with a
+key tied to the current frame's `SP`. A captured continuation
+restored to a different `SP` produces a signature mismatch and the
+authenticating `ret` faults.
+
+Chez implements continuations via a C kernel routine
+(`continuation` in `c/scheme.c`, plus stack-segment underflow
+handlers in `c/gc.c`). The kernel maintains its own copy of saved
+stack segments. **The shadow stack is not in that picture today.**
+
+### 6.3 Proposal: Runtime Detection + Opt-In Enablement
+
+Don't enable SHSTK by default in the Chez kernel. Instead:
+
+1. **Compile with the flags on** (Phase 1 — no runtime cost on CPUs
+   without the feature).
+2. **Detect at runtime** whether the CPU and kernel support SHSTK
+   (`getauxval(AT_HWCAP2) & HWCAP2_SHSTK` on x86_64,
+   `HWCAP_PACA` / `HWCAP2_BTI` / `HWCAP2_GCS` on aarch64).
+3. **Provide an explicit enable API** in `(std os hardening)` —
+   something like `(enable-shadow-stack!)` — that calls
+   `arch_prctl(ARCH_SHSTK_ENABLE)` and is **only safe to call before
+   any `call/cc` ever runs**. Document this constraint loudly.
+4. **For programs that never use `call/cc`** (the common case for
+   security-critical tools — DNS servers, parsers, etc.) this is
+   trivially safe: enable in `main` before any Scheme code runs.
+5. **For programs that do use `call/cc`** — punt until Phase 3.4.
+
+### 6.4 Future Work: Continuation-Aware Shadow-Stack Management
+
+The principled fix is to extend the Chez kernel's continuation
+machinery to track shadow-stack state alongside the saved stack
+segments. Sketch:
+
+- Each continuation object grows a `shadow_stack_top` field.
+- The underflow handler in `c/gc.c` saves the current SSP (read via
+  `rdsspq` instruction) when copying out a stack segment.
+- The continuation invocation path in `c/scheme.c` restores SSP via
+  `rstorssp` and rebuilds the shadow stack to match the restored
+  real stack.
+
+This is a fork-side patch, weeks of work, and needs validation
+against `mats/cont.ms` running with SHSTK actually enabled — which
+requires CET hardware on the CI runner. **Defer to Phase 3 as
+research; not blocking Phase 1.**
+
+The same shape of work applies to ARM GCS in Phase 4.
+
+### 6.5 Honest Status
+
+The single sentence: **the compile-time flags are free; the runtime
+enablement is bounded by whether the program uses `call/cc`.** For
+the Jerboa tools that don't (security-critical parsers, DNS, anything
+shipped as a static binary daemon), SHSTK can be turned on at
+startup. For the REPL, the WPO build entry, anything that exercises
+`call/cc`, the kernel patch in 6.4 is a prerequisite.
+
+---
+
+## 7. Phase 4 — Frontier CPU Features
+
+**Effort:** weeks to months per item. **Risk:** medium. **Timeline:**
+12+ months out; tracked here so they're not invented twice.
+
+### 7.1 ARM Memory Tagging Extension (MTE)
+
+ARMv8.5 + memtag. Catches use-after-free, double-free, linear buffer
+overflow at hardware speed. Requires:
+
+- `-march=armv8.5-a+memtag` on the Chez build.
+- GC integration: every allocation tagged on creation, tag invalidated
+  on free. Pointers carry a 4-bit tag in the top byte; loads/stores
+  check tag against allocation tag.
+- Glibc 2.34+ provides `mallopt(M_TAG_MEMORY, …)` — but Chez has its
+  own GC, so this is bespoke work in `c/gc.c` and `c/alloc.c`.
+
+Hardware availability: Pixel 8+, some Samsung Exynos, server
+chips (Neoverse N3/V3). Not yet on Graviton or Apple Silicon.
+
+### 7.2 ARM Guarded Control Stack (GCS)
+
+ARMv9.4. The ARM equivalent of SHSTK. Same `call/cc` hazard as 6.2;
+same kernel work as 6.4 needed. Flag is
+`-mbranch-protection=standard+gcs`.
+
+Hardware availability: roadmapped, not yet shipping in
+deployment-relevant targets.
+
+### 7.3 Intel Memory Protection Keys (MPK / PKU)
+
+Skylake+. Allows fine-grained W^X within a single process via 16
+protection keys assignable per-page. Use case for Chez: JIT pages
+get a dedicated key; non-JIT code paths drop the write key before
+calling into Scheme, preventing arbitrary writes to JIT memory from
+within compiled code.
+
+This is the most impactful x86_64 item past Phase 1 because it
+narrows the W^X window structurally rather than just at the mmap
+boundary. But the work is non-trivial: every JIT codegen path in
+`s/x86_64.ss` that emits writable pages needs the PKEY dance.
+
+### 7.4 Intel TDX / AMD SEV-SNP
+
+Confidential computing. Out of scope for the language itself; a
+deployment concern. Mention only to note that hardening Chez for
+deployment inside a confidential VM is the same Chez binary — no
+language-level work needed.
+
+---
+
+## 8. Testing Strategy
+
+### 8.1 CPU Coverage Matrix
+
+| CPU                       | SHSTK | IBT | PAC | BTI | GCS | MTE | Notes              |
+|---------------------------|:-----:|:---:|:---:|:---:|:---:|:---:|--------------------|
+| Intel 11th gen+ (Tiger Lake) | ✓  | ✓   |     |     |     |     | CI: GitHub runners |
+| Intel 10th gen and older  |       |     |     |     |     |     | Compile-only check |
+| AMD Zen 4+                | ✓     | ✓   |     |     |     |     | Underrepresented in CI |
+| Apple Silicon M1+         |       |     | ✓   |     |     |     | macOS, dev machines |
+| Apple Silicon M3+         |       |     | ✓   | ✓   |     |     |                    |
+| AWS Graviton 3+           |       |     | ✓   | ✓   |     |     |                    |
+| AWS Graviton 4 / Neoverse V2 |    |     | ✓   | ✓   |     |     |                    |
+
+At least one CI runner per row of "✓" is necessary to actually
+validate the mitigation. Compile-only test on the rest.
+
+### 8.2 Regression Tests
+
+Phase 1 land must pass:
+- `vendor/ChezScheme/`: `make test` (full upstream suite, ~1 hour).
+- Jerboa: `make test`.
+- The `tests/test-restrict-hardened.ss` script the Makefile already
+  references (line 549) — extend it to exercise SHSTK-on paths once
+  Phase 3.3 lands.
+
+Phase 3 land must additionally pass:
+- `mats/cont.ms` and `mats/thread.ms` with SHSTK actively enabled
+  (requires CET-capable runner; bare-metal or properly-passthrough'd
+  VM, not all cloud KVM exposes CET to guests).
+
+### 8.3 Build-Time Sanity Checks
+
+Add a `make check-hardening` target that greps the workarea Makefile
+for the expected flags and `objdump`s the built `scheme` binary for
+ENDBR64 / BTI / canary presence. Fails CI if hardening silently
+regressed (e.g., someone added a third `./configure` call without
+`--enable-harden`).
+
+---
+
+## 9. Convergence Order
+
+Recommended sequence to land:
+
+1. **Phase 1** (1 hour) — wire `--enable-harden` into the three call
+   sites. Unlocks every already-shipped mitigation. Lowest-risk,
+   highest-impact single change.
+2. **Phase 2.4** (1 hour) — fix `-fPIE` on `--static` builds so
+   libkernel actually participates in static-PIE ASLR.
+3. **Phase 2.1–2.3** (1 day) — extra CFLAGS as a fork-side patch
+   to `--enable-harden`.
+4. **Phase 3.3** (days) — runtime SHSTK enablement for
+   non-`call/cc` programs.
+5. **Phase 3.4** (weeks) — kernel continuation work to make
+   SHSTK / PAC safe under `call/cc`.
+6. **Phase 4** — MTE, MPK, GCS as separate proposals.
+
+Phases 1–2 alone bring the libkernel hardening level up to par
+with the Jerboa shim, closing the gap `secure.md` describes as
+"the single highest-impact change remaining."
+
+---
+
+## 10. Cross-References
+
+- `secure.md` — overall security strategy; this doc is the
+  Path-1 detail it currently lacks.
+- `docs/Future-proofing.md` §3.8 — strategic argument for shrinking
+  the Chez TCB; this doc is the *defensive* answer (harden what's
+  there) complementing the *offensive* answer (replace it with s7 or
+  shift to WASM) the doc considers.
+- `docs/chez-fork.md` — narrative on what the fork has already
+  added; Phase 1 here exists because of patches `829bc806` and
+  `beefa2a6`.
+- `docs/harden.md` — application-binary hardening (separate layer).
+- `vendor/ChezScheme/configure` lines 899–924 — the canonical source
+  of truth for what `--enable-harden` does today.
+
+---
+
+## 11. Maintenance
+
+When CPU vendors ship new mitigations (next Intel/AMD generation,
+ARMv9.5+, RISC-V Zicfilp/Zicfiss), add a row to §3 and a phase entry
+to §7. When `vendor/ChezScheme/`'s `--enable-harden` is extended in
+the fork, shrink §5 correspondingly. The pattern from
+`Future-proofing.md` §9 applies here: drift between this document
+and the configure script is itself a smell — fix the script or fix
+the doc.