Add binary hardening flags to musl build and secure binary strategy doc

ober

539f275e1ec99c2ea2535d443a0d11d11322d4ca

diff --git a/jerboa-native-rs/.cargo/config.toml b/jerboa-native-rs/.cargo/config.toml
new file mode 100644
index 0000000..31c41b2
--- /dev/null
+++ b/jerboa-native-rs/.cargo/config.toml
@@ -0,0 +1,29 @@
+# Hardening flags for the Rust native library.
+# These apply when building libjerboa_native (cdylib + staticlib).
+
+[target.x86_64-unknown-linux-gnu]
+rustflags = [
+  "-C", "link-arg=-Wl,-z,relro,-z,now",     # Full RELRO + BIND_NOW
+  "-C", "link-arg=-fcf-protection=full",      # Intel CET (SHSTK + IBT)
+]
+
+[target.x86_64-unknown-linux-musl]
+rustflags = [
+  "-C", "link-arg=-Wl,-z,relro,-z,now",
+  "-C", "link-arg=-fcf-protection=full",
+]
+
+[target.aarch64-unknown-linux-gnu]
+rustflags = [
+  "-C", "link-arg=-Wl,-z,relro,-z,now",
+]
+
+[target.x86_64-unknown-freebsd]
+rustflags = [
+  "-C", "link-arg=-Wl,-z,relro,-z,now",
+]
+
+[target.aarch64-unknown-freebsd]
+rustflags = [
+  "-C", "link-arg=-Wl,-z,relro,-z,now",
+]
diff --git a/jerboa-native-rs/src/regex_native.rs b/jerboa-native-rs/src/regex_native.rs
index c306d09..3c5e5d8 100644
--- a/jerboa-native-rs/src/regex_native.rs
+++ b/jerboa-native-rs/src/regex_native.rs
@@ -144,3 +144,155 @@ pub extern "C" fn jerboa_regex_free(handle: u64) -> i32 {
         0
     })
 }
+
+// ========== Extended API for grep/sed integration ==========
+
+/// Compile with PCRE2-compatible flags bitmask.
+/// Flags: 0x8=CASELESS, 0x20=DOTALL, 0x400=MULTILINE, 0x80000=UTF (ignored, always UTF-8)
+/// Returns 0 on success, -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_regex_compile_ex(
+    pattern: *const u8, pattern_len: usize,
+    flags: u32,
+    handle: *mut u64,
+) -> i32 {
+    ffi_wrap(|| {
+        if pattern.is_null() || handle.is_null() { return -1; }
+        let pat_bytes = unsafe { std::slice::from_raw_parts(pattern, pattern_len) };
+        let pat = match std::str::from_utf8(pat_bytes) {
+            Ok(s) => s,
+            Err(_) => {
+                set_last_error("invalid UTF-8 in pattern".to_string());
+                return -1;
+            }
+        };
+        // Build inline flags prefix from PCRE2-compatible bitmask
+        let mut prefix = String::from("(?");
+        let mut has_flags = false;
+        if flags & 0x8 != 0 { prefix.push('i'); has_flags = true; }     // CASELESS
+        if flags & 0x400 != 0 { prefix.push('m'); has_flags = true; }   // MULTILINE
+        if flags & 0x20 != 0 { prefix.push('s'); has_flags = true; }    // DOTALL
+        let full_pattern = if has_flags {
+            prefix.push(')');
+            format!("{}{}", prefix, pat)
+        } else {
+            pat.to_string()
+        };
+        match Regex::new(&full_pattern) {
+            Ok(re) => {
+                let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+                REGEX_STORE.lock().unwrap().insert(id, re);
+                unsafe { *handle = id; }
+                0
+            }
+            Err(e) => {
+                set_last_error(format!("regex compile error: {}", e));
+                -1
+            }
+        }
+    })
+}
+
+/// Find first match starting at byte offset.
+/// Returns 1 if found, 0 if not, -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_regex_find_at(
+    handle: u64,
+    text: *const u8, text_len: usize,
+    start_offset: usize,
+    match_start: *mut usize,
+    match_end: *mut usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if match_start.is_null() || match_end.is_null() { return -1; }
+        let store = REGEX_STORE.lock().unwrap();
+        let re = match store.get(&handle) {
+            Some(r) => r,
+            None => return -1,
+        };
+        let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) };
+        let s = match std::str::from_utf8(text_bytes) {
+            Ok(s) => s,
+            Err(_) => return -1,
+        };
+        if start_offset > s.len() { return 0; }
+        match re.find_at(s, start_offset) {
+            Some(m) => {
+                unsafe {
+                    *match_start = m.start();
+                    *match_end = m.end();
+                }
+                1
+            }
+            None => 0,
+        }
+    })
+}
+
+/// Find match with capture groups starting at byte offset.
+/// Writes (start, end) pairs to ovector_buf as usize values:
+///   ovector[0] = full match start, ovector[1] = full match end,
+///   ovector[2] = group 1 start, ovector[3] = group 1 end, etc.
+/// Unmatched optional groups get usize::MAX (0xFFFFFFFFFFFFFFFF).
+/// ovector_capacity is the number of usize slots available.
+/// Returns: number of groups written (>= 1 on match), 0 if no match, -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_regex_captures(
+    handle: u64,
+    text: *const u8, text_len: usize,
+    start_offset: usize,
+    ovector_buf: *mut usize,
+    ovector_capacity: usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if ovector_buf.is_null() || ovector_capacity < 2 { return -1; }
+        let store = REGEX_STORE.lock().unwrap();
+        let re = match store.get(&handle) {
+            Some(r) => r,
+            None => return -1,
+        };
+        let text_bytes = unsafe { std::slice::from_raw_parts(text, text_len) };
+        let s = match std::str::from_utf8(text_bytes) {
+            Ok(s) => s,
+            Err(_) => return -1,
+        };
+        if start_offset > s.len() { return 0; }
+        let caps = match re.captures_at(s, start_offset) {
+            Some(c) => c,
+            None => return 0,
+        };
+        let num_groups = caps.len(); // includes group 0 (full match)
+        let slots_needed = num_groups * 2;
+        let slots_to_write = slots_needed.min(ovector_capacity);
+        let groups_to_write = slots_to_write / 2;
+        let ov = unsafe { std::slice::from_raw_parts_mut(ovector_buf, slots_to_write) };
+        for i in 0..groups_to_write {
+            match caps.get(i) {
+                Some(m) => {
+                    ov[i * 2] = m.start();
+                    ov[i * 2 + 1] = m.end();
+                }
+                None => {
+                    ov[i * 2] = usize::MAX;
+                    ov[i * 2 + 1] = usize::MAX;
+                }
+            }
+        }
+        groups_to_write as i32
+    })
+}
+
+/// Get the number of capture groups in a compiled regex (including group 0).
+/// Returns count >= 1, or -1 on error.
+#[no_mangle]
+pub extern "C" fn jerboa_regex_group_count(handle: u64) -> i32 {
+    ffi_wrap(|| {
+        let store = REGEX_STORE.lock().unwrap();
+        let re = match store.get(&handle) {
+            Some(r) => r,
+            None => return -1,
+        };
+        // captures_len() returns the number of capture groups including group 0
+        (re.captures_len()) as i32
+    })
+}
diff --git a/lib/jerboa/build/musl.sls b/lib/jerboa/build/musl.sls
index e303a8a..6a7e6fd 100644
--- a/lib/jerboa/build/musl.sls
+++ b/lib/jerboa/build/musl.sls
@@ -242,47 +242,56 @@
 
   ;; ========== Link Command Generation ==========
   
