docs: maximum-hardening plan for vendored Chez Scheme
ober
fc9dbd22a1a2033b927fc0d7aa88637207cf34b9
new file mode 100644 --- /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.