Harden generated Chez binary launchers
ober
1b6c47126ea7acc9012b862ff9fff7bd76b48053
--- 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"))) --- 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"))) --- 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`. --- 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)`. --- 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)]) --- 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)))) --- 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) --- 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 "" --- 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))) --- 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 ""