-  (define (musl-link-command output-path object-files static-libs)
-    "Generate the musl-gcc link command for a static binary.
-     
-     Uses musl-gcc -static which handles CRT objects and -lc automatically.
-     We only need to specify our object files, libkernel.a, and any extra
-     static libraries.
-     
+  (define (musl-link-command output-path object-files static-libs . opts)
+    "Generate the musl-gcc link command for a hardened static PIE binary.
+
+     Uses musl-gcc -static-pie for ASLR support, with full RELRO and
+     BIND_NOW for GOT protection.  Pass no-harden: #t to get plain -static.
+
      Parameters:
        output-path  - Path for the output executable
        object-files - List of .o files to link
        static-libs  - List of additional .a archives
-     
+
+     Keyword options:
+       no-harden:   - #t to disable PIE/RELRO (plain -static)
+
      Returns: Command string"
-    (let* ([gcc (musl-gcc-path)]
+    (let* ([no-harden? (%musl-kwarg 'no-harden: opts #f)]
+           [gcc (musl-gcc-path)]
            [libkernel (musl-libkernel-path)]
            [libz (musl-libz-path)]
            [liblz4 (musl-liblz4-path)]
-           
+
            ;; Object files as space-separated string
            [objs (apply string-append
                    (map (lambda (o) (format " '~a'" o))
                         object-files))]
-           
+
            ;; Chez runtime archives
            [chez-libs (apply string-append
                        (filter values
                          (list (format " '~a'" libkernel)
                                (and libz (format " '~a'" libz))
                                (and liblz4 (format " '~a'" liblz4)))))]
-           
+
            ;; User static libraries
            [user-libs (apply string-append
                        (map (lambda (a) (format " '~a'" a))
                             static-libs))]
-           
+
            ;; Standard libraries needed by Chez runtime
-           [std-libs "-lm -lrt -lpthread"])
-      
-      ;; musl-gcc -static handles CRT objects and libc linking automatically
-      (format "~a -static~a~a~a ~a -o '~a'"
+           [std-libs "-lm -lrt -lpthread"]
+
+           ;; Hardened: static PIE + full RELRO + BIND_NOW
+           ;; Plain: just -static (for debugging or unsupported platforms)
+           [link-flags (if no-harden?
+                         "-static"
+                         "-static-pie -Wl,-z,relro,-z,now")])
+
+      (format "~a ~a~a~a~a ~a -o '~a'"
               gcc
+              link-flags
               objs             ;; Application + Chez main.o + static_boot.o
               chez-libs        ;; Chez runtime archives
               user-libs        ;; User static libs
@@ -310,6 +319,7 @@
        extra-c-files:  - Additional C files to compile
        extra-cflags:   - Additional C compiler flags
        verbose:        - Print commands as they execute
+       no-harden:      - #t to disable hardening flags (PIE, RELRO, stack protector, etc.)
      
      Returns: output-path on success, raises on error"
     
@@ -325,6 +335,7 @@
            [extra-c (%musl-kwarg 'extra-c-files: opts '())]
            [extra-cflags (%musl-kwarg 'extra-cflags: opts "")]
            [verbose? (%musl-kwarg 'verbose: opts #f)]
+           [no-harden? (%musl-kwarg 'no-harden: opts #f)]
            
            ;; Build directory
            [build-dir (format "/tmp/jerboa-musl-~a" 
@@ -369,9 +380,21 @@
                 (when verbose? (display "[4/5] Compiling C...\n"))
                 (let* ([static-boot-o (format "~a/static_boot.o" build-dir)]
                        [include-flag (format "-I'~a'" scheme-h-dir)]
-                       [compile-cmd 
-                        (format "~a -c -O2 ~a ~a -o '~a' '~a'"
-                                gcc include-flag extra-cflags
+                       [harden-cflags
+                        (if no-harden? ""
+                          (string-append
+                            " -fstack-protector-strong"
+                            " -fstack-clash-protection"
+                            " -D_FORTIFY_SOURCE=2"
+                            " -fPIE"
+                            ;; CET: only on x86_64 Linux where CPU may support it.
+                            ;; Harmless on CPUs without CET (instructions are NOPs).
+                            (if (memq (machine-type) '(a6le ta6le i3le ti3le))
+                              " -fcf-protection=full"
+                              "")))]
+                       [compile-cmd
+                        (format "~a -c -O2~a ~a ~a -o '~a' '~a'"
+                                gcc harden-cflags include-flag extra-cflags
                                 static-boot-o static-boot-c)]
                        [rc (begin
                              (when verbose? (printf "  ~a~n" compile-cmd))
@@ -384,12 +407,13 @@
                   ;; Compile extra C files
                   (let ([extra-objs
                          (map (lambda (c-file)
-                                (let ([o-file (format "~a/~a.o" 
+                                (let ([o-file (format "~a/~a.o"
                                                 build-dir
-                                                (%musl-path-root 
+                                                (%musl-path-root
                                                   (%musl-path-last c-file)))])
-                                  (let ([cmd (format "~a -c -O2 ~a ~a -o '~a' '~a'"
-                                                     gcc include-flag extra-cflags 
+                                  (let ([cmd (format "~a -c -O2~a ~a ~a -o '~a' '~a'"
+                                                     gcc harden-cflags
+                                                     include-flag extra-cflags
                                                      o-file c-file)])
                                     (when verbose? (printf "  ~a~n" cmd))
                                     (unless (= (system cmd) 0)
@@ -401,13 +425,14 @@
                     
                     ;; Step 5: Link
                     (when verbose? (display "[5/5] Linking...\n"))
-                    (let* ([all-objs (cons* chez-main-o 
+                    (let* ([all-objs (cons* chez-main-o
                                             static-boot-o
                                             extra-objs)]
-                           [link-cmd (musl-link-command 
-                                      output-path 
+                           [link-cmd (musl-link-command
+                                      output-path
                                       all-objs
-                                      static-libs)]
+                                      static-libs
+                                      'no-harden: no-harden?)]
                            [rc (begin
                                  (when verbose? (printf "  ~a~n" link-cmd))
                                  (system link-cmd))])
diff --git a/secure.md b/secure.md
new file mode 100644
index 0000000..3163114
--- /dev/null
+++ b/secure.md
@@ -0,0 +1,438 @@
+# Jerboa Secure Binary Strategy
+
+## The Problem
+
+Jerboa compiles to Chez Scheme native code. A static musl binary (e.g., jerboa-secmon at 11MB) embeds:
+
+| Component | Size | ROP Gadgets |
+|-----------|------|-------------|
+| petite.boot | ~1.9MB | None (`.rodata` data) |
+| scheme.boot | ~1.0MB | None (data) |
+| app.boot | ~2.6MB | None (data) |
+| libkernel (Chez runtime) | ~1.0MB | **Thousands** — GC, compiler, thread scheduler, I/O |
+| musl libc | ~200KB | Some |
+| Rust native lib | ~500KB | Minimal (memory-safe code) |
+| ffi-shim + program | ~25KB | Minimal |
+
+The Chez runtime (`libkernel`) is the primary source of ROP gadgets. At ~1MB of native code, it contains thousands of potentially usable gadget sequences. It is heavily interconnected (GC references compiler, compiler references I/O, I/O references threading) so `--gc-sections` removes very little.
+
+Boot files are pure data — zero gadget contribution. The Rust native library contributes minimal gadgets due to memory safety.
+
+---
+
+## Path 1: Harden the Chez Binary
+
+**Effort**: Days. **Impact**: Highest bang for buck.
+
+This doesn't shrink the binary but makes gadgets effectively unusable through hardware and OS enforcement.
+
+### Intel CET (Control-flow Enforcement Technology)
+
+Requires CPU support (`ibt` and `user_shstk` in `/proc/cpuinfo`). Available on Intel 12th gen+ and AMD Zen 4+.
+
+#### Shadow Stack (SHSTK) — Priority: Very High
+
+Hardware maintains a second copy of return addresses on a separate, protected shadow stack. On every `ret`, the CPU cross-checks the return address against the shadow copy. If they differ (because an attacker overwrote the real stack), the CPU faults with `#CP`.
+
+This **directly defeats classical ROP**, which fundamentally relies on corrupted return addresses.
+
+**Chez compatibility**: Likely works without modification. Chez uses standard `call`/`ret` calling conventions. The JIT-generated code uses normal x86_64 function prologues/epilogues. Shadow stack enforcement should be transparent.
+
+**To enable**: Rebuild Chez with `-fcf-protection=return` (SHSTK only) or `-fcf-protection=full` (SHSTK + IBT). The binary must also be linked with CET-aware linker flags.
+
+#### Indirect Branch Tracking (IBT) — Priority: Medium
+
+Every indirect jump/call target must begin with an `ENDBR64` instruction. If control flow arrives at a non-`ENDBR64` instruction via indirect branch, the CPU faults.
+
+**Chez compatibility**: Problematic. Chez's code generator emits native x86_64 code at runtime (during `compile-program` and boot file loading). This JIT-generated code does **not** have `ENDBR64` instructions at function entries. IBT will fault on calls into Chez-compiled code.
+
+**Fix**: Patch Chez's code generator (`compile.ss` and `gc.c`) to emit `ENDBR64` (4 bytes: `f3 0f 1e fa`) at every compiled function entry point. This is a targeted change — feasible but requires understanding Chez internals.
+
+**Alternative**: Enable SHSTK-only mode via `prctl(PR_SET_SHADOW_STACK, ...)` without IBT. Gets return-address protection without the indirect-branch requirement.
+
+### Standard Hardening CFLAGS
+
+Rebuild Chez and all C code with:
+
+```
+-fstack-protector-strong    # Stack canaries on functions with arrays/address-taken vars
+-fstack-clash-protection    # Probe large stack allocations (prevent guard-page bypass)
+-fcf-protection=full        # CET: shadow stack + indirect branch tracking
+-D_FORTIFY_SOURCE=2         # Compile-time and runtime buffer overflow checks
+-Wl,-z,relro,-z,now         # Full RELRO (read-only GOT/PLT after startup)
+-pie                        # Position-independent executable (for ASLR)
+```
+
+**Status: DONE in `lib/jerboa/build/musl.sls`** — all of the above are now applied by
+default. The `build-musl-binary` and `musl-link-command` functions automatically add these
+flags. Pass `no-harden: #t` to disable for debugging. CET flags (`-fcf-protection=full`)
+are only added on x86_64 Linux machine types.
+
+**Status: DONE in `jerboa-native-rs/.cargo/config.toml`** — Rust builds now pass
+`-Wl,-z,relro,-z,now` for all targets and `-fcf-protection=full` for x86_64 Linux.
+
+**NOT YET DONE — downstream projects**: jerboa-secmon and jerboa-dns have their own
+build scripts (`build-secmon-musl.ss`, `build-secmon-musl.sh`, `Dockerfile`) that hardcode
+`musl-gcc -c -O2` and `musl-gcc -static` directly instead of using `(jerboa build musl)`.
+These need the same flags added manually, or better, ported to use the shared build module.
+
+**NOT YET DONE — Chez Scheme itself (libkernel.a)**: This is the biggest gap. The
+Dockerfile builds Chez with `./configure --threads --disable-x11 --static CC=musl-gcc`
+and no `CFLAGS` override. The resulting `libkernel.a` — the ~1MB primary source of ROP
+gadgets — has **zero hardening**: no stack canaries, no CET, no FORTIFY_SOURCE. Fix by
+passing hardening flags when building Chez:
+
+```bash
+./configure --threads --disable-x11 --static \
+  CC=musl-gcc \
+  CFLAGS="-O2 -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=2 -fPIE -fcf-protection=full"
+```
+
+This is the single highest-impact change remaining — it hardens the code that contributes
+the most ROP gadgets.
+
+### Static PIE
+
+Switch musl build from `-static` to **`-static-pie`**. This enables full ASLR for the entire binary — the base address, stack, heap, and mmap regions are all randomized on each execution.
+
+Static linking actually slightly increases the gadget set (musl code is embedded rather than at a separate randomized address), but static PIE with ASLR makes all gadget addresses unpredictable. An attacker needs an information leak before they can chain gadgets.
+
+**Status: DONE in `lib/jerboa/build/musl.sls`** — `musl-link-command` now uses
+`-static-pie` by default instead of `-static`.
+
+### Namespace Isolation
+
+Add Linux namespace isolation to the security cage module (`lib/std/security/cage.sls`). After initialization, call:
+
+```c
+unshare(CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET)
+```
+
+This creates:
+- **PID namespace**: Process can't see or signal other processes
+- **Mount namespace**: Process sees only an allow-listed filesystem
+- **Network namespace**: Process has only its own network stack (or a specific pre-bound socket)
+
+Combined with existing seccomp + Landlock/Capsicum, this creates micro-VM-like isolation without VM overhead.
+
+**Requirement**: `CAP_SYS_ADMIN` or unprivileged user namespaces enabled (`sysctl kernel.unprivileged_userns_clone=1`).
+
+### Hardening Summary
+
+| Technique | Works with Chez JIT? | Effort | ROP Impact |
+|-----------|---------------------|--------|------------|
+| **SHSTK (shadow stack)** | Likely yes | Low (rebuild Chez) | **Directly defeats ROP** |
+| **IBT (indirect branch)** | No (needs Chez patch) | High | Constrains gadgets to function entries |
+| Stack canaries | C code only, not JIT | Low (add CFLAG) | Detects stack buffer overflows |
+| Static PIE + ASLR | Yes | Low (change linker flag) | Gadget addresses unpredictable |
+| `-fstack-clash-protection` | Yes | Trivial | Prevents guard-page bypass |
+| seccomp post-init | Yes | **Already done** | Blocks ptrace, process_vm_readv |
+| Landlock/Capsicum post-init | Yes | **Already done** | Blocks filesystem escape |
+| Anti-debug | Yes | **Already done** | Blocks dynamic analysis |
+| Integrity verification | Yes | **Already done** | Detects binary modification |
+| Namespace isolation | Yes | Medium | PID/mount/network containment |
+
+### FreeBSD Portability (Path 1)
+
+**Intel CET is not available on any released FreeBSD.** Kernel SHSTK plumbing exists in
+15-CURRENT only (Konstantin Belousov's work). IBT enforcement has not been implemented.
+Userland CET support (rtld awareness, base compiled with `-fcf-protection`) is incomplete
+even in -CURRENT. This means the highest-value ROP mitigation — hardware shadow stacks —
+is Linux-only for the foreseeable future.
+
+**What FreeBSD 14.x provides today:**
+
+| Feature | FreeBSD Status | vs Linux |
+|---------|---------------|----------|
+| ASLR + PIE | On by default since 13.0 | **Lower entropy**: ~14 bits (stack) vs Linux ~22 bits |
+| Stack canaries (`-strong`) | Since 12.0 | Comparable |
+| `_FORTIFY_SOURCE=2` | Since 14.0 | Linux distros had this since ~2006 |
+| Full RELRO + BIND_NOW | Since 12.0 | Comparable |
+| Stack clash protection | Since ~13.x (clang) | Comparable |
+| Capsicum capability mode | Mature, since 9.0 | No Linux equivalent (seccomp is different) |
+| ARM PAC | Since 13.0 (arm64) | Comparable (Linux 5.0+) |
+| ARM BTI | Since 14.0 (arm64) | Comparable (Linux 5.10+) |
+| W^X opt-in (PROTMAX) | Since 14.0 via `procctl` | Linux has `prctl(SET_MDWE)` since 6.3 |
+| SafeStack | Available (clang), not default | Same on Linux |
+
+**The Capsicum vs seccomp gap for ROP containment:**
+
+Capsicum and seccomp solve different problems. Capsicum restricts *which resources* a process
+can access (file descriptors, paths). seccomp restricts *which syscalls* a process can make.
+
+For ROP mitigation specifically, seccomp has a critical advantage: it can **block
+`mprotect(PROT_EXEC)`** after initialization, preventing a ROP chain from making attacker-
+controlled data executable (the typical second stage after gaining control flow). Capsicum
+cannot do this — it does not filter memory operations.
+
+FreeBSD's `procctl(PROC_PROTMAX_CTL)` partially addresses this gap: when enabled, the
+maximum protection on a mapping is set at `mmap` time and cannot be escalated via `mprotect`.
+However, this breaks Chez's JIT pattern (`mmap(RW)` then `mprotect(RX)`) unless all JIT
+compilation completes before PROTMAX is enabled. A post-init PROTMAX call — after boot files
+are loaded and all code is compiled — could work but requires careful sequencing.
+
+**FreeBSD-specific hardening not available on Linux:**
+
+- **Capsicum**: Process-level capability confinement with per-fd rights limiting. More
+  principled than seccomp for resource access control. Already integrated into Jerboa's
+  cage module.
+- **HardenedBSD**: A FreeBSD fork with PaX-like features (strict W^X via NOEXEC, SEGVGUARD
+  for brute-force ASLR defeat prevention, mandatory SafeStack). Tracks FreeBSD 14/15
+  branches. Worth considering as a deployment target for maximum hardening.
+
+**Practical implication**: On FreeBSD, the lack of CET makes **Path 2 (WASM sandbox) more
+important** than on Linux. Without hardware return-address protection, software isolation of
+security-critical parsers becomes the primary ROP defense rather than a defense-in-depth layer.
+
+**ARM64 FreeBSD** (e.g., AWS Graviton) is a notable bright spot: PAC (since 13.0) provides
+hardware return-address signing, and BTI (since 14.0) provides landing-pad enforcement. These
+are the ARM equivalents of CET SHSTK and IBT respectively, and they are available on released
+FreeBSD. For maximum hardware security on FreeBSD, target arm64.
+
+---
+
+## Path 2: WASM Sandbox for Parsers
+
+**Effort**: Weeks. **Impact**: Strongest isolation for the most-attacked code.
+
+### Why WASM Eliminates ROP
+
+WebAssembly is **structurally immune** to classical ROP:
+
+- **Separate code and data spaces**: Linear memory (where attacker-controlled input lives) cannot address the code section. You cannot scan executable pages for gadget sequences because they are in a completely separate address space managed by the runtime.
+- **No raw jumps**: All control flow uses `block`/`loop`/`if`/`br`/`br_table`. There is no `ret` instruction that pops an address off the stack.
+- **Opaque execution stack**: The WASM call stack is managed by the runtime, invisible to guest code. Buffer overflows in linear memory cannot overwrite return addresses because return addresses don't exist in linear memory.
+- **Typed indirect calls**: `call_indirect` validates function type signatures at runtime. You cannot redirect an indirect call to an arbitrary function.
+
+Even with arbitrary write within WASM linear memory, an attacker cannot hijack control flow. The attack surface shifts entirely to bugs in the WASM runtime itself.
+
+### Architecture: Split Design
+
+```
+Jerboa binary
+├── Chez runtime (orchestration, policy, configuration DSL)
+├── Rust native lib (crypto via ring, TLS via rustls)
+└── wasmi WASM interpreter (security-critical parsers)
+    ├── dns_parser.wasm    (DNS packet parsing)
+    ├── http_parser.wasm   (HTTP request parsing)
+    └── proto_fsm.wasm     (protocol state machines)
+```
+
+Security-critical parsing code is written in Rust, compiled to `.wasm`, and executed inside an interpreter-mode WASM runtime embedded in the Jerboa binary. The Chez/Jerboa layer handles orchestration, policy evaluation, and configuration.
+
+### WASM Runtime Options
+
+| Runtime | Binary overhead | Mode | Gadgets in runtime | Best for |
+|---------|----------------|------|-------------------|----------|
+| **wasm3** | ~100KB static | Interpreter | Near-zero (tiny loop) | Maximum minimality |
+| **WAMR** | ~50-85KB | Interpreter | Near-zero | Embedded/IoT |
+| **wasmi** | ~2-3MB as Rust lib | Interpreter | Near-zero | Pure Rust, integrates with jerboa-native-rs |
+| **Wasmtime** | ~15-20MB | JIT (Cranelift) | Some (JIT code) | Full WASI, best tooling |
+
+**Key insight**: Interpreter-mode runtimes generate **zero native code at runtime**. The entire attack surface is the statically-compiled interpreter loop. A wasm3 interpreter is ~3,000 lines of C. Compare that to the millions of gadget-contributing instructions in Chez's runtime.
+
+### Integration with jerboa-native-rs
+
+Add wasmi as a dependency in `jerboa-native-rs/Cargo.toml`. Expose FFI functions:
+
+```rust
+#[no_mangle]
+pub extern "C" fn jerboa_wasm_load(module_bytes: *const u8, len: usize) -> i32;
+
+#[no_mangle]
+pub extern "C" fn jerboa_wasm_call(
+    func_name: *const u8, name_len: usize,
+    input: *const u8, input_len: usize,
+    output: *mut u8, output_len: usize,
+    actual_len: *mut usize
+) -> i32;
+```
+
+Scheme code calls through `foreign-procedure`:
+
+```scheme
+(define wasm-call
+  (foreign-procedure "jerboa_wasm_call" (string int u8* int u8* int void*) int))
+```
+
+### Performance
+
+Interpreter-mode WASM is 10-100x slower than native. For a DNS server, the bottleneck is network I/O, not parsing compute. A DNS response parser running in wasm3 at 1/50th native speed still finishes in microseconds — acceptable for security-critical paths.
+
+### Existing WASM Infrastructure
+
+Jerboa already has embryonic WASM support in `lib/jerboa/wasm/`:
+- `codegen.sls` — Compile restricted Scheme subset to WASM binary (i32-only, no closures)
+- `runtime.sls` — Stack-based WASM interpreter for testing
+- `format.sls` — LEB128 encoding, section parsing, binary format primitives
+
+This is currently educational/testing-grade but the format layer is real and could serve as a foundation.
+
+---
+
+## Path 3: Minimal Interpreter Binary
+
+**Effort**: Months. **Impact**: Smallest possible footprint (~500KB).
+
+For tools where binary size and gadget count are the absolute priority, replace the Chez runtime entirely with a minimal Scheme interpreter.
+
+### s7 Scheme
+
+- Single C file, ~35,000 lines. Compiles to **~400KB**.
+- Interpreter-only: no JIT means no W+X pages, no dynamically generated gadgets.
+- Full access to C hardening flags: `-fcf-protection=full`, `-fstack-protector-strong`, etc.
+- R7RS-ish with extensions. Not Chez-compatible.
+- ~50-100x slower than Chez for compute-heavy code. Acceptable for I/O-bound servers.
+
+### Architecture
+
+```
+Static binary (~500KB-1MB)
+├── s7 Scheme interpreter (400KB, fully hardened C)
+├── Rust native lib (crypto, TLS, WASM runtime)
+└── Application logic in s7 Scheme
+```
+
+### Tradeoff
+
+This means **leaving Jerboa** for those specific tools. You'd be writing a different program in a different Scheme dialect. The Jerboa prelude, reader syntax, defstruct/defclass/defmethod, capability system, effects, contracts — none of it carries over.
+
+This path only makes sense if you're willing to write the security tools as standalone projects that don't depend on Jerboa's ecosystem. Jerboa could still be used for development, prototyping, and testing, with the production binary being a separate s7-based build.
+
+---
+
+## Approaches NOT Worth Pursuing
+
+### Scheme-to-Rust Transpilation
+
+Fundamental type system mismatch makes automated translation impossible:
+- **Continuations**: Chez's `call/cc` captures the native stack. Rust has no equivalent.
+- **Garbage collection**: Scheme values are GC-managed. Rust uses ownership. No mechanical translation exists.
+- **Dynamic typing**: Every Scheme value is a tagged pointer. Translating to Rust means `enum Value { Int(i64), String(Rc<String>), ... }` everywhere, losing Rust's type safety benefits.
+
+No tool exists for this. Building one would take years. The few academic efforts (Ribbit Scheme) produce minimal subsets lacking Jerboa's features.
+
+### Alternative Scheme Compilers (Chicken, Gambit)
+
+Chicken compiles Scheme to C via Cheney-on-the-MTA. A minimal program produces a ~400KB binary. Gambit is similar but larger (~1-2MB). Both give full access to C hardening flags.
+
+**What you lose**: Chez's native-code performance (3-10x slower), the entire Jerboa prelude, all MCP tooling, reader syntax extensions, defstruct/defclass/defmethod, the capability system, taint tracking, effects, contracts.
+
+Not worth it unless starting from scratch.
+
+### Full LTO Across Chez + Rust
+
+Not technically feasible. Chez's `libkernel.a` is pre-built; LTO would require recompiling Chez with `-flto`. Even then, cross-language LTO (C + Rust) is not supported by current toolchains for this combination.
+
+---
+
+## Recommended Architecture
+
+### Target: Self-Sandboxing Static PIE Binary
+
+The architecture adapts to platform capabilities. WASM-sandboxed parsing is the
+universal constant; OS-level confinement and hardware enforcement vary.
+
+```
++--------------------------------------------------+
+|  Static PIE ELF (~8MB)                           |
+|  Full ASLR + platform-specific HW enforcement    |
+|                                                   |
+|  +-----------+  +-----------+  +---------------+ |
+|  | Chez      |  | Rust      |  | wasmi WASM    | |
+|  | Runtime   |  | Native    |  | Interpreter   | |
+|  |           |  | (ring,    |  | (DNS parser   | |
+|  |           |  |  rustls)  |  |  in sandboxed | |
+|  |           |  |           |  |  WASM module) | |
+|  +-----------+  +-----------+  +---------------+ |
+|                                                   |
+|  Jerboa orchestration layer:                      |
+|  - Capability-gated network I/O                   |
+|  - Policy evaluation (who queries what)           |
+|  - Audit logging (hash-chain)                     |
+|  - Configuration via Jerboa DSL                   |
+|                                                   |
+|  Post-init self-sandbox (platform-dependent):     |
+|  +---------------------+------------------------+ |
+|  |      Linux          |      FreeBSD           | |
+|  +---------------------+------------------------+ |
+|  | CET/SHSTK (hw ROP)  | ARM PAC+BTI (arm64)   | |
+|  | seccomp (syscall     | Capsicum (resource     | |
+|  |   filter, block      |   confinement, block   | |
+|  |   mprotect(EXEC))    |   path-based access)   | |
+|  | Landlock (fs paths)  | PROTMAX (post-init,    | |
+|  | Namespaces (PID,     |   block mprotect       | |
+|  |   mount, network)    |   escalation)          | |
+|  | Anti-debug (ptrace)  | Anti-debug (ptrace)    | |
+|  | Integrity check      | Integrity check        | |
+|  +---------------------+------------------------+ |
++--------------------------------------------------+
+```
+
+### Defense Layers by Platform
+
+**Linux (x86_64 with CET)** — Six independent layers:
+
+1. **ASLR** (~22 bits entropy) — Information leak to discover gadget addresses
+2. **CET/SHSTK** — Hardware bypass to use corrupted return addresses
+3. **WASM sandbox** — Runtime bug to escape parser isolation
+4. **seccomp** — Kernel exploit to make blocked syscalls (including `mprotect(EXEC)`)
+5. **Namespaces** — Kernel exploit to escape PID/mount/network isolation
+6. **Landlock** — Kernel exploit to access blocked filesystem paths
+
+**FreeBSD (x86_64)** — Five layers, no hardware ROP protection:
+
+1. **ASLR** (~14 bits entropy) — Information leak (lower bar than Linux)
+2. **WASM sandbox** — Runtime bug to escape parser isolation (**primary ROP defense**)
+3. **Capsicum** — Kernel exploit to escape capability mode
+4. **PROTMAX** — Kernel exploit or pre-PROTMAX timing to escalate memory protections
+5. **Anti-debug + integrity** — Bypass tracing detection and hash verification
+
+**FreeBSD (arm64)** — Six layers, with hardware enforcement:
+
+1. **ASLR** — Information leak to discover gadget addresses
+2. **ARM PAC** — Hardware bypass to forge signed return addresses
+3. **ARM BTI** — Hardware bypass to jump to non-BTI-landing-pad instructions
+4. **WASM sandbox** — Runtime bug to escape parser isolation
+5. **Capsicum** — Kernel exploit to escape capability mode
+6. **PROTMAX** — Kernel exploit to escalate memory protections
+
+**Key insight**: On FreeBSD x86_64, the WASM sandbox is not defense-in-depth — it is the
+**primary** control-flow integrity mechanism. This elevates Path 2 from "nice to have" to
+"essential" on that platform. On FreeBSD arm64, PAC+BTI restore hardware-level protection,
+making it the strongest FreeBSD deployment target.
+
+### Platform Feature Matrix
+
+| Defense | Linux x86_64 | FreeBSD x86_64 | FreeBSD arm64 |
+|---------|-------------|----------------|---------------|
+| ASLR | High entropy | Low entropy | Low entropy |
+| HW return-addr protection | CET/SHSTK | **None** | ARM PAC |
+| HW indirect-branch protection | CET/IBT (needs Chez patch) | **None** | ARM BTI |
+| Syscall filtering | seccomp-bpf | **None** (Capsicum is resource-based) | **None** |
+| Block mprotect(EXEC) | seccomp | PROTMAX (post-init only) | PROTMAX (post-init only) |
+| Resource confinement | Landlock | Capsicum | Capsicum |
+| Process isolation | Namespaces | **None** (jails require root) | **None** |
+| WASM parser sandbox | Yes | Yes | Yes |
+| Anti-debug | ptrace self-trace | ptrace self-trace | ptrace self-trace |
+| Compiler hardening (CFLAGS) | Full | Full | Full |
+
+### Implementation Priority
+
+Priority is adjusted for cross-platform impact. Items that benefit both Linux and FreeBSD
+are ranked higher than Linux-only features.
+
+| Phase | Work | Effort | Linux | FreeBSD | Notes |
+|-------|------|--------|-------|---------|-------|
+| **1a** | Embed wasmi in jerboa-native-rs | Week | Defense-in-depth | **Primary ROP defense** | Cross-platform, highest priority |
+| **1b** | Write DNS parser in Rust → WASM | Week | Defense-in-depth | **Primary ROP defense** | Cross-platform |
+| **1c** | Switch musl build to static PIE | Hours | Full ASLR | Full ASLR | Cross-platform |
+| **1d** | Rebuild Chez with hardening CFLAGS | Days | All flags | All except CET | Cross-platform (flag subset varies) |
+| **2a** | Enable CET/SHSTK in Chez build | Days | **Defeats ROP** | N/A | Linux-only, highest HW impact |
+| **2b** | Add namespace isolation to cage | Days | Micro-VM containment | N/A | Linux-only |
+| **2c** | Enable PROTMAX post-init on FreeBSD | Days | N/A | Block mprotect escalation | FreeBSD-only, requires post-JIT sequencing |
+| **2d** | ARM PAC+BTI build target | Days | N/A | HW ROP defense (arm64) | FreeBSD arm64 only |
+| **3a** | Patch Chez codegen for ENDBR64 | Weeks | Full CET (IBT+SHSTK) | N/A | Linux-only, enables IBT |
+| **3b** | Encrypted boot files | Weeks | Resist static analysis | Resist static analysis | Cross-platform |
+| **3c** | Firecracker deployment wrapper | Weeks | VM-level isolation | N/A (bhyve possible) | Linux-primary |
+| **3d** | HardenedBSD deployment target | Days | N/A | Strict W^X, SEGVGUARD | FreeBSD fork, maximum hardening |