Harden generated Chez binary launchers

ober

1b6c47126ea7acc9012b862ff9fff7bd76b48053

diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index b8e8181..506a093 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -6195,4 +6195,26 @@
      "noninteractive")
    ("title"
      .
-     "Build jsh base without embed passphrase prompt")))
+     "Build jsh base without embed passphrase prompt"))
+ (("code"
+    .
+    ";; Pattern for Chez/Jerboa binary builders that generate a C launcher:\n;; 1. Derive flags from the target Chez machine type, not the build host.\n;; 2. Add launcher CFLAGS before compiling generated main.c / C shims:\n;;    Linux x86_64: -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fstack-clash-protection -fcf-protection=full\n;;    Linux arm64:  -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fstack-clash-protection -mbranch-protection=standard\n;;    ELF other:    -fstack-protector-strong -D_FORTIFY_SOURCE=2\n;; 3. Add ELF LDFLAGS on Linux/FreeBSD/OpenBSD/NetBSD:\n;;    -Wl,-z,relro,-z,now\n;;    and on Linux x86_64, when supported: -Wl,-z,ibt,-z,shstk\n;; 4. Add an optional Linux x86_64 C startup hook before Sscheme_init:\n;;    syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK)\n;;    Gate it behind JERBOA_ENABLE_SHSTK=1 or JERBOA_ENABLE_SHSTK=try.\n;; 5. Keep escape hatches:\n;;    JERBOA_BINARY_HARDEN=0 disables launcher hardening for debugging.\n;;    JERBOA_CET_ELF_NOTES=0 disables -z ibt/-z shstk for older linkers.") ("id" . "generated-binary-launcher-hardening") ("imports")
+   ("notes"
+     .
+     "Do not infer target hardening from the current host when cross-building. For jerbuild binary, use the csv-dir machine leaf when --csv-dir is supplied. Do not enable SHSTK by default for arbitrary Scheme programs until Chez continuation restoration is shadow-stack-aware; call/cc can violate SHSTK's LIFO assumptions.")
+   ("tags" "binary" "hardening" "CET" "SHSTK" "launcher"
+     "jerbuild")
+   ("title"
+     .
+     "Harden generated binary launchers for CET/BTI targets"))
+ (("code"
+    .
+    "#!chezscheme\n(import (chezscheme))\n\n(define source-path-placeholder \"<app>\")\n\n(define (sanitized-source-object sfd bfp efp)\n  (if (source-file-descriptor? sfd)\n      (make-source-object\n        (source-file-descriptor\n          source-path-placeholder\n          (source-file-descriptor-checksum sfd))\n        bfp\n        efp)\n      #f))\n\n(define (strip-fasl-metadata! path)\n  (when (file-exists? path)\n    (let ([tmp (format \"~a.stripped.~a\" path (get-process-id))])\n      (strip-fasl-file path tmp\n        (fasl-strip-options inspector-source\n                            profile-source\n                            source-annotations))\n      (rename-file tmp path))))\n\n(current-make-source-object sanitized-source-object)\n(debug-level 0)\n(generate-inspector-information #f)\n(generate-procedure-source-information #f)\n(enable-error-source-expression #f)\n\n(compile-program \"main.ss\")\n(strip-fasl-metadata! \"main.so\")") ("id" . "sanitize-chez-source-paths-in-release-builds")
+   ("imports" "(chezscheme)")
+   ("notes"
+     .
+     "Use source-file-descriptor, not make-source-file-descriptor, when constructing a sanitized source file descriptor from an existing checksum. In Chez 10.4, make-source-file-descriptor expects a binary input port as its second argument and will reject a checksum integer. strip(1) cannot remove source paths stored in FASL/boot data; use current-make-source-object before compilation and strip-fasl-file after compile-program/compile-library/make-boot-file. Omit compile-time-information stripping unless you are certain the resulting FASL/boot will not need visit-time macro information later.")
+   ("tags" "chezscheme" "source-paths"
+     "current-make-source-object" "fasl" "release-build"
+     "strip-fasl-file")
+   ("title" . "Sanitize Chez source paths in release builds")))
diff --git a/data/error-fixes.sexp b/data/error-fixes.sexp
index a79cc38..2d3e099 100644
--- a/data/error-fixes.sexp
+++ b/data/error-fixes.sexp
@@ -2283,4 +2283,41 @@
    ("pattern"
      .
      "export form outside of a module or library .*src/jsh/ffi\\.ss.*while verifying.*jerboa-src/src/jsh/.*\\.ss")
-   ("type" . "module-resolution")))
+   ("type" . "module-resolution"))
+ (("code_example"
+    .
+    "make chez\nPATH=\"$PWD/.chez/bin:$PATH\" scheme --libdirs lib --script tests/test-core.ss")
+   ("explanation"
+     .
+     "MCP verifier tools shell out to `scheme`. In a fresh checkout or client environment, the repo may not yet have `.chez/bin/scheme`, and the MCP process may not inherit a PATH containing any Scheme executable.")
+   ("fix"
+     .
+     "Install or bootstrap Chez so `scheme` is on PATH, or run the repo build target that creates `.chez/bin/scheme` before using MCP verifier tools. If verifying manually, use the repo-local binary path after bootstrap, e.g. `.chez/bin/scheme --libdirs lib --script <file>.ss`.")
+   ("id" . "scheme-command-not-found-verifier")
+   ("pattern" . "/bin/sh: scheme: command not found")
+   ("type" . "tooling"))
+ (("explanation"
+    .
+    "The file-wide MCP verifier can raise this reader/expander error for jerbuild.ss while the repository's native binary build compiles the same source successfully. Avoid random edits to unrelated defstruct forms; use the repo build as the authoritative check.")
+   ("fix"
+     .
+     "When this exact error appears from jerboa_verify on the full jerbuild.ss file, first validate with the real build path (`make binary` on non-Linux hosts or `make docker-build` on Linux) before editing source. If the build passes, treat it as a verifier limitation around file-wide expansion rather than a proven malformed defstruct.")
+   ("id" . "jerbuild-defstruct-verifier-cadr")
+   ("pattern"
+     .
+     "Exception in cadr: incorrect list structure (defstruct)")
+   ("type" . "verifier-limitation"))
+ (("code_example"
+    .
+    "(current-make-source-object\n  (lambda (sfd bfp efp)\n    (make-source-object\n      (source-file-descriptor \"<app>\" (source-file-descriptor-checksum sfd))\n      bfp\n      efp)))")
+   ("explanation"
+     .
+     "Chez's make-source-file-descriptor signature is (make-source-file-descriptor obj binary-input-port [reset?]). The source-file-descriptor procedure constructs an SFD directly from a path object and checksum, which is the correct API when sanitizing source objects with current-make-source-object.")
+   ("fix"
+     .
+     "Do not pass a checksum integer to make-source-file-descriptor. For an existing checksum, use (source-file-descriptor path checksum). Use make-source-file-descriptor only when you have a binary input port and want Chez to compute the checksum.")
+   ("id" . "make-source-file-descriptor-checksum-binary-port")
+   ("pattern"
+     .
+     "make-source-file-descriptor: .* is not a binary input port")
+   ("type" . "arity/api")))
diff --git a/docs/chez-hardening.md b/docs/chez-hardening.md
index 8b7c946..b7d2c48 100644
--- a/docs/chez-hardening.md
+++ b/docs/chez-hardening.md
@@ -326,6 +326,24 @@ Don't enable SHSTK by default in the Chez kernel. Instead:
    trivially safe: enable in `main` before any Scheme code runs.
 5. **For programs that do use `call/cc`** — punt until Phase 3.4.
 
