Add hardware hardening report smoke

ober

2659a56a7fd98b0c35f577e4ad94f78e66667c49

diff --git a/.builds/ci.yml b/.builds/ci.yml
index 6cb637c..86384bc 100644
--- a/.builds/ci.yml
+++ b/.builds/ci.yml
@@ -16,7 +16,7 @@ sources:
 tasks:
 - build-and-test: |
     cd jerboa
-      make jerboa
+      make security-hardware-smoke
       make data-check
       make test
       make fuzz-smoke
diff --git a/Makefile b/Makefile
index 8202ae5..de17fed 100644
--- a/Makefile
+++ b/Makefile
@@ -57,7 +57,7 @@ TYPED_LLVMIR_PARITY_FN ?= sample_typed_llvmir_smoke::main
 LLVM_BIN ?= $(shell if command -v llvm-as >/dev/null 2>&1; then dirname "$$(command -v llvm-as)"; elif [ -x /opt/homebrew/opt/llvm/bin/llvm-as ]; then echo /opt/homebrew/opt/llvm/bin; elif [ -x /usr/local/opt/llvm/bin/llvm-as ]; then echo /usr/local/opt/llvm/bin; fi)
 TYPED_WRAPPER_DIR ?= build/typed/jerboa
 
-.PHONY: help chez chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push
+.PHONY: help chez chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-known-flaky test-reader test-core test-runtime test-try-debug test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-sqlite-robustness test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-llvmir test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile security-hardware-smoke fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict data-check docker-build docker-push
 .PHONY: check-cross-tools docker fuzz-websocket-fuzz jlsp jlsp-freebsd-amd64 \
         jlsp-install jlsp-linux-amd64 jlsp-portable jmcp-freebsd-amd64 \
         jmcp-freebsd-arm64 jmcp-linux-amd64 jmcp-linux-arm64 \
@@ -180,6 +180,7 @@ help:
 	@echo "  test-wrappers    FFI wrapper module tests"
 	@echo "  test-security    Security tests"
 	@echo "  security         Run profile-aware local security checks"
+	@echo "  security-hardware-smoke  Report/check binary hardware hardening"
 	@echo "  test-native      Rust native library tests"
 	@echo "  test-all         All test suites combined"
 	@echo "  test-phase2      Phase 2 feature tests"
@@ -1451,6 +1452,26 @@ security-production: security-profile test-security-profile
 security-profile:
 	@test -f .jerboa/security.json
 
+security-hardware-smoke: jerboa
+	@echo "--- Hardware hardening smoke ---"
+	@JERBOA_SECURITY_REPORT=1 dist/jerboa
+	@if [ "$(HOST_UNAME_S):$(HOST_UNAME_M)" = "Linux:x86_64" ]; then \
+	    if [ "$${JERBOA_BINARY_HARDEN:-1}" = "0" ] || [ "$${JERBOA_CET_ELF_NOTES:-1}" = "0" ]; then \
+	        echo "  ELF CET note check: skipped by environment"; \
+	    elif command -v readelf >/dev/null 2>&1; then \
+	        if readelf -n dist/jerboa 2>/dev/null | grep -Eq 'IBT|SHSTK'; then \
+	            echo "  ELF CET notes: PASS"; \
+	        else \
+	            echo "  ELF CET notes: FAIL (expected IBT/SHSTK GNU property notes)" >&2; \
+	            exit 1; \
+	        fi; \
+	    else \
+	        echo "  ELF CET note check: skipped (readelf not installed)"; \
+	    fi; \
+	else \
+	    echo "  ELF CET note check: skipped on $(HOST_UNAME_S)/$(HOST_UNAME_M)"; \
+	fi
+
 # Rust native library
 RUST_NATIVE_DIR = jerboa-native-rs
 UNAME_S := $(shell uname -s)
diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 506a093..8b7b3b7 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -6198,15 +6198,15 @@
      "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")
