Add binary hardening modules: antidebug, seccomp, integrity

ober

26766fc8260e205c4f72cba4f86191d3c5648c62

diff --git a/docs/harden-usage.md b/docs/harden-usage.md
new file mode 100644
index 0000000..4297e1f
--- /dev/null
+++ b/docs/harden-usage.md
@@ -0,0 +1,527 @@
+# Hardening API Usage Guide
+
+Practical guide for using Jerboa's binary hardening modules from consumer projects like jerboa-shell. Covers the three new libraries — `(std os antidebug)`, `(std os seccomp)`, `(std os integrity)` — plus integration with the existing `(std os landlock-native)` and `(std crypto secure-mem)`.
+
+All functions are backed by `libjerboa_native.so` (Rust/ring/libc). No OpenSSL dependency.
+
+---
+
+## Prerequisites
+
+The consuming project needs `libjerboa_native.so` accessible at runtime. Options:
+
+```bash
+# Option 1: Copy to your project's lib/ directory
+cp ~/mine/jerboa/lib/libjerboa_native.so ~/mine/jerboa-shell/lib/
+
+# Option 2: Build it fresh
+cd ~/mine/jerboa/jerboa-native-rs && cargo build --release
+cp target/release/libjerboa_native.so ~/mine/jerboa-shell/lib/
+
+# Option 3: For static musl builds, link libjerboa_native.a
+cd ~/mine/jerboa/jerboa-native-rs && cargo build --release
+# produces target/release/libjerboa_native.a
+```
+
+The Scheme wrappers search for the library in this order:
+1. `libjerboa_native.so` (system library path / `LD_LIBRARY_PATH`)
+2. `lib/libjerboa_native.so` (relative to working directory)
+3. `./lib/libjerboa_native.so`
+
+For static binaries (musl builds), symbols are pre-registered via `Sforeign_symbol` in the C main — no runtime loading needed.
+
+---
+
+## Quick Start: Minimal Hardening
+
+Add this to your program's startup (e.g., in `main.sls` or the entry point script):
+
+```scheme
+(import (std os antidebug)
+        (std os seccomp)
+        (std os integrity))
+
+;; 1. Block debugger attachment (one-shot, irreversible)
+(guard (e [#t (void)])  ; tolerate failure in dev mode
+  (antidebug-ptrace!))
+
+;; 2. Check for existing tracers
+(when (antidebug-traced?)
+  (display "integrity violation\n" (current-error-port))
+  (exit 1))
+
+;; 3. Check for library injection
+(when (antidebug-ld-preload?)
+  (display "integrity violation\n" (current-error-port))
+  (exit 1))
+
+;; 4. Kernel-block debug syscalls (irreversible)
+(when (seccomp-available?)
+  (seccomp-lock!))
+```
+
+---
+
+## Module Reference
+
+### (std os antidebug)
+
+#### antidebug-ptrace!
+
+```scheme
+(antidebug-ptrace!) → void
+```
+
+Calls `PTRACE_TRACEME` on the current process. If successful, no debugger can attach afterward. Raises `&antidebug-error` if a debugger is already attached.
+
+**Irreversible.** Calling twice always fails the second time (process is already self-traced). Wrap in `guard` if you want to tolerate failure in development:
+
+```scheme
+(guard (e [(antidebug-error? e) (void)])
+  (antidebug-ptrace!))
+```
+
+#### antidebug-traced?
+
+```scheme
+(antidebug-traced?) → boolean
+```
+
+Reads `/proc/self/status` and checks `TracerPid`. Returns `#t` if a debugger/tracer is attached, `#f` if clean. Raises on error (e.g., procfs unavailable).
+
+#### antidebug-ld-preload?
+
+```scheme
+(antidebug-ld-preload?) → boolean
+```
+
+Checks both the current environment and `/proc/self/environ` for `LD_PRELOAD`. The `/proc/self/environ` check catches cases where an attacker set `LD_PRELOAD` at exec time then cleared it from the process environment.
+
+Returns `#t` if `LD_PRELOAD` is set to a non-empty value, `#f` if clean.
+
+#### antidebug-breakpoint?
+
+```scheme
+(antidebug-breakpoint? addr) → boolean
+```
+
+Checks if the byte at `addr` (a `uptr`, unsigned pointer) is `0xCC` (INT3 software breakpoint). Useful for verifying that key function entry points haven't been patched by a debugger.
+
+**Warning:** `addr` must point to readable memory in the process's `.text` section. Passing an invalid address will crash.
+
+To get a function's address in Chez Scheme:
+
+```scheme
+;; Use foreign-callable or inspect compiled code object addresses
+;; Most useful in the C main, where you can take &function_name
+```
+
+#### antidebug-timing-anomaly?
+
+```scheme
+(antidebug-timing-anomaly? max-ns) → boolean
+```
+
+Runs a calibration loop and measures elapsed time. If it takes longer than `max-ns` nanoseconds, returns `#t` (indicating single-stepping in a debugger). Recommended threshold: `50000000` (50ms).
+
+```scheme
+(when (antidebug-timing-anomaly? 50000000)
+  (exit 1))
+```
+
+#### antidebug-check-all
+
+```scheme
+(antidebug-check-all) → alist
+```
+
+Runs all non-destructive checks in one call. Returns an alist:
+
+```scheme
+((traced . #f) (ld-preload . #f) (timing . #f))
+```
+
+Any `#t` value indicates a detection. The timing check uses a 50ms internal threshold.
+
+```scheme
+(let ([results (antidebug-check-all)])
+  (when (ormap cdr results)
+    (display "debug environment detected\n" (current-error-port))
+    (exit 1)))
+```
+
+Or check individually:
+
+```scheme
+(let ([results (antidebug-check-all)])
+  (when (cdr (assq 'traced results))
+    (log "tracer detected"))
+  (when (cdr (assq 'ld-preload results))
+    (log "LD_PRELOAD detected")))
+```
+
+---
+
+### (std os seccomp)
+
+#### seccomp-available?
+
+```scheme
+(seccomp-available?) → boolean
+```
+
+Returns `#t` if the kernel supports seccomp-bpf filtering. Should be `#t` on any Linux 3.5+ kernel with `CONFIG_SECCOMP_FILTER`.
+
+#### seccomp-lock!
+
+```scheme
+(seccomp-lock!) → void
+```
+
+Installs a BPF filter that kills the process if any of these syscalls are attempted:
+
+| Syscall | Number | Why blocked |
+|---------|--------|-------------|
+| `ptrace` | 101 | Prevents debugger attach after startup |
+| `process_vm_readv` | 310 | Prevents cross-process memory reads |
+| `process_vm_writev` | 311 | Prevents cross-process memory writes |
+| `personality` | 135 | Prevents `READ_IMPLIES_EXEC` (NX bypass) |
+
+All other syscalls remain allowed. **Irreversible** — the filter persists for the process lifetime. Also sets `PR_SET_NO_NEW_PRIVS` (required by seccomp, prevents suid escalation).
+
+Raises `&seccomp-error` on failure.
+
+**Call this AFTER all initialization is complete** — after loading shared libraries, opening files, spawning initial threads, etc.
+
+#### seccomp-lock-strict!
+
+```scheme
+(seccomp-lock-strict! syscall-list) → void
+```
+
+Whitelist mode: ONLY the listed syscall numbers are allowed. Everything else kills the process. **Much more restrictive** — use only if you know exactly what syscalls your program needs.
+
+```scheme
+;; Minimal set for a program that only does I/O and exits
+(seccomp-lock-strict!
+  '(0     ; read
+    1     ; write
+    3     ; close
+    9     ; mmap
+    11    ; munmap
+    12    ; brk
+    60    ; exit
+    231   ; exit_group
+    ))
+```
+
+**Warning:** Chez Scheme's runtime (GC, threads, I/O) uses many syscalls. Getting the whitelist wrong will kill your process with SIGSYS. Start with `seccomp-lock!` (blocklist mode) unless you have a specific need for strict mode.
+
+---
+
+### (std os integrity)
+
+#### integrity-hash-self
+
+```scheme
+(integrity-hash-self) → bytevector
+```
+
+Reads `/proc/self/exe` and returns its SHA-256 hash as a 32-byte bytevector. This always reads the actual binary on disk, even if the process was started via a symlink or with a different `argv[0]`.
+
+```scheme
+(let ([hash (integrity-hash-self)])
+  (printf "binary SHA-256: ~a~%"
+    (bytevector->hex hash)))  ; you'd need a hex conversion helper
+
+;; Or just compare:
+(define expected-hash #vu8(... 32 bytes ...))
+(unless (integrity-verify-hash expected-hash)
+  (exit 1))
+```
+
+#### integrity-verify-hash
+
+```scheme
+(integrity-verify-hash expected-hash) → boolean
+```
+
+Reads `/proc/self/exe`, SHA-256 hashes it, and compares against `expected-hash` using constant-time comparison (prevents timing side-channels).
+
+`expected-hash` must be a 32-byte bytevector. Raises `&integrity-error` if not.
+
+Returns `#t` if the binary matches, `#f` if modified.
+
+#### integrity-verify-signature
+
+```scheme
+(integrity-verify-signature pubkey signature exclude-offset exclude-len) → boolean
+```
+
+Reads `/proc/self/exe`, optionally zeros out a region (where the signature is embedded), and verifies an Ed25519 signature.
+
+- `pubkey`: 32-byte bytevector (Ed25519 public key)
+- `signature`: 64-byte bytevector (Ed25519 signature)
+- `exclude-offset`: byte offset of embedded signature in binary (0 if external)
+- `exclude-len`: length of region to zero (0 if external)
+
+Returns `#t` if the signature is valid, `#f` if not. Raises on invalid input sizes.
+
+**Build-time signing workflow:**
+
+1. Build the binary with a zeroed signature slot.
+2. Hash the binary (with zeroed slot).
+3. Sign the hash with your Ed25519 private key offline.
+4. Patch the signature into the binary.
+5. At runtime, `integrity-verify-signature` zeros the same region and verifies.
+
+```scheme
+;; Runtime verification (pubkey embedded in source or config)
+(define my-pubkey #vu8(... 32 bytes ...))
+(define my-sig   #vu8(... 64 bytes ...))
+
+(unless (integrity-verify-signature my-pubkey my-sig
+          #x1000  ; signature lives at offset 0x1000
+          64)     ; 64 bytes to zero
+  (display "signature verification failed\n" (current-error-port))
+  (exit 1))
+```
+
+#### integrity-hash-file
+
+```scheme
+(integrity-hash-file path) → bytevector
+```
+
+SHA-256 hash of an entire file. `path` is a string. Returns 32-byte bytevector.
+
+```scheme
+;; Verify a companion file hasn't been tampered with
+(define expected #vu8(...))
+(unless (bytevector=? (integrity-hash-file "/etc/myapp/config.enc") expected)
+  (error 'startup "config file tampered"))
+```
+
+#### integrity-hash-region
+
+```scheme
+(integrity-hash-region path offset length) → bytevector
+```
+
+SHA-256 hash of a specific byte range within a file. `offset` and `length` are exact integers. Pass `length` = 0 to hash from `offset` to end of file.
+
+Useful for hashing only the `.text` section of an ELF (more robust than full-file hashing since ELF headers and debug sections may be modified by tools).
+
+---
+
+## Integration Patterns for jerboa-shell
+
+### Pattern 1: Startup Hardening in main.sls
+
+Add a hardening phase early in the jsh startup sequence, before loading user config files:
+
+```scheme
+;; In (jsh main) or a new (jsh harden) module
+
+(define (harden-startup!)
+  ;; Phase 1: Anti-debug (before anything sensitive loads)
+  (guard (e [#t (void)])  ; don't crash in dev/test
+    (antidebug-ptrace!))
+
+  ;; Phase 2: Environment checks
+  (let ([checks (antidebug-check-all)])
+    (when (cdr (assq 'traced checks))
+      (exit 1))
+    (when (cdr (assq 'ld-preload checks))
+      (exit 1)))
+
+  ;; Phase 3: Kernel lockdown (AFTER all .so loading is complete)
+  (when (seccomp-available?)
+    (seccomp-lock!)))
+```
+
+Call `(harden-startup!)` from `main` after `Sbuild_heap` / library loading but before processing user input.
+
+### Pattern 2: Conditional Hardening via Environment Variable
+
+For development, you probably want to disable hardening:
+
+```scheme
+(define (harden-startup!)
+  (unless (getenv "JSH_DEV")  ; skip in dev mode
+    (guard (e [#t (void)])
+      (antidebug-ptrace!))
+    (let ([checks (antidebug-check-all)])
+      (when (ormap cdr checks)
+        (exit 1)))
+    (when (seccomp-available?)
+      (seccomp-lock!))))
+```
+
+### Pattern 3: Self-Integrity for Static Binary
+
+For `jsh-musl` (the static musl binary), verify the binary hash at startup:
+
+```scheme
+;; Expected hash computed at build time and embedded in source
+;; or loaded from a signed manifest file
+(define (verify-binary-integrity!)
+  (let ([expected (load-expected-hash)])  ; from embedded data or signed file
+    (unless (integrity-verify-hash expected)
+      (display "binary integrity check failed\n" (current-error-port))
+      (exit 1))))
+```
+
+For the build script (`build-binary-jsh.ss`), compute the hash after linking:
+
+```bash
+# After building jsh-musl:
+HASH=$(sha256sum jsh-musl | cut -d' ' -f1)
+echo "Expected hash: $HASH"
+# Embed in a config file or use as a deployment check
+```
+
+### Pattern 4: Layered Defense in C Main (gsh-main.c / jsh-main.c)
+
+For the strongest protection, add checks in the C main before Chez even starts:
+
+```c
+#include <sys/ptrace.h>
+
+extern int jerboa_antidebug_ptrace(void);
+extern int jerboa_antidebug_check_tracer(void);
+extern int jerboa_antidebug_check_ld_preload(void);
+extern int jerboa_integrity_verify_hash(const unsigned char *, size_t);
+extern int jerboa_seccomp_lock(void);
+
+int main(int argc, char *argv[]) {
+    // Phase 0: Before Chez init — C-level checks
+    if (jerboa_antidebug_ptrace() != 0) _exit(1);
+    if (jerboa_antidebug_check_tracer() != 0) _exit(1);
+    if (jerboa_antidebug_check_ld_preload() != 0) _exit(1);
+
+    // Phase 1: Chez init
+    Sscheme_init(NULL);
+    Sregister_boot_file_bytes(...);
+    Sbuild_heap(NULL, NULL);
+
+    // Phase 2: After all loading, lock syscalls
+    jerboa_seccomp_lock();
+
+    // Phase 3: Run the Scheme program
+    return Sscheme_script(prog_path, argc, argv);
+}
+```
+
+To link against `libjerboa_native.a` in the musl build:
+
+```bash
+# In build-jsh-musl.sh, add to the link step:
+musl-gcc -static -o jsh-musl \
+    jsh-main.o ffi-shim.o \
+    -L ~/mine/jerboa/jerboa-native-rs/target/release \
+    -ljerboa_native \
+    -lkernel -llz4 -lz -lm -ldl -lpthread
+```
+
+### Pattern 5: Sandboxing with Landlock After Init
+
+Combine hardening with Landlock to restrict filesystem access:
+
+```scheme
+(import (std os landlock-native)
+        (std os antidebug)
+        (std os seccomp))
+
+(define (full-lockdown! home-dir)
+  ;; 1. Anti-debug
+  (guard (e [#t (void)]) (antidebug-ptrace!))
+  (when (antidebug-traced?) (exit 1))
+
+  ;; 2. Filesystem sandbox via Landlock
+  (when (landlock-available?)
+    (landlock-enforce!
+      ;; Read-only paths
+      (list "/etc" home-dir)
+      ;; Read-write paths
+      (list (string-append home-dir "/.jsh_history")
+            "/tmp")
+      ;; Executable paths
+      (list "/usr/bin" "/bin")))
+
+  ;; 3. Syscall lockdown (LAST — after all setup)
+  (when (seccomp-available?)
+    (seccomp-lock!)))
+```
+
+### Pattern 6: Watchdog Thread
+
+For ongoing protection, spawn a background thread that periodically re-checks:
+
+```scheme
+(import (chezscheme)
+        (std os antidebug))
+
+(define (start-watchdog!)
+  (fork-thread
+    (lambda ()
+      (let loop ()
+        (sleep (make-time 'time-duration 0 5))  ; every 5 seconds
+        (when (antidebug-traced?)
+          (exit 1))
+        (loop)))))
+```
+
+---
+
+## Error Handling
+
+All three modules define condition types for structured error handling:
+
+```scheme
+;; Antidebug errors
+(guard (e [(antidebug-error? e)
+           (printf "antidebug: ~a~%" (antidebug-error-reason e))])
+  (antidebug-ptrace!))
+
+;; Seccomp errors
+(guard (e [(seccomp-error? e)
+           (printf "seccomp: ~a~%" (seccomp-error-reason e))])
+  (seccomp-lock!))
+
+;; Integrity errors
+(guard (e [(integrity-error? e)
+           (printf "integrity: ~a~%" (integrity-error-reason e))])
+  (integrity-hash-self))
+```
+
+For the Rust-side error detail, `(std os integrity)` exposes `native-last-error` internally, and integrity errors include it in the message condition.
+
+---
+
+## Security Notes
+
+**Order matters.** The recommended sequence is:
+
+1. `antidebug-ptrace!` — must be first (blocks debugger attach)
+2. `antidebug-check-all` — detect existing tracers/injection
+3. `integrity-verify-hash` or `integrity-verify-signature` — verify binary
+4. Load all shared libraries and open all files
+5. `landlock-enforce!` — filesystem restrictions (blocks new .so loading)
+6. `seccomp-lock!` — syscall restrictions (must be LAST)
+
+Reversing steps 5 and 6 is fine. But `seccomp-lock!` must come after all library loading and file opening, because the BPF filter is permanent.
+
+**Don't leak detection details.** Use the same generic error message for all failures. Don't tell an attacker which check caught them:
+
+```scheme
+;; Good: generic message
+(when (antidebug-traced?) (exit 1))
+
+;; Bad: tells attacker what to bypass
+(when (antidebug-traced?)
+  (display "TracerPid check failed\n")
+  (exit 1))
+```
+
+**Dev mode escape hatch.** Always provide a way to disable hardening for development and testing. An environment variable (`JSH_DEV=1`) is the simplest approach. Never ship with the escape hatch enabled.
diff --git a/docs/harden.md b/docs/harden.md
new file mode 100644
index 0000000..44a8d42
--- /dev/null
+++ b/docs/harden.md
@@ -0,0 +1,711 @@
+# Binary Hardening Guide
+
+Techniques for producing Jerboa binaries that resist tampering, debugging, and reverse engineering. All techniques target the single-binary ELF described in [single-binary.md](single-binary.md) and leverage the existing Rust native backend (`jerboa-native-rs`).
+
+## Implementation Status
+
+The following Rust modules and Scheme wrappers are **implemented and tested**:
+
+| Module | Rust | Scheme | Tests |
+|--------|------|--------|-------|
+| Anti-debug (ptrace, TracerPid, LD_PRELOAD, timing) | `antidebug.rs` | `(std os antidebug)` | 4/4 pass |
+| seccomp-bpf filtering | `seccomp.rs` | `(std os seccomp)` | 2/2 pass |
+| Integrity (SHA-256 self-hash, Ed25519 verify, file hash) | `integrity.rs` | `(std os integrity)` | 13/13 pass |
+| Landlock sandboxing | `landlock.rs` | `(std os landlock-native)` | existing |
+| Secure memory | `secure_mem.rs` | `(std crypto secure-mem)` | existing |
+
+Sections 7 (Encrypted Boot Files) and 12 (Build Pipeline) are design-only — they require a C main entry point (e.g., `jsh-main.c`) which lives in the consuming project (jerboa-shell), not this repo.
+
+---
+
+## Table of Contents
+
+1. [Threat Model](#1-threat-model)
+2. [Ed25519 Code Signing](#2-ed25519-code-signing)
+3. [Self-Integrity Check (SHA-256)](#3-self-integrity-check-sha-256)
+4. [Anti-Debug: ptrace Self-Trace](#4-anti-debug-ptrace-self-trace)
+5. [Anti-Debug: TracerPid and Environment Checks](#5-anti-debug-tracerpid-and-environment-checks)
+6. [Anti-Debug: Timing Checks](#6-anti-debug-timing-checks)
+7. [Encrypted Boot Files](#7-encrypted-boot-files)
+8. [Symbol Stripping and Obfuscation](#8-symbol-stripping-and-obfuscation)
+9. [Landlock Self-Sandboxing](#9-landlock-self-sandboxing)
+10. [seccomp Post-Init Filter](#10-seccomp-post-init-filter)
+11. [Secure Memory for Keys](#11-secure-memory-for-keys)
+12. [Build Pipeline Integration](#12-build-pipeline-integration)
+13. [Comparison with Other Languages](#13-comparison-with-other-languages)
+14. [Limitations and Honest Caveats](#14-limitations-and-honest-caveats)
+
+---
+
+## 1. Threat Model
+
+These protections target:
+
+- **Binary modification**: An attacker patches the ELF to change behavior (bypass auth, remove license checks, inject code).
+- **Dynamic analysis**: An attacker attaches gdb/strace/ltrace to inspect runtime state, extract keys, or trace control flow.
+- **Static analysis**: An attacker runs `strings`, `objdump`, or Ghidra on the binary to understand the Scheme code embedded in boot files.
+- **Library injection**: An attacker uses `LD_PRELOAD` or `LD_LIBRARY_PATH` to intercept function calls.
+
+These protections do NOT target:
+
+- Kernel-level attackers (root with kernel module access).
+- Hardware-level attacks (cold boot, bus snooping).
+- Side-channel attacks on the crypto itself (ring already handles that).
+
+No userspace binary can fully protect itself from a sufficiently privileged attacker. The goal is to raise the cost of attack above the value of what's protected.
+
+---
+
+## 2. Ed25519 Code Signing
+
+The strongest tamper-detection mechanism. An attacker who modifies the binary cannot forge a valid signature without the private key.
+
+### How It Works
+
+1. Build the binary normally.
+2. Hash the ELF (excluding the signature region).
+3. Sign the hash with an Ed25519 private key (kept offline).
+4. Append or embed the 64-byte signature.
+5. At startup, verify the signature using the embedded public key.
+
+### Rust Implementation
+
+Add to `jerboa-native-rs/src/integrity.rs`:
+
+```rust
+use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519};
+use std::fs;
+
+/// Verify the binary's Ed25519 signature.
+/// Returns 1 if valid, 0 if invalid, -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_verify_self_signature(
+    pubkey: *const u8,      // 32-byte Ed25519 public key
+    sig_offset: u64,        // byte offset where 64-byte signature lives
+) -> i32 {
+    // Read /proc/self/exe (always resolves to the real binary)
+    let binary = match fs::read("/proc/self/exe") {
+        Ok(b) => b,
+        Err(_) => return -1,
+    };
+
+    let sig_off = sig_offset as usize;
+    if sig_off + 64 > binary.len() { return -1; }
+
+    // Extract signature, then zero it for verification
+    let signature = binary[sig_off..sig_off + 64].to_vec();
+    let mut message = binary;
+    // Zero out the signature region (hash what the binary looked like before signing)
+    for b in &mut message[sig_off..sig_off + 64] {
+        *b = 0;
+    }
+
+    let pk = unsafe { std::slice::from_raw_parts(pubkey, 32) };
+    let verify_key = UnparsedPublicKey::new(&ED25519, pk);
+
+    match verify_key.verify(&message, &signature) {
+        Ok(()) => 1,
+        Err(_) => 0,
+    }
+}
+```
+
+### C Main Integration
+
+In `jsh-main.c`, before `Sbuild_heap`:
+
+```c
+// Embedded at build time by the signing script
+static const unsigned char ed25519_pubkey[32] = { /* ... */ };
+#define SIG_OFFSET 0x00000000ULL  // patched by signing script
+
+extern int jerboa_verify_self_signature(const unsigned char *pubkey, uint64_t sig_offset);
+
+int main(int argc, char *argv[]) {
+    if (jerboa_verify_self_signature(ed25519_pubkey, SIG_OFFSET) != 1) {
+        write(2, "integrity check failed\n", 23);
+        _exit(1);
+    }
+    // ... normal Chez init ...
+}
+```
+
+### Build-Time Signing Script
+
+```bash
+#!/bin/bash
+# sign-binary.sh — run after linking jsh
+BINARY="$1"
+PRIVKEY="$2"  # Ed25519 private key (keep offline)
+
+# 1. Find the signature slot (64 zero bytes at a known symbol)
+SIG_OFFSET=$(nm "$BINARY" | grep '__jerboa_signature' | awk '{print "0x"$1}')
+
+# 2. Zero the slot, hash, sign
+dd if=/dev/zero of="$BINARY" bs=1 count=64 seek=$((SIG_OFFSET)) conv=notrunc
+HASH=$(sha256sum "$BINARY" | awk '{print $1}')
+
+# 3. Sign with Ed25519 (via a small Rust tool or openssl)
+SIGNATURE=$(jerboa-sign --key "$PRIVKEY" --hash "$HASH")
+
+# 4. Write signature into the slot
+echo -n "$SIGNATURE" | xxd -r -p | dd of="$BINARY" bs=1 count=64 seek=$((SIG_OFFSET)) conv=notrunc
+```
+
+### Providing the Signature Slot
+
+In `jsh-main.c`, reserve a known location:
+
+```c
+// 64-byte signature slot — zeroed at compile time, filled by signing script
+__attribute__((section(".jerboa_sig")))
+volatile const unsigned char __jerboa_signature[64] = {0};
+```
+
+---
+
+## 3. Self-Integrity Check (SHA-256)
+
+A simpler alternative to full code signing. Less secure (an attacker can recompute the hash), but useful as a quick sanity check or as a complement to signing.
+
+### The Bootstrapping Problem
+
+You cannot embed a SHA-256 hash of a file inside that same file. Three solutions:
+
+**Option A: Hash with exclusion zone**
+
+Hash everything except a known 32-byte region. The build script writes the hash into that region post-link.
+
+```c
+// Reserve the hash slot
+__attribute__((section(".jerboa_hash")))
+volatile const unsigned char __jerboa_expected_hash[32] = {0};
+
+static int check_self_hash(void) {
+    // Read /proc/self/exe
+    int fd = open("/proc/self/exe", O_RDONLY);
+    // ... read entire file into buffer ...
+
+    // Zero out the hash slot before hashing
+    memset(buffer + hash_slot_offset, 0, 32);
+
+    // SHA-256 the modified buffer
+    unsigned char actual[32];
+    jerboa_sha256(buffer, file_size, actual, 32);
+
+    // Compare
+    return jerboa_timing_safe_equal(actual, 32,
+        (const unsigned char *)__jerboa_expected_hash, 32);
+}
+```
+
+**Option B: ELF segment hashing**
+
+Hash only `.text` + `.rodata` sections (the code and constant data). This is more robust against tools that modify ELF headers or debug sections.
+
+```c
+#include <elf.h>
+
+static int check_code_segments(void) {
+    // Parse ELF headers from /proc/self/exe
+    // Find PT_LOAD segments with PF_X (executable) flag
+    // Hash those segments only
+    // Compare against embedded expected hash
+}
+```
+
+**Option C: Detached signature file**
+
+Store the hash in a separate `jsh.sig` file. Simplest to implement but requires distributing two files.
+
+### Build Script for Option A
+
+```bash
+#!/bin/bash
+BINARY="$1"
+HASH_OFFSET=$(nm "$BINARY" | grep '__jerboa_expected_hash' | awk '{print "0x"$1}')
+
+# Zero the slot
+dd if=/dev/zero of="$BINARY" bs=1 count=32 seek=$((HASH_OFFSET)) conv=notrunc
+
+# Hash the binary with zeroed slot
+HASH=$(sha256sum "$BINARY" | cut -d' ' -f1)
+
+# Write hash into slot
+echo -n "$HASH" | xxd -r -p | dd of="$BINARY" bs=1 count=32 seek=$((HASH_OFFSET)) conv=notrunc
+```
+
+---
+
+## 4. Anti-Debug: ptrace Self-Trace
+
+A process can only have one tracer. By tracing yourself, you prevent gdb/strace from attaching.
+
+### Implementation
+
+In `jsh-main.c`, as the very first thing in `main()`:
+
+```c
+#include <sys/ptrace.h>
+
+static void anti_debug_ptrace(void) {
+    if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
+        // PTRACE_TRACEME failed — something is already tracing us
+        _exit(1);
+    }
+}
+```
+
+### Hardening the Check
+
+A single ptrace call is trivially patchable (NOP out the branch). Make it harder:
+
+```c
+static void anti_debug_ptrace(void) {
+    // Call from multiple places with different consequences
+    volatile int result = ptrace(PTRACE_TRACEME, 0, NULL, NULL);
+
+    // Don't branch immediately — use the result later
+    // to derive a value needed for decryption (see section 7)
+    if (result == -1) {
+        // Corrupt a key byte — decryption will fail silently later
+        // rather than giving an obvious "debugger detected" message
+        boot_key[0] ^= 0xFF;
+    }
+}
+```
+
+This ties the anti-debug check to the decryption path. An attacker who patches out the check still gets garbage when decrypting boot files.
+
+---
+
+## 5. Anti-Debug: TracerPid and Environment Checks
+
+Complementary checks that catch different attack vectors than ptrace.
+
+### TracerPid
+
+```c
+static int check_tracer_pid(void) {
+    FILE *f = fopen("/proc/self/status", "r");
+    if (!f) return 0;  // can't check, proceed cautiously
+
+    char line[256];
+    while (fgets(line, sizeof(line), f)) {
+        if (strncmp(line, "TracerPid:", 10) == 0) {
+            long pid = strtol(line + 10, NULL, 10);
+            fclose(f);
+            return pid != 0;  // nonzero = debugger attached
+        }
+    }
+    fclose(f);
+    return 0;
+}
+```
+
+### LD_PRELOAD Detection
+
+```c
+static int check_ld_preload(void) {
+    // Check environment
+    if (getenv("LD_PRELOAD") != NULL) return 1;
+
+    // Also check /proc/self/environ in case env was cleared after load
+    int fd = open("/proc/self/environ", O_RDONLY);
+    if (fd < 0) return 0;
+    char buf[4096];
+    ssize_t n = read(fd, buf, sizeof(buf));
+    close(fd);
+
+    // Search for LD_PRELOAD in the raw environ block
+    for (ssize_t i = 0; i < n - 10; i++) {
+        if (memcmp(buf + i, "LD_PRELOAD", 10) == 0) return 1;
+    }
+    return 0;
+}
+```
+
+### Breakpoint Detection
+
+Check for `INT3` (0xCC) instructions at key function entry points:
+
+```c
+static int check_breakpoints(void) {
+    // Check our own function entry points for software breakpoints
+    unsigned char *check_fn = (unsigned char *)&check_self_hash;
+    unsigned char *main_fn = (unsigned char *)&main;
+
+    if (*check_fn == 0xCC || *main_fn == 0xCC) return 1;
+    return 0;
+}
+```
+
+---
+
+## 6. Anti-Debug: Timing Checks
+
+Debuggers slow execution. Measure critical sections and abort if they take too long.
+
+```c
+#include <time.h>
+
+static void timed_check(void (*fn)(void), long max_ns) {
+    struct timespec t1, t2;
+    clock_gettime(CLOCK_MONOTONIC, &t1);
+    fn();
+    clock_gettime(CLOCK_MONOTONIC, &t2);
+
+    long elapsed = (t2.tv_sec - t1.tv_sec) * 1000000000L
+                 + (t2.tv_nsec - t1.tv_nsec);
+
+    if (elapsed > max_ns) {
+        _exit(1);
+    }
+}
+
+// Usage: integrity check should complete in <100ms
+timed_check(check_self_hash, 100000000L);
+```
+
+### Continuous Background Check
+
+After Chez is initialized, spawn a thread that periodically re-checks:
+
+```c
+static void *watchdog_thread(void *arg) {
+    while (1) {
+        usleep(5000000);  // every 5 seconds
+        if (check_tracer_pid()) _exit(1);
+        if (check_breakpoints()) _exit(1);
+    }
+    return NULL;
+}
+
+// After Sbuild_heap, before Sscheme_script:
+pthread_t watchdog;
+pthread_create(&watchdog, NULL, watchdog_thread, NULL);
+```
+
+---
+
+## 7. Encrypted Boot Files
+
+This is the highest-value technique unique to Jerboa's architecture. Boot files contain all Scheme source in compiled form — encrypting them prevents static analysis.
+
+### Architecture
+
+```
+Build time:
+  petite.boot ──→ AES-256-GCM encrypt ──→ petite_boot_enc.h (ciphertext + nonce + tag)
+  scheme.boot ──→ AES-256-GCM encrypt ──→ scheme_boot_enc.h
+  jsh.boot    ──→ AES-256-GCM encrypt ──→ jsh_boot_enc.h
+
+Runtime:
+  Read encrypted arrays from .rodata
+  ──→ Derive decryption key
+  ──→ AES-256-GCM decrypt into mmap'd memory
+  ──→ Sregister_boot_file_bytes(decrypted)
+  ──→ Wipe decrypted copy after Chez loads it
+```
+
+### Key Derivation
+
+The decryption key should not be a single static value (too easy to extract). Combine multiple sources:
+
+```c
+static void derive_boot_key(unsigned char key[32]) {
+    unsigned char material[128];
+    int offset = 0;
+
+    // Component 1: Embedded key fragment (32 bytes, split across functions)
+    memcpy(material + offset, key_fragment_1, 16); offset += 16;
+    memcpy(material + offset, key_fragment_2, 16); offset += 16;
+
+    // Component 2: Binary's own code hash (ties key to unmodified binary)
+    unsigned char code_hash[32];
+    hash_text_section(code_hash);
+    memcpy(material + offset, code_hash, 32); offset += 32;
+
+    // Component 3: Compile-time constant (changes per build)
+    memcpy(material + offset, build_nonce, 16); offset += 16;
+
+    // HKDF-SHA256 to derive final key
+    jerboa_sha256(material, offset, key, 32);
+
+    // Wipe intermediates
+    explicit_bzero(material, sizeof(material));
+    explicit_bzero(code_hash, sizeof(code_hash));
+}
+```
+
+Component 2 is the critical trick: the key depends on the binary's own `.text` section hash. If an attacker patches the anti-debug checks, the `.text` hash changes, the derived key changes, and decryption fails. This creates a cryptographic binding between the code integrity and the ability to run.
+
+### Decryption at Startup
+
+```c
+static void *decrypt_boot(const unsigned char *enc_data, unsigned int enc_size,
+                          const unsigned char *nonce, unsigned int *out_size) {
+    unsigned char key[32];
+    derive_boot_key(key);
+
+    // Allocate via secure memory (mlock'd, no core dump, no fork)
+    unsigned char *output = (unsigned char *)jerboa_secure_alloc(enc_size);
+
+    size_t pt_len = 0;
+    int rc = jerboa_aead_open(
+        key, 32,
+        nonce, 12,
+        enc_data, enc_size,
+        NULL, 0,           // no AAD
+        output, enc_size,
+        &pt_len
+    );
+
+    explicit_bzero(key, 32);
+
+    if (rc != 0) {
+        jerboa_secure_free(output, enc_size);
+        return NULL;