+**Implemented for generated binaries:** the Jerboa binary launchers now
+include an x86_64 Linux startup hook before `Sscheme_init`. It is opt-in:
+
+```bash
+JERBOA_ENABLE_SHSTK=1   ./dist/jerboa ...
+JERBOA_ENABLE_SHSTK=try ./dist/jerboa ...  # continue if unsupported
+```
+
+This covers `make binary`, `make jerboa`, standalone `jerbuild`,
+`jerbuild binary`, `make jmcp`, and native `make jlsp`. The hook is deliberately
+not enabled by default until the continuation-aware kernel work in 6.4 lands.
+
+The same launcher build paths also apply target-aware hardening flags:
+Linux x86_64 gets `-fcf-protection=full` and, by default,
+`-Wl,-z,ibt,-z,shstk`; Linux arm64 gets `-mbranch-protection=standard`; ELF
+targets get `-Wl,-z,relro,-z,now`. Use `JERBOA_BINARY_HARDEN=0` for debugging
+or `JERBOA_CET_ELF_NOTES=0` for an older linker that lacks CET note support.
+
 ### 6.4 Future Work: Continuation-Aware Shadow-Stack Management
 
 The principled fix is to extend the Chez kernel's continuation
@@ -460,7 +478,7 @@ Recommended sequence to land:
    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
+4. **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
    SHSTK / PAC safe under `call/cc`.
diff --git a/docs/secure.md b/docs/secure.md
index 3163114..a25132a 100644
--- a/docs/secure.md
+++ b/docs/secure.md
@@ -44,11 +44,11 @@ This **directly defeats classical ROP**, which fundamentally relies on corrupted
 
 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.
+**Chez compatibility**: Supported in Jerboa's vendored Chez. The x86_64 code generator emits `ENDBR64` at Scheme function entries and foreign-callable prologues, so compiled Scheme entry points satisfy IBT landing-pad rules.
 
-**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.
+**Binary requirements**: Build Chez and generated launchers with CET flags, and link Linux x86_64 binaries with CET-aware ELF notes. Jerboa's binary builders do this by default for Linux x86_64; set `JERBOA_CET_ELF_NOTES=0` only when diagnosing an older linker.
 
-**Alternative**: Enable SHSTK-only mode via `prctl(PR_SET_SHADOW_STACK, ...)` without IBT. Gets return-address protection without the indirect-branch requirement.
+**Runtime SHSTK**: Because Chez continuations can violate shadow-stack LIFO assumptions, runtime SHSTK is explicit: run binaries with `JERBOA_ENABLE_SHSTK=1` to require it, or `JERBOA_ENABLE_SHSTK=try` to enable it when the CPU/kernel support it and continue otherwise.
 
 ### Standard Hardening CFLAGS
 
@@ -71,6 +71,13 @@ 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.
 