+    ";; 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. Add a no-side-effect report path:\n;;    JERBOA_SECURITY_REPORT=1 ./binary\n;;    It should print compiled CET/BTI/PAC state plus SHSTK runtime status,\n;;    then exit before Sscheme_init and without enabling SHSTK.\n;; 6. 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.\n;; 7. Add a smoke target that runs the report path and, on Linux x86_64,\n;;    verifies readelf -n shows IBT/SHSTK notes when available.") ("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.")
+     "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. A security report path is safe by default because it exits before booting Scheme or enabling SHSTK.")
    ("tags" "binary" "hardening" "CET" "SHSTK" "launcher"
      "jerbuild")
    ("title"
      .
-     "Harden generated binary launchers for CET/BTI targets"))
+     "Harden and report generated binary launcher security state"))
  (("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")
@@ -6217,4 +6217,16 @@
    ("tags" "chezscheme" "source-paths"
      "current-make-source-object" "fasl" "release-build"
      "strip-fasl-file")
-   ("title" . "Sanitize Chez source paths in release builds")))
+   ("title" . "Sanitize Chez source paths in release builds"))
+ (("code"
+    .
+    ";; During header generation, store bytes through a reversible transform.\n(define embed-xor-key #xA7)\n\n(define (write-encoded-c-array bv var-name out)\n  (let ([n (bytevector-length bv)])\n    (display (format \"static const unsigned char ~a[] = {\\n\" var-name) out)\n    (let loop ([i 0])\n      (when (< i n)\n        (let ([stored (bitwise-xor (bytevector-u8-ref bv i) embed-xor-key)])\n          (display (format \"0x~2,'0x,\" stored) out))\n        (when (= (mod (+ i 1) 16) 0) (newline out))\n        (loop (+ i 1))))\n    (display \"};\\n\" out)\n    (display (format \"static const unsigned int ~a_size = sizeof(~a);\\n\" var-name var-name) out)))\n\n;; In the generated C launcher, decode before registering boot files or\n;; writing the WPO program to memfd/tmpfile:\n;;\n;; #define JSH_BLOB_XOR_KEY 0xA7\n;; static unsigned char *decode_blob(const unsigned char *src, size_t size) {\n;;     unsigned char *dst = malloc(size ? size : 1);\n;;     if (!dst) { perror(\"malloc blob\"); exit(1); }\n;;     for (size_t i = 0; i < size; i++) dst[i] = src[i] ^ JSH_BLOB_XOR_KEY;\n;;     return dst;\n;; }\n;;\n;; unsigned char *petite_plain = decode_blob(petite_boot, petite_boot_size);\n;; Sregister_boot_file_bytes(\"petite\", petite_plain, petite_boot_size);") ("id" . "encode-embedded-chez-blobs-in-c-launcher")
+   ("imports" "(chezscheme)")
+   ("notes"
+     .
+     "This keeps raw FASL/source/help strings out of the final ELF's static data, which strip(1) cannot remove. Keep decoded buffers alive at least through Sbuild_heap/Sscheme_program initialization; freeing after Sscheme_deinit is a conservative default. This is obfuscation/string-surface reduction, not cryptographic secrecy.")
+   ("tags" "chez" "boot" "wpo" "static-binary" "c-launcher"
+     "strings")
+   ("title"
+     .
+     "Encode embedded Chez boot and WPO blobs in generated C launchers")))
diff --git a/docs/chez-hardening.md b/docs/chez-hardening.md
index b7d2c48..191f7ec 100644
--- a/docs/chez-hardening.md
+++ b/docs/chez-hardening.md
@@ -337,12 +337,17 @@ 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.
+Set `JERBOA_SECURITY_REPORT=1` on those generated launchers to print the
+compiled CET/BTI/PAC state and the current SHSTK runtime status, then exit
+without booting Scheme or enabling SHSTK.
 
 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.
+Run `make security-hardware-smoke` to exercise this report path and verify
+Linux x86_64 ELF CET notes when `readelf` is available.
 
 ### 6.4 Future Work: Continuation-Aware Shadow-Stack Management
 
