Update core hardening safety docs
ober
3c49c0920d98ba749e874866ee097bb04fdb43cc
--- a/docs/chez-hardening.md +++ b/docs/chez-hardening.md @@ -95,10 +95,10 @@ status and which Chez subsystem they touch. | 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)| +| Register clearing | universal | `-fzero-call-used-regs=…` | None | In `--enable-harden` when compiler accepts it | +| Auto-var-init | universal | `-ftrivial-auto-var-init=zero` | None | In `--enable-harden` when compiler accepts it | | Full RELRO + Now | universal | `-Wl,-z,relro,-z,now` | None | In `--enable-harden` | -| PIE + ASLR | universal | `-fPIE -pie` | None | In `--enable-harden` (non-static) | +| PIE + ASLR | universal | `-fPIE -pie` | None | In `--enable-harden` (`-fPIC`/`-pie` non-static, probed `-fPIE` 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 | @@ -187,12 +187,13 @@ the canary/FORTIFY paths exercise. --- -## 5. Phase 2 — Extra CFLAGS Not Yet in `--enable-harden` +## 5. Phase 2 — Extra CFLAGS in `--enable-harden` -**Effort:** ~1 day (mostly testing). **Risk:** low. +**Status:** implemented in `vendor/ChezScheme/configure`. **Risk:** low. -`--enable-harden` covers the standard mitigations but omits four -worthwhile ones: +`--enable-harden` covers the standard mitigations and now probes four +additional flags. Unsupported flags are skipped rather than breaking older +toolchains or cross compilers. ### 5.1 Register Clearing — `-fzero-call-used-regs=used-gpr` @@ -220,38 +221,27 @@ 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). +`--enable-harden` adds `-pie` only when not `--static`. For static builds it now +probes and adds `-fPIE` to libkernel object compilation without adding +executable-only `-pie` to intermediate link flags. The final Jerboa binary link +remains responsible for static-PIE. -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 Configure Implementation -### 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. +`vendor/ChezScheme/configure` uses a small compile-only probe before appending +each optional flag: ```sh -CFLAGS="${CFLAGS} -fzero-call-used-regs=used-gpr" -CFLAGS="${CFLAGS} -ftrivial-auto-var-init=zero" -CFLAGS="${CFLAGS} -Wformat -Wformat-security -Werror=format-security" +try_harden_cflags "-fzero-call-used-regs=used-gpr" +try_harden_cflags "-ftrivial-auto-var-init=zero" +try_harden_cflags "-Wformat -Wformat-security -Werror=format-security" if [ "$staticbuild" = "yes" ] ; then - CFLAGS="${CFLAGS} -fPIE" + try_harden_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.) +Compiler-version guards are therefore capability probes rather than brittle +version parsing. --- @@ -476,18 +466,15 @@ regressed (e.g., someone added a third `./configure` call without 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** (done for generated binaries) — runtime SHSTK enablement for +1. **Phase 1** (done) — `--enable-harden` is wired into native, cross, Docker, + and musl helper build paths. +2. **Phase 2.1–2.4** (done) — extra compiler-probed CFLAGS and static `-fPIE` + are folded into the vendored Chez configure path. +3. **Phase 3.3** (done for generated binaries) — runtime SHSTK enablement for non-`call/cc` programs. -5. **Phase 3.4** (weeks) — kernel continuation work to make +4. **Phase 3.4** (weeks) — kernel continuation work to make SHSTK / PAC safe under `call/cc`. -6. **Phase 4** — MTE, MPK, GCS as separate proposals. +5. **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 --- a/docs/safety-guide.md +++ b/docs/safety-guide.md @@ -52,6 +52,7 @@ protections, runs your code, and returns the result to the parent. ;; - 30 second timeout ;; - compute-only seccomp filter (blocks network, filesystem writes) ;; - No Landlock rules (add your own for filesystem restriction) +;; - No memory cap unless max-memory-size is configured (run-safe (lambda () (+ 1 2))) ;; => 3 ;; Evaluate a string in a restricted environment (113-binding allowlist): @@ -65,6 +66,7 @@ protections, runs your code, and returns the result to the parent. (run-safe (lambda () (process-data input)) (make-sandbox-config 'timeout 10 + 'max-memory-size (* 256 1024 1024) 'seccomp 'io-only)) ;; Filesystem sandbox: read-only access to specific directories: @@ -87,6 +89,8 @@ protections, runs your code, and returns the result to the parent. | `seccomp` | `'compute-only`, `'io-only`, `'network-server`, filter, `#f` | `'compute-only` | Syscall filter (irreversible) | | `landlock` | ruleset or `#f` | `#f` | Filesystem access restriction (irreversible) | | `capabilities` | list or `'()` | `'()` | Runtime capability tokens | +| `max-output-size` | bytes | 1 MiB | Max child result bytes read from the pipe | +| `max-memory-size` | bytes or `#f` | `#f` | Optional child address-space cap via `setrlimit` | ### Default Parameters @@ -108,20 +112,21 @@ You can set global defaults instead of passing config every time: - **Syscall abuse**: Seccomp BPF filters are kernel-enforced. Even arbitrary code execution inside the sandbox cannot bypass them. - **Infinite loops**: Engine-based timeout preempts runaway Scheme code. +- **Memory exhaustion**: `max-memory-size` installs a child-process memory cap + before the untrusted thunk runs. - **Parent compromise**: All protections are applied in a forked child. The parent process remains unrestricted. ### What It Does NOT Protect Against -- **Memory exhaustion**: A `(make-bytevector 1000000000)` inside the sandbox - can OOM the child before the timeout fires. There is no `setrlimit` yet. - For DoS protection, run sandboxed code in a resource-limited container. +- **Default memory exhaustion**: `max-memory-size` is opt-in because platforms + disagree on address-space limits. Set it explicitly for untrusted work, and + use cgroups or containers when you need process-tree limits. - **Blocked FFI calls**: The engine timeout preempts Scheme code but cannot interrupt a blocking C/Rust FFI call. Use socket-level timeouts (`SO_RCVTIMEO`) for network operations. -- **Temp file race**: The child communicates results via `/tmp/jerboa-sandbox-*` - with a random suffix. A local attacker with `/tmp` access could potentially - interfere. This is a known issue. +- **Stdout/stderr capture**: `max-output-size` caps the result channel read + from the child pipe. It is not a general stdout/stderr capture mechanism. --- @@ -711,12 +716,13 @@ to catch common issues before they become bugs. Be honest about what Jerboa does NOT protect against: -### No Memory Limits in Sandbox +### Memory Limits Are Opt-In -`run-safe` does not set `setrlimit(RLIMIT_AS)` in the child process. A -malicious thunk can exhaust memory before the timeout fires. If you run -untrusted code, use OS-level memory limits (cgroups, containers) as a -complementary measure. +`run-safe` supports `max-memory-size`, which installs `setrlimit(RLIMIT_AS)` in +the child process. The default remains `#f` because Chez heap layout and kernel +limit semantics vary by platform. If you run untrusted code, set +`max-memory-size` and use OS-level memory limits (cgroups, containers) when you +need process-tree enforcement. ### Kernel Protections Are Linux x86_64 Only @@ -725,11 +731,11 @@ numbers. On other platforms (macOS, ARM Linux, BSDs), these protections silently degrade — the code runs without kernel sandboxing. The Scheme-level allowlist and timeout still apply. -### Temp File Race in `run-safe` +### `run-safe` Result Channel -`run-safe` communicates results via `/tmp/jerboa-sandbox-<random>`. A local -attacker with `/tmp` access could create symlinks to redirect the result -file. Current implementations should avoid this path for adversarial local multi-user contexts until it uses `mkstemp` or `O_TMPFILE`. +`run-safe` communicates results through `pipe(2)`, not a predictable temp file. +The old `/tmp/jerboa-sandbox-*` symlink/TOCTOU concern is no longer part of the +current implementation. ### Engine Timeout Cannot Interrupt FFI @@ -769,11 +775,12 @@ produce a "multiple definitions" warning. This is cosmetic but means symbol resolution order could theoretically surprise you in edge cases. In practice, the safe version always wins because it is bound last. -### Build Reproducibility Is Partial +### Reproducibility Evidence Is Local -`(std build reproducible)` provides content-addressed artifacts and SHA-256 -hashing, but bit-identical reproducible builds end-to-end are still being verified. The SBOM detects Scheme, C, and Rust dependencies but full build -orchestration is not complete. +`make reproducibility-report` performs a local two-build comparison of the +multicall binary, object cache, bundle, links, and source manifest, and exits +nonzero on mismatch. Cross-machine reproducibility still requires comparing +those reports from independent builders. ---