+**Status: DONE in generated binary launchers** — `make binary`, `make jerboa`,
+standalone `jerbuild`, `jerbuild binary`, `make jmcp`, and native `make jlsp`
+now apply target-aware C/link hardening to their launcher C code. Linux x86_64
+gets CET CFLAGS plus `-z ibt`/`-z shstk` ELF notes by default; Linux arm64 gets
+`-mbranch-protection=standard`; ELF targets get RELRO/NOW. Set
+`JERBOA_BINARY_HARDEN=0` to disable launcher hardening for debugging.
+
 **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)`.
diff --git a/jerbuild.ss b/jerbuild.ss
index a2c6365..f504f0c 100644
--- a/jerbuild.ss
+++ b/jerbuild.ss
@@ -1685,6 +1685,77 @@
       [(string-ends-with? s "nt")  "Windows"]
       [else                        "Unknown"])))
 
+(define (env-enabled? name default)
+  (let ([v (getenv name)])
+    (cond
+      [(not v) default]
+      [(or (string=? v "")
+           (string=? v "0")
+           (string=? v "false")
+           (string=? v "False")
+           (string=? v "FALSE")
+           (string=? v "no")
+           (string=? v "No")
+           (string=? v "NO")
+           (string=? v "off")
+           (string=? v "Off")
+           (string=? v "OFF"))
+       #f]
+      [else #t])))
+
+(define (linux-machine-type? mt)
+  (string-ends-with? mt "le"))
+
+(define (x86-64-machine-type? mt)
+  (or (string-starts-with? mt "ta6")
+      (string-starts-with? mt "a6")))
+
+(define (arm64-machine-type? mt)
+  (or (string-starts-with? mt "tarm64")
+      (string-starts-with? mt "arm64")))
+
+(define (target-hardening-cflags mt)
+  (cond
+    [(or (string-ends-with? mt "le")
+         (string-ends-with? mt "fb")
+         (string-ends-with? mt "ob")
+         (string-ends-with? mt "nb"))
+     (string-append
+       " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
+       (if (linux-machine-type? mt) " -fstack-clash-protection" "")
+       (cond
+         [(and (linux-machine-type? mt) (x86-64-machine-type? mt))
+          " -fcf-protection=full"]
+         [(and (linux-machine-type? mt) (arm64-machine-type? mt))
+          " -mbranch-protection=standard"]
+         [else ""]))]
+    [else ""]))
+
+(define (target-hardening-ldflags mt)
+  (cond
+    [(or (string-ends-with? mt "le")
+         (string-ends-with? mt "fb")
+         (string-ends-with? mt "ob")
+         (string-ends-with? mt "nb"))
+     (string-append
+       " -Wl,-z,relro,-z,now"
+       (if (and (linux-machine-type? mt)
+                (x86-64-machine-type? mt)
+                (env-enabled? "JERBOA_CET_ELF_NOTES" #t))
+           " -Wl,-z,ibt,-z,shstk"
+           ""))]
+    [else ""]))
+
+(define (binary-hardening-cflags mt)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (target-hardening-cflags mt)
+      ""))
+
+(define (binary-hardening-ldflags mt)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (target-hardening-ldflags mt)
+      ""))
+
 (define (selector->string selector)
   (cond
     [(symbol? selector) (symbol->string selector)]
@@ -1950,19 +2021,56 @@
 (define *binary-main-c-template*
   ;; Generated main.c for the standalone binary. Mirrors the structure of
   ;; support/build-binary.sh's CMAIN block.
-  "#include \"scheme.h\"
+  "#define _GNU_SOURCE
+#include \"scheme.h\"
+#include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <sys/types.h>
+#if defined(__linux__) && defined(__x86_64__)
+#include <sys/syscall.h>
+#endif
 
 #include \"petite_boot.h\"
 #include \"scheme_boot.h\"
 #include \"program_boot.h\"
 #include \"ffi_symbols.h\"
 
+#if defined(__linux__) && defined(__x86_64__)
+#ifndef ARCH_SHSTK_ENABLE
+#define ARCH_SHSTK_ENABLE 0x5001
+#endif
+#ifndef ARCH_SHSTK_SHSTK
+#define ARCH_SHSTK_SHSTK (1ULL << 0)
+#endif
+
+static int env_flag_enabled(const char *v) {
+    return v && *v &&
+           strcmp(v, \"0\") != 0 &&
+           strcmp(v, \"false\") != 0 &&
+           strcmp(v, \"FALSE\") != 0 &&
+           strcmp(v, \"no\") != 0 &&
+           strcmp(v, \"NO\") != 0 &&
+           strcmp(v, \"off\") != 0 &&
+           strcmp(v, \"OFF\") != 0;
+}
+
+static void maybe_enable_shadow_stack(void) {
+    const char *v = getenv(\"JERBOA_ENABLE_SHSTK\");
+    if (!env_flag_enabled(v)) return;
+    if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {
+        if (strcmp(v, \"try\") == 0) return;
+        fprintf(stderr, \"jerboa: ARCH_SHSTK_ENABLE failed: %s\\n\", strerror(errno));
+        exit(127);
+    }
+}
+#else
+static void maybe_enable_shadow_stack(void) { }
+#endif
+
 static const char *write_program_tmpfile(void) {
     static char path[] = \"/tmp/jerboa-prog-XXXXXX\";
     int fd = mkstemp(path);
@@ -1976,6 +2084,8 @@ static const char *write_program_tmpfile(void) {
 }
 
 int main(int argc, const char *argv[]) {
+    maybe_enable_shadow_stack();
+
     Sscheme_init(NULL);
     Sregister_boot_file_bytes(\"petite\",
         (void *)petite_boot_data, petite_boot_size);
@@ -2107,7 +2217,7 @@ int main(int argc, const char *argv[]) {
         (for-each (lambda (a) (printf "      -> ~a\n" a)) archives)
         archives))))
 
-(define (compile-c-source src obj-dir cc csv-dir index extra-cflags)
+(define (compile-c-source src obj-dir cc csv-dir harden-cflags index extra-cflags)
   ;; Compile src to obj-dir/extra-<index>-<basename-without-.c>.o.
   ;; extra-cflags: string (raw, appended after -O2) or #f.
   (let* ([base (path-basename src)]
@@ -2116,9 +2226,10 @@ int main(int argc, const char *argv[]) {
                  base)]
          [out  (format "~a/extra-~a-~a.o" obj-dir index stem)]
          [cflags (if extra-cflags (string-append " " extra-cflags) "")]
-         [cmd  (format "~a -I~a -O2~a -c ~a -o ~a"
+         [cmd  (format "~a -I~a -O2~a~a -c ~a -o ~a"
                        cc
                        (shell-quote csv-dir)
+                       harden-cflags
                        cflags
                        (shell-quote src)
                        (shell-quote out))])
@@ -2297,7 +2408,12 @@ int main(int argc, const char *argv[]) {
          [petite-boot (format "~a/petite.boot" csv-dir)]
          [scheme-boot (format "~a/scheme.boot" csv-dir)]
          [libkernel   (format "~a/libkernel.a" csv-dir)]
-         [scheme-h    (format "~a/scheme.h" csv-dir)])
+         [scheme-h    (format "~a/scheme.h" csv-dir)]
+         [link-mt     (if csv-dir-override
+                        (path-basename csv-dir-override)
+                        (symbol->string mt))]
+         [harden-cflags (binary-hardening-cflags link-mt)]
+         [harden-ldflags (binary-hardening-ldflags link-mt)])
     (define static-native-archive #f)
     (define static-native-symbols '())
 
@@ -2368,6 +2484,9 @@ int main(int argc, const char *argv[]) {
       (printf "    CC:     ~a\n" cc)
       (printf "    Chez:   ~a~a\n" csv-dir
               (if csv-dir-override " (override)" ""))
+      (unless (and (string=? harden-cflags "") (string=? harden-ldflags ""))
+        (printf "    Harden: CFLAGS='~a' LDFLAGS='~a'\n"
+                harden-cflags harden-ldflags))
       (when rust-target
         (printf "    Rust target: ~a\n" rust-target))
       (when xpatch
@@ -2459,7 +2578,8 @@ int main(int argc, const char *argv[]) {
                         (loop (cdr srcs)
                               (+ i 1)
                               (cons (compile-c-source
-                                      (car sp) obj-dir cc csv-dir i (cdr sp))
+                                      (car sp) obj-dir cc csv-dir
+                                      harden-cflags i (cdr sp))
                                     acc)))]))])])
 
         (printf "==> [3/5] Embed boots + program as C arrays\n")
@@ -2521,13 +2641,14 @@ int main(int argc, const char *argv[]) {
                              "")))
                        '("liblz4.a" "libz.a")))]
                ;; Link order: main.c -> user .o -> Rust .a -> extra .a ->
-               ;; libkernel -> lz4/z -> extra ldflags -> OS libs.
+               ;; libkernel -> lz4/z -> hardening -> extra ldflags -> OS libs.
                ;; Anything that uses Scheme_* symbols must come before libkernel;
                ;; anything libkernel needs (lz4/z, ncurses) must come after.
-               [cc-cmd (format "~a -I~a -I~a -O2 -o ~a ~a~a~a~a ~a~a~a ~a"
+               [cc-cmd (format "~a -I~a -I~a -O2~a -o ~a ~a~a~a~a ~a~a~a~a ~a"
                                cc
                                (shell-quote obj-dir)
                                (shell-quote csv-dir)
+                               harden-cflags
                                (shell-quote output)
                                (shell-quote main-c)
                                (join-quoted user-objs)
@@ -2535,6 +2656,7 @@ int main(int argc, const char *argv[]) {
                                (join-quoted extra-archives)
                                (shell-quote libkernel)
                                chez-archives
+                               harden-ldflags
                                (join-raw extra-ldflags)
                                (or os-libs-override
                                    (let ([f (format "~a/os-libs" csv-dir)])
diff --git a/lsp/build-binary.ss b/lsp/build-binary.ss
index 55b2bb6..d1df4d0 100644
--- a/lsp/build-binary.ss
+++ b/lsp/build-binary.ss
@@ -46,8 +46,79 @@
     (and (>= slen suflen)
          (string=? suffix (substring s (- slen suflen) slen)))))
 
+(define (string-has-prefix? s prefix)
+  (let ((slen (string-length s))
+        (plen (string-length prefix)))
+    (and (>= slen plen)
+         (string=? prefix (substring s 0 plen)))))
+
+(define (env-enabled? name default)
+  (let ((v (getenv name)))
+    (cond
+      ((not v) default)
+      ((or (string=? v "")
+           (string=? v "0")
+           (string=? v "false")
+           (string=? v "False")
+           (string=? v "FALSE")
+           (string=? v "no")
+           (string=? v "No")
+           (string=? v "NO")
+           (string=? v "off")
+           (string=? v "Off")
+           (string=? v "OFF"))
+       #f)
+      (else #t))))
+
+(define machine-string (symbol->string (machine-type)))
+
+(define (linux-machine? mt)
+  (string-has-suffix? mt "le"))
+
+(define (x86-64-machine? mt)
+  (or (string-has-prefix? mt "ta6")
+      (string-has-prefix? mt "a6")))
+
+(define (arm64-machine? mt)
+  (or (string-has-prefix? mt "tarm64")
+      (string-has-prefix? mt "arm64")))
+
+(define (binary-hardening-cflags mt)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (cond
+        ((or (string-has-suffix? mt "le")
+             (string-has-suffix? mt "fb")
+             (string-has-suffix? mt "ob")
+             (string-has-suffix? mt "nb"))
+         (string-append
+           " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
+           (if (linux-machine? mt) " -fstack-clash-protection" "")
+           (cond
+             ((and (linux-machine? mt) (x86-64-machine? mt)) " -fcf-protection=full")
+             ((and (linux-machine? mt) (arm64-machine? mt)) " -mbranch-protection=standard")
+             (else ""))))
+        (else ""))
+      ""))
+
+(define (binary-hardening-ldflags mt)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (cond
+        ((or (string-has-suffix? mt "le")
+             (string-has-suffix? mt "fb")
+             (string-has-suffix? mt "ob")
+             (string-has-suffix? mt "nb"))
+         (string-append
+           " -Wl,-z,relro,-z,now"
+           (if (and (linux-machine? mt)
+                    (x86-64-machine? mt)
+                    (env-enabled? "JERBOA_CET_ELF_NOTES" #t))
+               " -Wl,-z,ibt,-z,shstk"
+               "")))
+        (else ""))
+      ""))
+
 (define macos?
-  (string-has-suffix? (symbol->string (machine-type)) "osx"))
+  (string-has-suffix? machine-string "osx"))
 
 (define termux?
   (and (getenv "PREFIX")
@@ -227,17 +298,44 @@
   (lambda (out)
     (fprintf out "/* Auto-generated — do not edit */\n")
     (fprintf out "#define _GNU_SOURCE\n")
+    (fprintf out "#include <errno.h>\n")
     (fprintf out "#include <stdlib.h>\n")
     (fprintf out "#include <stdio.h>\n")
     (fprintf out "#include <string.h>\n")
     (fprintf out "#include <unistd.h>\n")
+    (fprintf out "#if defined(__linux__) && defined(__x86_64__)\n")
+    (fprintf out "#include <sys/syscall.h>\n")
+    (fprintf out "#endif\n")
     (fprintf out "#include \"scheme.h\"\n")
     (fprintf out "#include \"jerboa_lsp_petite_boot.h\"\n")
     (fprintf out "#include \"jerboa_lsp_scheme_boot.h\"\n")
     (fprintf out "#include \"jerboa_lsp_boot.h\"\n")
     (fprintf out "#include \"jerboa_lsp_program.h\"\n")
     (fprintf out "\n")
+    (fprintf out "#if defined(__linux__) && defined(__x86_64__)\n")
+    (fprintf out "#ifndef ARCH_SHSTK_ENABLE\n")
+    (fprintf out "#define ARCH_SHSTK_ENABLE 0x5001\n")
+    (fprintf out "#endif\n")
+    (fprintf out "#ifndef ARCH_SHSTK_SHSTK\n")
+    (fprintf out "#define ARCH_SHSTK_SHSTK (1ULL << 0)\n")
+    (fprintf out "#endif\n")
+    (fprintf out "static int env_flag_enabled(const char *v) {\n")
+    (fprintf out "  return v && *v && strcmp(v, \"0\") && strcmp(v, \"false\") && strcmp(v, \"FALSE\") && strcmp(v, \"no\") && strcmp(v, \"NO\") && strcmp(v, \"off\") && strcmp(v, \"OFF\");\n")
+    (fprintf out "}\n")
+    (fprintf out "static void maybe_enable_shadow_stack(void) {\n")
+    (fprintf out "  const char *v = getenv(\"JERBOA_ENABLE_SHSTK\");\n")
+    (fprintf out "  if (!env_flag_enabled(v)) return;\n")
+    (fprintf out "  if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {\n")
+    (fprintf out "    if (strcmp(v, \"try\") == 0) return;\n")
+    (fprintf out "    fprintf(stderr, \"jlsp: ARCH_SHSTK_ENABLE failed: %s\\n\", strerror(errno));\n")
+    (fprintf out "    exit(127);\n")
+    (fprintf out "  }\n")
+    (fprintf out "}\n")
+    (fprintf out "#else\n")
+    (fprintf out "static void maybe_enable_shadow_stack(void) { }\n")
+    (fprintf out "#endif\n\n")
     (fprintf out "int main(int argc, char *argv[]) {\n")
+    (fprintf out "  maybe_enable_shadow_stack();\n")
     (fprintf out "  /* Extract program .so to a temp file */\n")
     (fprintf out "  char prog_path[256];\n")
     (fprintf out "  const char *tmpdir = getenv(\"TMPDIR\");\n")
@@ -279,13 +377,16 @@
       ;; Linux
       "-lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses")))
 
-(let ((cc (or (getenv "CC") "cc")))
-  (let ((rc (system (format "~a -c -I~a -o jerboa-lsp-main.o jerboa-lsp-main.c" cc chez-dir))))
+(let ((cc (or (getenv "CC") "cc"))
+      (harden-cflags (binary-hardening-cflags machine-string))
+      (harden-ldflags (binary-hardening-ldflags machine-string)))
+  (let ((rc (system (format "~a -c -I~a -O2~a -o jerboa-lsp-main.o jerboa-lsp-main.c"
+                            cc chez-dir harden-cflags))))
     (unless (= rc 0)
       (printf "Error: C compilation failed~n")
       (exit 1)))
-  (let ((rc (system (format "~a -o jlsp jerboa-lsp-main.o -L~a ~a"
-                            cc chez-dir link-libs))))
+  (let ((rc (system (format "~a -o jlsp jerboa-lsp-main.o -L~a~a ~a"
+                            cc chez-dir harden-ldflags link-libs))))
     (unless (= rc 0)
       (printf "Error: linking failed~n")
       (exit 1))))
diff --git a/mcp/build-jmcp.sh b/mcp/build-jmcp.sh
index b9cc711..c8af18e 100755
--- a/mcp/build-jmcp.sh
+++ b/mcp/build-jmcp.sh
@@ -120,20 +120,107 @@ cat > "$builder" <<'SCHEME'
       'replace)
     (printf "    embed ~a (~a bytes)~n" input-path size)))
 
+(define (env-enabled? name default)
+  (let ([v (getenv name)])
+    (cond
+      [(not v) default]
+      [(or (string=? v "")
+           (string=? v "0")
+           (string=? v "false")
+           (string=? v "False")
+           (string=? v "FALSE")
+           (string=? v "no")
+           (string=? v "No")
+           (string=? v "NO")
+           (string=? v "off")
+           (string=? v "Off")
+           (string=? v "OFF"))
+       #f]
+      [else #t])))
+
+(define (linux-x86-64-machine? target-os machine)
+  (and (string=? target-os "linux")
+       (or (string-prefix? "ta6" machine)
+           (string-prefix? "a6" machine))))
+
+(define (linux-arm64-machine? target-os machine)
+  (and (string=? target-os "linux")
+       (or (string-prefix? "tarm64" machine)
+           (string-prefix? "arm64" machine))))
+
+(define (target-hardening-cflags target-os machine)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (cond
+        [(or (string=? target-os "linux")
+             (string=? target-os "freebsd")
+             (string=? target-os "openbsd")
+             (string=? target-os "netbsd"))
+         (string-append
+           " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
+           (if (string=? target-os "linux") " -fstack-clash-protection" "")
+           (cond
+             [(linux-x86-64-machine? target-os machine) " -fcf-protection=full"]
+             [(linux-arm64-machine? target-os machine)  " -mbranch-protection=standard"]
+             [else ""]))]
+        [else ""])
+      ""))
+
+(define (target-hardening-ldflags target-os machine)
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (cond
+        [(or (string=? target-os "linux")
+             (string=? target-os "freebsd")
+             (string=? target-os "openbsd")
+             (string=? target-os "netbsd"))
+         (string-append
+           " -Wl,-z,relro,-z,now"
+           (if (and (linux-x86-64-machine? target-os machine)
+                    (env-enabled? "JERBOA_CET_ELF_NOTES" #t))
+               " -Wl,-z,ibt,-z,shstk"
+               ""))]
+        [else ""])
+      ""))
+
 (define (emit-main-c path static?)
   (call-with-output-file path
     (lambda (out)
       (display "/* Generated by tools/build-jmcp-binary.sh. */\n" out)
       (display "#define _GNU_SOURCE\n" out)
+      (display "#include <errno.h>\n" out)
       (display "#include <stdlib.h>\n" out)
       (display "#include <stdio.h>\n" out)
       (display "#include <string.h>\n" out)
       (display "#include <unistd.h>\n" out)
       (display "#include <signal.h>\n" out)
+      (display "#if defined(__linux__) && defined(__x86_64__)\n" out)
+      (display "#include <sys/syscall.h>\n" out)
+      (display "#endif\n" out)
       (display "#include \"scheme.h\"\n" out)
       (display "#include \"petite_boot.h\"\n" out)
       (display "#include \"scheme_boot.h\"\n" out)
       (display "#include \"jmcp_program.h\"\n\n" out)
+      (display "#if defined(__linux__) && defined(__x86_64__)\n" out)
+      (display "#ifndef ARCH_SHSTK_ENABLE\n" out)
+      (display "#define ARCH_SHSTK_ENABLE 0x5001\n" out)
+      (display "#endif\n" out)
+      (display "#ifndef ARCH_SHSTK_SHSTK\n" out)
+      (display "#define ARCH_SHSTK_SHSTK (1ULL << 0)\n" out)
+      (display "#endif\n" out)
+      (display "static int env_flag_enabled(const char *v) {\n" out)
+      (display "  return v && *v && strcmp(v, \"0\") && strcmp(v, \"false\") && strcmp(v, \"FALSE\") && strcmp(v, \"no\") && strcmp(v, \"NO\") && strcmp(v, \"off\") && strcmp(v, \"OFF\");\n" out)
+      (display "}\n" out)
+      (display "static void maybe_enable_shadow_stack(void) {\n" out)
+      (display "  const char *v = getenv(\"JERBOA_ENABLE_SHSTK\");\n" out)
+      (display "  if (!env_flag_enabled(v)) return;\n" out)
+      (display "  if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {\n" out)
+      (display "    if (strcmp(v, \"try\") == 0) return;\n" out)
+      (display "    fprintf(stderr, \"jmcp: ARCH_SHSTK_ENABLE failed: %s\\n\", strerror(errno));\n" out)
+      (display "    exit(127);\n" out)
+      (display "  }\n" out)
+      (display "}\n" out)
+      (display "#else\n" out)
+      (display "static void maybe_enable_shadow_stack(void) { }\n" out)
+      (display "#endif\n\n" out)
       (when static?
         (display "void *dlopen(const char *f, int flags) { (void)flags; return f ? NULL : (void*)1; }\n" out)
         (display "void *dlsym(void *h, const char *s) { (void)h; (void)s; return NULL; }\n" out)
@@ -160,6 +247,7 @@ cat > "$builder" <<'SCHEME'
       (display "  return 0;\n" out)
       (display "}\n\n" out)
       (display "int main(int argc, char *argv[]) {\n" out)
+      (display "  maybe_enable_shadow_stack();\n" out)
       (display "  setenv(\"JERBOA_STATIC\", \"1\", 1);\n" out)
       (display "  char prog_path[1024];\n" out)
       (display "  if (write_program(prog_path, sizeof(prog_path)) != 0) return 1;\n" out)
@@ -240,24 +328,26 @@ cat > "$builder" <<'SCHEME'
                  (loop (read in) #f)]))))
         'replace))))
 
-(define (default-link-command cc target-os static? chez-dir output main-c)
-  (let ([ccq (or cc "cc")]
-        [outq (shell-quote output)]
-        [mainq (shell-quote main-c)]
-        [chezq (shell-quote chez-dir)])
+(define (default-link-command cc target-os machine static? chez-dir output main-c)
+  (let* ([ccq (or cc "cc")]
+         [outq (shell-quote output)]
+         [mainq (shell-quote main-c)]
+         [chezq (shell-quote chez-dir)]
+         [harden-cflags (target-hardening-cflags target-os machine)]
+         [harden-ldflags (target-hardening-ldflags target-os machine)])
     (cond
       [(and static? (string=? target-os "linux"))
-       (format "~a -O2 -static -Wl,--export-dynamic -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -Wl,--defsym=_dl_find_object=0 -lm -ldl -lpthread"
-               ccq chezq outq mainq chezq chezq chezq)]
+       (format "~a -O2~a -static -Wl,--export-dynamic -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a~a -Wl,--defsym=_dl_find_object=0 -lm -ldl -lpthread"
+               ccq harden-cflags chezq outq mainq chezq chezq chezq harden-ldflags)]
       [(string=? target-os "freebsd")
-       (format "~a -O2 -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -lthr -lutil -lncurses"
-               ccq chezq outq mainq chezq chezq chezq)]
+       (format "~a -O2~a -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a~a -lm -lthr -lutil -lncurses"
+               ccq harden-cflags chezq outq mainq chezq chezq chezq harden-ldflags)]
       [(string=? target-os "macos")
        (format "~a -O2 -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -lpthread -lncurses -liconv"
                ccq chezq outq mainq chezq chezq chezq)]
       [else
-       (format "~a -O2 -I~a -o ~a ~a -L~a -lkernel -llz4 -lz -lm -ldl -lpthread -lncurses"
-               ccq chezq outq mainq chezq)])))
+       (format "~a -O2~a -I~a -o ~a ~a -L~a -lkernel -llz4 -lz~a -lm -ldl -lpthread -lncurses"
+               ccq harden-cflags chezq outq mainq chezq harden-ldflags)])))
 
 (define repo-dir (current-directory))
 (define entry (env "JMCP_ENTRY" (format "~a/bin/jerboa-mcp.ss" repo-dir)))
@@ -347,7 +437,7 @@ cat > "$builder" <<'SCHEME'
 (printf "==> link~n")
 (let* ([cc (env "CC" "cc")]
        [cmd (or (getenv "JMCP_LINK_COMMAND")
-                (default-link-command cc target-os static? chez-dir output main-c))])
+                (default-link-command cc target-os machine static? chez-dir output main-c))])
   (run cmd))
 
 (printf "~n=== jmcp binary ready: ~a ===~n" output)
diff --git a/support/build-binary.sh b/support/build-binary.sh
index a4c6c5e..412deee 100755
--- a/support/build-binary.sh
+++ b/support/build-binary.sh
@@ -105,6 +105,49 @@ CC="${CC:-$CC_DEFAULT}"
 # Allow caller to override link libs entirely (musl-static, etc.).
 OS_LIBS="${OS_LIBS_OVERRIDE:-$OS_LIBS}"
 
+flag_enabled() {
+    case "${1:-}" in
+        ""|0|false|False|FALSE|no|No|NO|off|Off|OFF) return 1 ;;
+        *) return 0 ;;
+    esac
+}
+
+target_hardening_cflags() {
+    target_os="$1"
+    machine="$2"
+    case "$target_os" in
+        Linux|FreeBSD|OpenBSD|NetBSD)
+            flags="-fstack-protector-strong -D_FORTIFY_SOURCE=2"
+            case "$target_os" in
+                Linux) flags="$flags -fstack-clash-protection" ;;
+            esac
+            case "$target_os:$machine" in
+                Linux:*a6*)    flags="$flags -fcf-protection=full" ;;
+                Linux:*arm64*) flags="$flags -mbranch-protection=standard" ;;
+            esac
+            printf '%s' "$flags"
+            ;;
+        *) printf '%s' "" ;;
+    esac
+}
+
+target_hardening_ldflags() {
+    target_os="$1"
+    machine="$2"
+    case "$target_os" in
+        Linux|FreeBSD|OpenBSD|NetBSD)
+            flags="-Wl,-z,relro,-z,now"
+            if [ "$target_os" = Linux ] && flag_enabled "${JERBOA_CET_ELF_NOTES:-1}"; then
+                case "$machine" in
+                    *a6*) flags="$flags -Wl,-z,ibt,-z,shstk" ;;
+                esac
+            fi
+            printf '%s' "$flags"
+            ;;
+        *) printf '%s' "" ;;
+    esac
+}
+
 STATIC_CFLAGS=""
 if [ -n "$BINARY_STATIC_ENV" ] && [ "$BINARY_STATIC_ENV" != 0 ]; then
     STATIC_CFLAGS="-DJERBOA_BINARY_STATIC_ENV=1"
@@ -122,6 +165,13 @@ EOF
     CSV_SEARCH_PREFIX="$JERBOA_CHEZ_PREFIX/lib"
 fi
 
+HARDEN_CFLAGS=""
+HARDEN_LDFLAGS=""
+if flag_enabled "${JERBOA_BINARY_HARDEN:-1}"; then
+    HARDEN_CFLAGS=$(target_hardening_cflags "$TARGET_OS" "$MACHINE_TYPE")
+    HARDEN_LDFLAGS=$(target_hardening_ldflags "$TARGET_OS" "$MACHINE_TYPE")
+fi
+
 # ── Find the Chez install dir (libkernel.a + scheme.h + boot files) ──────────
 CSV_DIR=""
 for prefix in "$CSV_SEARCH_PREFIX" /usr/local/lib /usr/lib /usr/lib64 /opt/homebrew/lib /opt/local/lib; do
@@ -150,6 +200,9 @@ fi
 echo "    CC:     $CC"
 echo "    Chez:   $CSV_DIR"
 echo "    Libs:   $BINARY_LIBDIRS"
+if [ -n "$HARDEN_CFLAGS$HARDEN_LDFLAGS" ]; then
+    echo "    Harden: CFLAGS='$HARDEN_CFLAGS' LDFLAGS='$HARDEN_LDFLAGS'"
+fi
 echo ""
 
 # ── Step 1: WPO-compile entry script -> program.so ───────────────────────────
@@ -217,13 +270,18 @@ echo "==> [3/4] Generate ${OUTPUT}-main.c"
 
 cat > "${OUTPUT}-main.c" <<'CMAIN'
 /* Jerboa binary entry point — generated by support/build-binary.sh */
+#define _GNU_SOURCE
 #include "scheme.h"
+#include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <sys/types.h>
+#if defined(__linux__) && defined(__x86_64__)
+#include <sys/syscall.h>
+#endif
 
 #include "petite_boot.h"
 #include "scheme_boot.h"
@@ -232,6 +290,38 @@ cat > "${OUTPUT}-main.c" <<'CMAIN'
 #include "typed_symbols.h"
 #endif
 
+#if defined(__linux__) && defined(__x86_64__)
+#ifndef ARCH_SHSTK_ENABLE
+#define ARCH_SHSTK_ENABLE 0x5001
+#endif
+#ifndef ARCH_SHSTK_SHSTK
+#define ARCH_SHSTK_SHSTK (1ULL << 0)
+#endif
+
+static int env_flag_enabled(const char *v) {
+    return v && *v &&
+           strcmp(v, "0") != 0 &&
+           strcmp(v, "false") != 0 &&
+           strcmp(v, "FALSE") != 0 &&
+           strcmp(v, "no") != 0 &&
+           strcmp(v, "NO") != 0 &&
+           strcmp(v, "off") != 0 &&
+           strcmp(v, "OFF") != 0;
+}
+
+static void maybe_enable_shadow_stack(void) {
+    const char *v = getenv("JERBOA_ENABLE_SHSTK");
+    if (!env_flag_enabled(v)) return;
+    if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {
+        if (strcmp(v, "try") == 0) return;
+        fprintf(stderr, "jerboa: ARCH_SHSTK_ENABLE failed: %s\n", strerror(errno));
+        exit(127);
+    }
+}
+#else
+static void maybe_enable_shadow_stack(void) { }
+#endif
+
 static const char *write_program_tmpfile(void) {
     static char path[] = "/tmp/jerboa-prog-XXXXXX";
     int fd = mkstemp(path);
@@ -245,6 +335,8 @@ static const char *write_program_tmpfile(void) {
 }
 
 int main(int argc, const char *argv[]) {
+    maybe_enable_shadow_stack();
+
 #ifdef JERBOA_BINARY_STATIC_ENV
     setenv("JERBOA_STATIC", "1", 1);
 #endif
@@ -303,12 +395,13 @@ if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then
 fi
 
 # shellcheck disable=SC2086
-$CC -I. -I"$CSV_DIR" -O2 $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \
+$CC -I. -I"$CSV_DIR" -O2 $HARDEN_CFLAGS $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \
     -o "$OUTPUT" \
     "${OUTPUT}-main.c" \
     "$CSV_DIR/libkernel.a" \
     $EXTRA_ARCHIVES \
     $TYPED_RUST_LINK \
+    $HARDEN_LDFLAGS \
     $OS_LIBS
 
 echo ""
diff --git a/support/build-jerboa-multicall.ss b/support/build-jerboa-multicall.ss
index 2df11e4..c53e7b5 100644
--- a/support/build-jerboa-multicall.ss
+++ b/support/build-jerboa-multicall.ss
@@ -325,6 +325,67 @@
         [(string=? m "tarm64osx") "macos-arm64"]
         [else m]))
 
+(define (env-enabled? name default)
+  (let ([v (getenv name)])
+    (cond
+      [(not v) default]
+      [(or (string=? v "")
+           (string=? v "0")
+           (string=? v "false")
+           (string=? v "False")
+           (string=? v "FALSE")
+           (string=? v "no")
+           (string=? v "No")
+           (string=? v "NO")
+           (string=? v "off")
+           (string=? v "Off")
+           (string=? v "OFF"))
+       #f]
+      [else #t])))
+
+(define (linux-x86-64-machine? target-os machine)
+  (and (eq? target-os 'linux)
+       (or (string-prefix? "ta6" machine)
+           (string-prefix? "a6" machine))))
+
+(define (linux-arm64-machine? target-os machine)
+  (and (eq? target-os 'linux)
+       (or (string-prefix? "tarm64" machine)
+           (string-prefix? "arm64" machine))))
+
+(define (target-hardening-cflags target-os machine)
+  (case target-os
+    [(linux freebsd openbsd netbsd)
+     (string-append
+       " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
+       (if (eq? target-os 'linux) " -fstack-clash-protection" "")
+       (cond
+         [(linux-x86-64-machine? target-os machine) " -fcf-protection=full"]
+         [(linux-arm64-machine? target-os machine)  " -mbranch-protection=standard"]
+         [else ""]))]
+    [else ""]))
+
+(define (target-hardening-ldflags target-os machine)
+  (case target-os
+    [(linux freebsd openbsd netbsd)
+     (string-append
+       " -Wl,-z,relro,-z,now"
+       (if (and (linux-x86-64-machine? target-os machine)
+                (env-enabled? "JERBOA_CET_ELF_NOTES" #t))
+           " -Wl,-z,ibt,-z,shstk"
+           ""))]
+    [else ""]))
+
+(define harden-cflags
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (target-hardening-cflags target-os machine)
+      ""))
+
+(define harden-ldflags
+  (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+      (target-hardening-ldflags target-os machine)
+      ""))
+
 (define csv-dir (or (getenv "CHEZ_DIR")
                     (find-csv-dir (format "~a/lib" (if cross? cross-prefix (format "~a/.chez" repo)))
                                   machine)))
@@ -351,6 +412,8 @@
 (printf "    machine: ~a~n" machine)
 (printf "    chez:    ~a~n" csv-dir)
 (printf "    output:  ~a~n" output)
+(unless (and (string=? harden-cflags "") (string=? harden-ldflags ""))
+  (printf "    harden:  CFLAGS='~a' LDFLAGS='~a'~n" harden-cflags harden-ldflags))
 (for-each require-file
   (list (format "~a/jerbuild.ss" repo)
         (format "~a/lsp/main-binary.ss" repo)
@@ -464,9 +527,10 @@
          (map (lambda (a) (if (file-exists? (format "~a/~a" csv-dir a)) (format " ~a/~a" csv-dir a) ""))
               '("liblz4.a" "libz.a"))))
 (define cc (env "CC" "cc"))
-(run (format "~a -I~a -I~a -O2 -o ~a ~a/support/multicall-main.c ~a/libkernel.a~a ~a"
-             cc (shell-quote build-dir) (shell-quote csv-dir) (shell-quote output)
-             (shell-quote repo) (shell-quote csv-dir) extra-archives os-libs))
+(run (format "~a -I~a -I~a -O2~a -o ~a ~a/support/multicall-main.c ~a/libkernel.a~a~a ~a"
+             cc (shell-quote build-dir) (shell-quote csv-dir)
+             harden-cflags (shell-quote output)
+             (shell-quote repo) (shell-quote csv-dir) extra-archives harden-ldflags os-libs))
 
 (printf "==> [6/6] symlinks~n")
 (for-each (lambda (nm) (run (format "ln -sf jerboa ~a/~a" (shell-quote out-dir) nm)))
diff --git a/support/build-jerbuild.sh b/support/build-jerbuild.sh
index 499c9c1..1335d3a 100755
--- a/support/build-jerbuild.sh
+++ b/support/build-jerbuild.sh
@@ -86,6 +86,49 @@ else
     esac
 fi
 
+flag_enabled() {
+    case "${1:-}" in
+        ""|0|false|False|FALSE|no|No|NO|off|Off|OFF) return 1 ;;
+        *) return 0 ;;
+    esac
+}
+
+target_hardening_cflags() {
+    target_os="$1"
+    machine="$2"
+    case "$target_os" in
+        Linux|FreeBSD|OpenBSD|NetBSD)
+            flags="-fstack-protector-strong -D_FORTIFY_SOURCE=2"
+            case "$target_os" in
+                Linux) flags="$flags -fstack-clash-protection" ;;
+            esac
+            case "$target_os:$machine" in
+                Linux:*a6*)    flags="$flags -fcf-protection=full" ;;
+                Linux:*arm64*) flags="$flags -mbranch-protection=standard" ;;
+            esac
+            printf '%s' "$flags"
+            ;;
+        *) printf '%s' "" ;;
+    esac
+}
+
+target_hardening_ldflags() {
+    target_os="$1"
+    machine="$2"
+    case "$target_os" in
+        Linux|FreeBSD|OpenBSD|NetBSD)
+            flags="-Wl,-z,relro,-z,now"
+            if [ "$target_os" = Linux ] && flag_enabled "${JERBOA_CET_ELF_NOTES:-1}"; then
+                case "$machine" in
+                    *a6*) flags="$flags -Wl,-z,ibt,-z,shstk" ;;
+                esac
+            fi
+            printf '%s' "$flags"
+            ;;
+        *) printf '%s' "" ;;
+    esac
+}
+
 # ── Determine machine type + Chez install dir ───────────────────────────────
 if [ "$CROSS_BUILD" = yes ]; then
     MACHINE_TYPE="$TARGET_MACHINE"
@@ -98,6 +141,13 @@ EOF
     CSV_SEARCH_PREFIX="$JERBOA_CHEZ_PREFIX/lib"
 fi
 
+HARDEN_CFLAGS=""
+HARDEN_LDFLAGS=""
+if flag_enabled "${JERBOA_BINARY_HARDEN:-1}"; then
+    HARDEN_CFLAGS=$(target_hardening_cflags "$TARGET_OS" "$MACHINE_TYPE")
+    HARDEN_LDFLAGS=$(target_hardening_ldflags "$TARGET_OS" "$MACHINE_TYPE")
+fi
+
 CSV_DIR=""
 for prefix in "$CSV_SEARCH_PREFIX" /usr/local/lib /usr/lib /usr/lib64 /opt/homebrew/lib /opt/local/lib; do
     for d in "$prefix"/csv*/"$MACHINE_TYPE"; do
@@ -119,6 +169,9 @@ fi
 echo "    CC:    $CC"
 echo "    Chez:  $CSV_DIR"
 echo "    Out:   ./$OUTPUT"
+if [ -n "$HARDEN_CFLAGS$HARDEN_LDFLAGS" ]; then
+    echo "    Harden: CFLAGS='$HARDEN_CFLAGS' LDFLAGS='$HARDEN_LDFLAGS'"
+fi
 echo ""