diff --git a/docs/secure.md b/docs/secure.md
index a25132a..bf327aa 100644
--- a/docs/secure.md
+++ b/docs/secure.md
@@ -50,6 +50,8 @@ Every indirect jump/call target must begin with an `ENDBR64` instruction. If con
 
 **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.
 
+**Runtime reporting**: Set `JERBOA_SECURITY_REPORT=1` to make generated launchers print their compiled CET/BTI/PAC state and current SHSTK runtime status, then exit without booting Scheme or enabling SHSTK.
+
 ### Standard Hardening CFLAGS
 
 Rebuild Chez and all C code with:
@@ -77,6 +79,8 @@ 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.
+Run `make security-hardware-smoke` to build the multicall binary, print its
+security report, and check Linux x86_64 ELF CET notes when `readelf` is present.
 
 **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
diff --git a/jerbuild.ss b/jerbuild.ss
index f504f0c..33fed0d 100644
--- a/jerbuild.ss
+++ b/jerbuild.ss
@@ -2043,33 +2043,104 @@
 #ifndef ARCH_SHSTK_ENABLE
 #define ARCH_SHSTK_ENABLE 0x5001
 #endif
+#ifndef ARCH_SHSTK_STATUS
+#define ARCH_SHSTK_STATUS 0x5005
+#endif
 #ifndef ARCH_SHSTK_SHSTK
 #define ARCH_SHSTK_SHSTK (1ULL << 0)
 #endif
+#endif
 
-static int env_flag_enabled(const char *v) {
+static int jerboa_env_flag_enabled(const char *v) {
     return v && *v &&
            strcmp(v, \"0\") != 0 &&
            strcmp(v, \"false\") != 0 &&
+           strcmp(v, \"False\") != 0 &&
            strcmp(v, \"FALSE\") != 0 &&
            strcmp(v, \"no\") != 0 &&
+           strcmp(v, \"No\") != 0 &&
            strcmp(v, \"NO\") != 0 &&
            strcmp(v, \"off\") != 0 &&
+           strcmp(v, \"Off\") != 0 &&
            strcmp(v, \"OFF\") != 0;
 }
 
-static void maybe_enable_shadow_stack(void) {
+static int jerboa_security_report_requested(void) {
+    return jerboa_env_flag_enabled(getenv(\"JERBOA_SECURITY_REPORT\"));
+}
+
+static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
+#if defined(__linux__) && defined(__x86_64__)
     const char *v = getenv(\"JERBOA_ENABLE_SHSTK\");
-    if (!env_flag_enabled(v)) return;
+    if (!jerboa_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));
+        fprintf(stderr, \"%s: ARCH_SHSTK_ENABLE failed: %s\\n\",
+                program_name ? program_name : \"jerboa\", strerror(errno));
         exit(127);
     }
+#else
+    (void)program_name;
+#endif
 }
+
+static void jerboa_print_security_report(const char *program_name) {
+    const char *request = getenv(\"JERBOA_ENABLE_SHSTK\");
+    printf(\"program: %s\\n\", program_name ? program_name : \"jerboa\");
+#if defined(__linux__)
+    printf(\"os: linux\\n\");
+#elif defined(__APPLE__)
+    printf(\"os: macos\\n\");
+#elif defined(__FreeBSD__)
+    printf(\"os: freebsd\\n\");
+#elif defined(__OpenBSD__)
+    printf(\"os: openbsd\\n\");
+#elif defined(__NetBSD__)
+    printf(\"os: netbsd\\n\");
+#else
+    printf(\"os: unknown\\n\");
+#endif
+#if defined(__x86_64__)
+    printf(\"arch: x86_64\\n\");
+#elif defined(__aarch64__) || defined(__arm64__)
+    printf(\"arch: arm64\\n\");
+#else
+    printf(\"arch: unknown\\n\");
+#endif
+#if defined(__CET__)
+    printf(\"cet-compiled: yes (__CET__=%d)\\n\", __CET__);
 #else
-static void maybe_enable_shadow_stack(void) { }
+    printf(\"cet-compiled: no\\n\");
 #endif
+#if defined(__ARM_FEATURE_BTI_DEFAULT)
+    printf(\"arm-bti-compiled: yes\\n\");
+#else
+    printf(\"arm-bti-compiled: no\\n\");
+#endif
+#if defined(__ARM_FEATURE_PAC_DEFAULT)
+    printf(\"arm-pac-compiled: yes (__ARM_FEATURE_PAC_DEFAULT=%d)\\n\",
+           __ARM_FEATURE_PAC_DEFAULT);
+#else
+    printf(\"arm-pac-compiled: no\\n\");
+#endif
+#if defined(__linux__) && defined(__x86_64__)
+    {
+        unsigned long features = 0;
+        errno = 0;
+        if (syscall(SYS_arch_prctl, ARCH_SHSTK_STATUS, &features) == 0) {
+            printf(\"shstk-runtime: %s (mask=0x%lx)\\n\",
+                   (features & ARCH_SHSTK_SHSTK) ? \"enabled\" : \"disabled\",
+                   features);
+        } else {
+            printf(\"shstk-runtime: unavailable (errno=%d %s)\\n\",
+                   errno, strerror(errno));
+        }
+    }
+#else
+    printf(\"shstk-runtime: not-applicable\\n\");
+#endif
+    printf(\"shstk-request: %s\\n\", request && *request ? request : \"<unset>\");
+}
 
 static const char *write_program_tmpfile(void) {
     static char path[] = \"/tmp/jerboa-prog-XXXXXX\";
@@ -2084,7 +2155,12 @@ static const char *write_program_tmpfile(void) {
 }
 
 int main(int argc, const char *argv[]) {
-    maybe_enable_shadow_stack();
+    if (jerboa_security_report_requested()) {
+        jerboa_print_security_report(\"jerboa\");
+        return 0;
+    }
+
+    jerboa_maybe_enable_shadow_stack(\"jerboa\");
 
     Sscheme_init(NULL);
     Sregister_boot_file_bytes(\"petite\",
diff --git a/lsp/build-binary.ss b/lsp/build-binary.ss
index d1df4d0..345078f 100644
--- a/lsp/build-binary.ss
+++ b/lsp/build-binary.ss
@@ -311,31 +311,14 @@
     (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 "#include \"hardening-report.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 "  if (jerboa_security_report_requested()) {\n")
+    (fprintf out "    jerboa_print_security_report(\"jlsp\");\n")
+    (fprintf out "    return 0;\n")
+    (fprintf out "  }\n")
+    (fprintf out "  jerboa_maybe_enable_shadow_stack(\"jlsp\");\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")
@@ -380,7 +363,7 @@
 (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"
+  (let ((rc (system (format "~a -c -I~a -Isupport -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")
diff --git a/mcp/build-jmcp.sh b/mcp/build-jmcp.sh
index c8af18e..900c394 100755
--- a/mcp/build-jmcp.sh
+++ b/mcp/build-jmcp.sh
@@ -198,29 +198,8 @@ cat > "$builder" <<'SCHEME'
       (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)
+      (display "#include \"jmcp_program.h\"\n" out)
+      (display "#include \"hardening-report.h\"\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)
@@ -247,7 +226,11 @@ 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 "  if (jerboa_security_report_requested()) {\n" out)
+      (display "    jerboa_print_security_report(\"jmcp\");\n" out)
+      (display "    return 0;\n" out)
+      (display "  }\n" out)
+      (display "  jerboa_maybe_enable_shadow_stack(\"jmcp\");\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)
@@ -328,31 +311,33 @@ cat > "$builder" <<'SCHEME'
                  (loop (read in) #f)]))))
         'replace))))
 
-(define (default-link-command cc target-os machine static? chez-dir output main-c)
+(define (default-link-command cc target-os machine static? chez-dir support-dir output main-c)
   (let* ([ccq (or cc "cc")]
          [outq (shell-quote output)]
          [mainq (shell-quote main-c)]
          [chezq (shell-quote chez-dir)]
+         [supportq (shell-quote support-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~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)]
+       (format "~a -O2~a -static -Wl,--export-dynamic -I~a -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 supportq chezq outq mainq chezq chezq chezq harden-ldflags)]
       [(string=? target-os "freebsd")
-       (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)]
+       (format "~a -O2~a -I~a -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a~a -lm -lthr -lutil -lncurses"
+               ccq harden-cflags supportq 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)]
+       (format "~a -O2 -I~a -I~a -o ~a ~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a -lm -lpthread -lncurses -liconv"
+               ccq supportq chezq outq mainq chezq chezq chezq)]
       [else
-       (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)])))
+       (format "~a -O2~a -I~a -I~a -o ~a ~a -L~a -lkernel -llz4 -lz~a -lm -ldl -lpthread -lncurses"
+               ccq harden-cflags supportq chezq outq mainq chezq harden-ldflags)])))
 
 (define repo-dir (current-directory))
 (define entry (env "JMCP_ENTRY" (format "~a/bin/jerboa-mcp.ss" repo-dir)))
 (define output (env "JMCP_OUTPUT" "jmcp"))
 (define build-dir (env "JMCP_BUILD_DIR" (format "~a/.build/~a" repo-dir output)))
+(define support-dir (format "~a/support" repo-dir))
 (define cross-prefix (getenv "JMCP_CROSS_PREFIX"))
 (define xpatch (getenv "JMCP_XPATCH"))
 (define target-os (env "JMCP_TARGET_OS"
@@ -437,7 +422,7 @@ cat > "$builder" <<'SCHEME'
 (printf "==> link~n")
 (let* ([cc (env "CC" "cc")]
        [cmd (or (getenv "JMCP_LINK_COMMAND")
-                (default-link-command cc target-os machine static? chez-dir output main-c))])
+                (default-link-command cc target-os machine static? chez-dir support-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 412deee..368f55e 100755
--- a/support/build-binary.sh
+++ b/support/build-binary.sh
@@ -289,38 +289,7 @@ cat > "${OUTPUT}-main.c" <<'CMAIN'
 #ifdef JERBOA_HAS_TYPED_SYMBOLS
 #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
+#include "hardening-report.h"
 
 static const char *write_program_tmpfile(void) {
     static char path[] = "/tmp/jerboa-prog-XXXXXX";
@@ -335,7 +304,12 @@ static const char *write_program_tmpfile(void) {
 }
 
 int main(int argc, const char *argv[]) {
-    maybe_enable_shadow_stack();
+    if (jerboa_security_report_requested()) {
+        jerboa_print_security_report("jerboa");
+        return 0;
+    }
+
+    jerboa_maybe_enable_shadow_stack("jerboa");
 
 #ifdef JERBOA_BINARY_STATIC_ENV
     setenv("JERBOA_STATIC", "1", 1);
@@ -395,7 +369,7 @@ if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then
 fi
 
 # shellcheck disable=SC2086
-$CC -I. -I"$CSV_DIR" -O2 $HARDEN_CFLAGS $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \
+$CC -I. -I"$JERBOA_HOME/support" -I"$CSV_DIR" -O2 $HARDEN_CFLAGS $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \
     -o "$OUTPUT" \
     "${OUTPUT}-main.c" \
     "$CSV_DIR/libkernel.a" \
diff --git a/support/build-jerbuild.sh b/support/build-jerbuild.sh
index 1335d3a..38e88d8 100755
--- a/support/build-jerbuild.sh
+++ b/support/build-jerbuild.sh
@@ -281,38 +281,7 @@ cat > "${OUTPUT}-main.c" <<'CMAIN'
 #include "program_boot.h"
 #include "bundle_tar.h"
 #include "bundle_meta.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, "jerbuild: ARCH_SHSTK_ENABLE failed: %s\n", strerror(errno));
-        exit(127);
-    }
-}
-#else
-static void maybe_enable_shadow_stack(void) { }
-#endif
+#include "hardening-report.h"
 
 /* Cache layout:
  *   $XDG_CACHE_HOME/jerbuild/<sha256>/         (or ~/.cache/jerbuild/<sha256>/)
@@ -424,7 +393,12 @@ static const char *write_program_tmpfile(void) {
 }
 
 int main(int argc, const char *argv[]) {
-    maybe_enable_shadow_stack();
+    if (jerboa_security_report_requested()) {
+        jerboa_print_security_report("jerbuild");
+        return 0;
+    }
+
+    jerboa_maybe_enable_shadow_stack("jerbuild");
 
     /* Fast paths that avoid booting Chez. */
     if (argc == 2 && strcmp(argv[1], "--jerboa-home") == 0) {
@@ -500,7 +474,7 @@ for a in liblz4.a libz.a; do
 done
 
 # shellcheck disable=SC2086
-$CC -I. -I"$CSV_DIR" -O2 $HARDEN_CFLAGS \
+$CC -I. -I"$JERBOA_HOME/support" -I"$CSV_DIR" -O2 $HARDEN_CFLAGS \
     -o "$OUTPUT" \
     "${OUTPUT}-main.c" \
     "$CSV_DIR/libkernel.a" \
diff --git a/support/hardening-report.h b/support/hardening-report.h
new file mode 100644
index 0000000..44b6742
--- /dev/null
+++ b/support/hardening-report.h
@@ -0,0 +1,134 @@
+/* Shared launcher hardening helpers for generated Jerboa/Chez binaries.
+ *
+ * Normal startup behavior is unchanged unless one of the documented
+ * environment variables is set:
+ *   JERBOA_ENABLE_SHSTK=1|try   opt in to Linux x86_64 shadow stacks
+ *   JERBOA_SECURITY_REPORT=1    print build/runtime hardening state and exit
+ */
+#ifndef JERBOA_HARDENING_REPORT_H
+#define JERBOA_HARDENING_REPORT_H
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(__linux__) && defined(__x86_64__)
+#include <sys/syscall.h>
+#include <unistd.h>
+#endif
+
+#if defined(__linux__) && defined(__x86_64__)
+#ifndef ARCH_SHSTK_ENABLE
+#define ARCH_SHSTK_ENABLE 0x5001
+#endif
+#ifndef ARCH_SHSTK_STATUS
+#define ARCH_SHSTK_STATUS 0x5005
+#endif
+#ifndef ARCH_SHSTK_SHSTK
+#define ARCH_SHSTK_SHSTK (1ULL << 0)
+#endif
+#endif
+
+static int jerboa_env_flag_enabled(const char *v) {
+    return v && *v &&
+           strcmp(v, "0") != 0 &&
+           strcmp(v, "false") != 0 &&
+           strcmp(v, "False") != 0 &&
+           strcmp(v, "FALSE") != 0 &&
+           strcmp(v, "no") != 0 &&
+           strcmp(v, "No") != 0 &&
+           strcmp(v, "NO") != 0 &&
+           strcmp(v, "off") != 0 &&
+           strcmp(v, "Off") != 0 &&
+           strcmp(v, "OFF") != 0;
+}
+
+static int jerboa_security_report_requested(void) {
+    return jerboa_env_flag_enabled(getenv("JERBOA_SECURITY_REPORT"));
+}
+
+static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
+#if defined(__linux__) && defined(__x86_64__)
+    const char *v = getenv("JERBOA_ENABLE_SHSTK");
+    if (!jerboa_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, "%s: ARCH_SHSTK_ENABLE failed: %s\n",
+                program_name ? program_name : "jerboa", strerror(errno));
+        exit(127);
+    }
+#else
+    (void)program_name;
+#endif
+}
+
+static void jerboa_print_security_report(const char *program_name) {
+    const char *request = getenv("JERBOA_ENABLE_SHSTK");
+
+    printf("program: %s\n", program_name ? program_name : "jerboa");
+
+#if defined(__linux__)
+    printf("os: linux\n");
+#elif defined(__APPLE__)
+    printf("os: macos\n");
+#elif defined(__FreeBSD__)
+    printf("os: freebsd\n");
+#elif defined(__OpenBSD__)
+    printf("os: openbsd\n");
+#elif defined(__NetBSD__)
+    printf("os: netbsd\n");
+#else
+    printf("os: unknown\n");
+#endif
+
+#if defined(__x86_64__)
+    printf("arch: x86_64\n");
+#elif defined(__aarch64__)
+    printf("arch: arm64\n");
+#elif defined(__arm64__)
+    printf("arch: arm64\n");
+#else
+    printf("arch: unknown\n");
+#endif
+
+#if defined(__CET__)
+    printf("cet-compiled: yes (__CET__=%d)\n", __CET__);
+#else
+    printf("cet-compiled: no\n");
+#endif
+
+#if defined(__ARM_FEATURE_BTI_DEFAULT)
+    printf("arm-bti-compiled: yes\n");
+#else
+    printf("arm-bti-compiled: no\n");
+#endif
+
+#if defined(__ARM_FEATURE_PAC_DEFAULT)
+    printf("arm-pac-compiled: yes (__ARM_FEATURE_PAC_DEFAULT=%d)\n",
+           __ARM_FEATURE_PAC_DEFAULT);
+#else
+    printf("arm-pac-compiled: no\n");
+#endif
+
+#if defined(__linux__) && defined(__x86_64__)
+    {
+        unsigned long features = 0;
+        errno = 0;
+        if (syscall(SYS_arch_prctl, ARCH_SHSTK_STATUS, &features) == 0) {
+            printf("shstk-runtime: %s (mask=0x%lx)\n",
+                   (features & ARCH_SHSTK_SHSTK) ? "enabled" : "disabled",
+                   features);
+        } else {
+            printf("shstk-runtime: unavailable (errno=%d %s)\n",
+                   errno, strerror(errno));
+        }
+    }
+#else
+    printf("shstk-runtime: not-applicable\n");
+#endif
+
+    printf("shstk-request: %s\n", request && *request ? request : "<unset>");
+}
+
+#endif
diff --git a/support/multicall-main.c b/support/multicall-main.c
index 814b264..9170f58 100644
--- a/support/multicall-main.c
+++ b/support/multicall-main.c
@@ -47,38 +47,7 @@
 #include "program_boot.h"
 #include "bundle_tar.h"
 #include "bundle_meta.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
+#include "hardening-report.h"
 
 /* ── jerbuild lib bundle: extracted to a per-sha cache dir on demand ───────
  * Layout: $XDG_CACHE_HOME/jerbuild/<sha256>/ (or ~/.cache/jerbuild/<sha256>/)
@@ -310,13 +279,18 @@ int main(int argc, const char *argv[]) {
     if (!strcmp(mode, "lsp")) mode = "jlsp";
     if (!strcmp(mode, "pkg")) mode = "jpkg";
 
+    if (jerboa_security_report_requested()) {
+        jerboa_print_security_report(mode);
+        return 0;
+    }
+
     /* Clean runtime mode: `jerboa runtime [script [args...]]` boots the
      * language runtime WITHOUT the embedded program image. Used by
      * `jerbuild binary` on a target host to run the WPO compile in a fresh
      * process with an empty library table, so bundled stdlib sources recompile
      * to native .wpo (cross-built .wpo carry the build host's machine-type). */
     if (!strcmp(name, "jerboa") && argc > 1 && !strcmp(argv[1], "runtime")) {
-        maybe_enable_shadow_stack();
+        jerboa_maybe_enable_shadow_stack("jerboa");
         Sscheme_init(NULL);
         Sregister_boot_file_bytes("petite", (void *)petite_boot_data, petite_boot_size);
         Sregister_boot_file_bytes("scheme", (void *)scheme_boot_data, scheme_boot_size);
@@ -353,7 +327,7 @@ int main(int argc, const char *argv[]) {
     }
     setenv("JERBOA_MULTICALL_NAME", mode, 1);
 
-    maybe_enable_shadow_stack();
+    jerboa_maybe_enable_shadow_stack(mode);
     Sscheme_init(NULL);
     Sregister_boot_file_bytes("petite", (void *)petite_boot_data, petite_boot_size);
     Sregister_boot_file_bytes("scheme", (void *)scheme_boot_data, scheme_boot_size);