docs: track fork-over-cisco divergence as a patch series

ober

d1ad34b6b9bf39159d9ee94d939a8d6a2d057be4

diff --git a/docs/chez-fork.md b/docs/chez-fork.md
new file mode 100644
index 0000000..a458edc
--- /dev/null
+++ b/docs/chez-fork.md
@@ -0,0 +1,121 @@
+# ChezScheme Fork Notes
+
+The Chez Scheme bundled at `vendor/ChezScheme/` is **not stock**. It's a
+soft fork hosted at `git.sr.ht/~lisp/ChezScheme` (working tree:
+`~/mine/ChezScheme`), branched from upstream `github.com/cisco/ChezScheme`
+and carrying jerboa-relevant patches.
+
+This doc is the **narrative** layer: themes, motivations, what
+jerboa depends on. The **line-level** layer — one unified-diff file
+per fork-side commit, carrying its commit message — lives at
+[`../vendor/ChezScheme-patches/`](../vendor/ChezScheme-patches/).
+Read this doc for orientation; drill into the patches for specifics.
+Regenerate the patch series with `./tools/regen-chez-patches.sh`
+when `~/mine/ChezScheme` advances.
+
+## At a glance
+
+- **Upstream:** `github.com/cisco/ChezScheme`
+- **Fork:** `git.sr.ht/~lisp/ChezScheme`
+- **Last common commit at time of writing:** `4bde3b35`
+- **Divergence:** ~52 commits, ~5500 insertions / 243 deletions across 63 files
+- **Vendored snapshot in this repo:** `vendor/ChezScheme/` @ `a4be658a`
+  (see `vendor/ChezScheme/UPSTREAM.md`)
+
+To re-derive the divergence yourself:
+
+```
+cd ~/mine/ChezScheme
+git fetch upstream
+git log --no-merges --oneline upstream/main..HEAD
+git diff --stat upstream/main..HEAD
+```
+
+## Themes
+
+The divergence groups into five themes. SHAs below are fork-local —
+resolve them in `~/mine/ChezScheme`, not in the vendored tree (where
+history was stripped).
+
+### 1. Security hardening
+
+Why: jerboa ships as a single statically-linked binary that parses
+untrusted FASL/data; stock Chez defaults assume a trusted build env.
+
+- `c3c3d6f3` — `--static` and `--foreign-libs` configure options
+  (required by the musl-static pipeline; enables hermetic binaries)
+- `b3f35405` — harden FASL deserialization, FFI, and build scripts
+  against malformed input
+- `aaa82190` — vfasl bounds checks + `path_append` hardening + HPUX typo
+- `829bc806` — ENDBR64 emission at function entries (Intel CET/IBT)
+- `beefa2a6` — `--enable-harden` configure flag + ARM64 BTI landing pads
+
+Touches: `c/fasl.c`, `c/vfasl.c`, `c/main.c`, `s/x86_64.ss`,
+`s/arm64.ss`, `configure`, `c/build.zuo`.
+
+### 2. Optimizer enhancements ("Phase N" work)
+
+Why: jerboa's stdlib (persistent collections, hashtables, defstruct,
+match) leans hard on Chez's optimizer. Each "Phase N" is a bottleneck we
+identified and fixed in Chez rather than working around in jerboa.
+
+- `506f43ac`, `b3ea8e63` — newhash: sealed-record dispatch for generic
+  + bulk hashtable ops
+- `2629173b`, `5c25af3e`, `e37d7c58`, `03087783` — cptypes hashtable
+  specialization (cell/ref-cell, clear!/copy, 2-arg eq-hashtable)
+- `1776fb2f` — cp0 literal string-append folding
+
+Touches: `s/cp0.ss`, `s/cptypes.ss`, `s/cptypes-lattice.ss`,
+`s/newhash.ss`, `s/primdata.ss`.
+
+Regression guards: `mats/cptypes.ms`, `mats/hash.ms`, `mats/record.ms`,
+`mats/bytevector.ms`.
+
+The deeper backstory for each Phase lives in `vendor/ChezScheme/plan.md`
+(2500+ lines of running log — kept inside the fork, see §5).
+
+### 3. New primitives
+
+Why: jerboa's stdlib calls these from Scheme without round-tripping
+through C FFI. Faster, and lets the optimizer see through them.
+
+- `9c919405` — `bytevector-slice`, `bytevector-append`
+- `6a7a21ce` — `base64-encode`, `base64-decode`
+- `ada7cd0c` — `sha1-bytevector`, `sha256-bytevector`
+- `b621c8d6` — ordered-hashtable (Phase 68) + record-walk (Phase 72)
+
+Touches: `s/bytevector.ss`, `s/newhash.ss`, `s/primdata.ss`.
+
+### 4. Build & packaging
+
+- `457a455c` — `sync-upstream` + `sync-upstream-status` make targets
+  (maintenance helpers for this fork)
+- `tools/gen-static-ffi.sh` (new) — generates static FFI tables for
+  `--static` builds
+- `make-android` (new) + `d293dea3` — Android/Termux build wiring
+- `5f9c697c` — `install.zuo`: copies instead of hard links
+- `wininstall/*.wxs` — WiX installer updates for `--static`
+
+### 5. Jerboa-side docs living inside the fork
+
+Three files live in `vendor/ChezScheme/` that are *jerboa* documentation,
+not Chez source. They ride along in the fork for editing convenience but
+are **not upstream-relevant** — never include them in a patch sent to
+cisco/ChezScheme.
+
+- `plan.md` — running log of Phase N investigations and outcomes
+- `findings.md` — research notes on optimizer behavior
+- `CLAUDE.md` — Claude instructions for work *inside* the Chez tree
+  (separate from jerboa-root `CLAUDE.md`)
+- `bench/jerboa-bench.ss` — jerboa-specific benchmark suite
+
+### 6. Routine fixes not yet upstreamed
+
+- `a4be658a` — ppc32 + logtest repairs (#1045)
+
+## Keeping this doc current
+
+When the fork advances or the vendored snapshot is resynced (recipe in
+`vendor/ChezScheme/UPSTREAM.md`), re-run the at-a-glance commands above
+and slot new commits into the themes. Add a new theme only if the work
+genuinely doesn't fit (new platform port, new subsystem, etc.).
diff --git a/tools/regen-chez-patches.sh b/tools/regen-chez-patches.sh
new file mode 100755
index 0000000..c78318d
--- /dev/null
+++ b/tools/regen-chez-patches.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Regenerate vendor/ChezScheme-patches/ from ~/mine/ChezScheme.
+#
+# Captures each fork-over-cisco commit as a numbered .patch file,
+# skipping commits that only touched jerboa-side docs.
+#
+# See docs/chez-fork.md for the narrative; see
+# vendor/ChezScheme-patches/README.md for how to read the output.
+#
+# Overrides:
+#   FORK_DIR=<path>       (default: $HOME/mine/ChezScheme)
+#   UPSTREAM_REF=<ref>    (default: upstream/main)
+
+set -euo pipefail
+
+FORK_DIR="${FORK_DIR:-$HOME/mine/ChezScheme}"
+UPSTREAM_REF="${UPSTREAM_REF:-upstream/main}"
+OUT_DIR="$(cd "$(dirname "$0")/.." && pwd)/vendor/ChezScheme-patches"
+
+if [ ! -d "$FORK_DIR/.git" ]; then
+  echo "ERROR: $FORK_DIR is not a git repo" >&2
+  echo "Set FORK_DIR=<path> or clone the fork to ~/mine/ChezScheme" >&2
+  exit 1
+fi
+
+# These paths are excluded from both commit selection (commits that
+# only touch them are skipped) and from each patch's diff body.
+SKIP=(
+  ':!plan.md'
+  ':!findings.md'
+  ':!CLAUDE.md'
+  ':!bench/jerboa-bench.ss'
+)
+
+(cd "$FORK_DIR" && git fetch upstream --quiet)
+
+mkdir -p "$OUT_DIR"
+# Wipe old patches but preserve README.md and any other non-patch files.
+find "$OUT_DIR" -maxdepth 1 -name '*.patch' -delete
+
+(
+  cd "$FORK_DIR"
+  i=1
+  for sha in $(git log --no-merges --reverse --format=%H \
+                 "${UPSTREAM_REF}..HEAD" -- "${SKIP[@]}"); do
+    git format-patch -1 "$sha" \
+      --start-number "$i" \
+      -o "$OUT_DIR" \
+      -- "${SKIP[@]}" > /dev/null
+    i=$((i + 1))
+  done
+)
+
+count=$(find "$OUT_DIR" -maxdepth 1 -name '*.patch' | wc -l | tr -d ' ')
+echo "wrote $count patches to $OUT_DIR"
diff --git a/vendor/ChezScheme-patches/0001-add-static-and-foreign-libs-configure-options-for-st.patch b/vendor/ChezScheme-patches/0001-add-static-and-foreign-libs-configure-options-for-st.patch
new file mode 100644
index 0000000..e9cf45b
--- /dev/null
+++ b/vendor/ChezScheme-patches/0001-add-static-and-foreign-libs-configure-options-for-st.patch
@@ -0,0 +1,537 @@
+From c3c3d6f32913c32a37a83f965d597a91efae1215 Mon Sep 17 00:00:00 2001
+From: Jaime Fournier <jaimef@linbsd.org>
+Date: Tue, 3 Mar 2026 13:23:59 -0700
+Subject: [PATCH] add --static and --foreign-libs configure options for static
+ binary builds
+
+Support fully static Chez Scheme binaries with embedded boot files and
+statically linked FFI libraries:
+
+- configure: add --static (implies --disable-x11/curses/iconv, uses
+  -static LDFLAGS, removes -ldl) and --foreign-libs=path1.a:path2.a:...
+  for registering FFI symbols from static libraries
+- c/version.h: guard all LOAD_SHARED_OBJECT definitions behind
+  DISABLE_DYNAMIC_LOAD so dlopen/dlsym are excluded from static builds
+- c/build.zuo: generate static_ffi.c (FFI symbol registration via
+  Sforeign_symbol) and static_boot.c (embedded boot files via xxd -i
+  and Sregister_boot_file_bytes) when static build is configured
+- c/main.c: support STATIC_BOOT for embedded boot file registration
+  before Sbuild_heap, skip when explicit -b/--boot flags are given
+  (preserving bootstrap builds); change CUSTOM_INIT to use extern
+  forward declaration when defined
+- tools/gen-static-ffi.sh: new script to extract exported text symbols
+  from .a files and generate Sforeign_symbol registration code
+
+Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
+---
+ c/build.zuo             | 101 ++++++++++++++++++++++++++++++++++++++--
+ c/main.c                |  31 +++++++++++-
+ c/version.h             |  18 ++++++-
+ configure               |  66 ++++++++++++++++++++------
+ tools/gen-static-ffi.sh |  61 ++++++++++++++++++++++++
+ 5 files changed, 255 insertions(+), 22 deletions(-)
+ create mode 100755 tools/gen-static-ffi.sh
+
+diff --git a/c/build.zuo b/c/build.zuo
+index dde5b3b6..396adcff 100644
+--- a/c/build.zuo
++++ b/c/build.zuo
+@@ -73,8 +73,8 @@
+   (define c-config-file
+     (at-dir "config.h"))
+   (define c-config-keys
+-    '(disablecurses disablex11 enablelibffi alwaysUseBootFile preloadBootFiles relativeBootFiles
+-                    InstallBin InstallLib))
++    '(disablecurses disablex11 disabledynload enablelibffi alwaysUseBootFile preloadBootFiles
++                    relativeBootFiles foreignLibs InstallBin InstallLib))
+ 
+   ;; sources for "kernel.o":
+   (define kernel-src-files
+@@ -178,6 +178,25 @@
+ 
+   (define preload-files (get-preload-files config at-dir mboot))
+ 
++  ;; Static build support
++  (define static-build? (equal? (hash-ref config 'disabledynload #f) "yes"))
++  (define foreign-libs-str (or (hash-ref config 'foreignLibs #f) ""))
++  (define foreign-libs (if (equal? foreign-libs-str "")
++                           '()
++                           (string-split foreign-libs-str ":")))
++  (define static-ffi.c (at-dir "static_ffi.c"))
++  (define static-ffi.o (at-dir (.c->.o "static_ffi")))
++  (define static-boot.c (at-dir "static_boot.c"))
++  (define static-boot.o (at-dir (.c->.o "static_boot")))
++  (define static-objs (append (if (pair? foreign-libs) (list static-ffi.o) '())
++                              (if static-build? (list static-boot.o) '())))
++  (define static-lib-inputs (if (pair? foreign-libs)
++                                (map (lambda (p) (if (relative-path? p)
++                                                     (at-dir "../.." p)
++                                                     p))
++                                     foreign-libs)
++                                '()))
++
+   (define (compile-one .c .o)
+     (c-compile .o .c
+                (let* ([config (config-define config cpu)]
+@@ -234,16 +253,18 @@
+               ,void
+               :recur]
+ 
+-     [:target ,exe (,kernel-dep ,@main-objs ,@res-deps ,@preload-files ,(at-source "mingw.zuo"))
++     [:target ,exe (,kernel-dep ,@main-objs ,@static-objs ,@res-deps ,@preload-files ,(at-source "mingw.zuo"))
+               ,(lambda (path token)
+                  (mkdir-p (path-only path))
+                  (c-link path
+                          (append main-objs
++                                 static-objs
+                                  res-deps
+                                  (append
+                                   (list kernel-dep)
+                                   (if zlib-system-lib '() (list zlib-lib))
+                                   (if lz4-system-lib '() (list lz4-lib)))
++                                 static-lib-inputs
+                                  (if (and (not msvc?)
+                                           (for-windows? config))
+                                      (make-mingw-to-ms-link-libraries m (at-dir) config)
+@@ -395,7 +416,27 @@
+      ,@(map (lambda (.c .o)
+               `[:target ,.o (,.c ,@main-hdrs)
+                         ,(lambda (.o target)
+-                           (compile-one .c .o))])
++                           (define extra-cppflags
++                             (~a (if static-build? " -DSTATIC_BOOT=static_boot_init" "")
++                                 (if (pair? foreign-libs) " -DCUSTOM_INIT=static_ffi_init" "")))
++                           (c-compile .o .c
++                                      (let* ([config (config-define config cpu)]
++                                             [config (config-merge config 'CPPFLAGS
++                                                                   (~a (or (lookup 'mdinclude) "")
++                                                                       extra-cppflags))]
++                                             [config (config-merge config 'CFLAGS (or (lookup 'warningFlags) ""))]
++                                             [config (config-include config
++                                                                     (at-dir (build-path "../boot" m))
++                                                                     (at-dir ".")
++                                                                     (at-source "."))]
++                                             [config (if zlib-system-lib
++                                                         config
++                                                         (config-include config zlib-dir))]
++                                             [config (if (equal? (or (lookup 'LZ4Lib) "") "")
++                                                         (config-include config
++                                                                         (build-path lz4-dir "lib"))
++                                                         config)])
++                                        config)))])
+             main-srcs main-objs)
+ 
+      [:depend ,(at-dir (.c->.o "scheme.c")) (,(at-source "itest.c"))]
+@@ -426,6 +467,8 @@
+                                   "#define DISABLE_CURSES"))
+                        (line (and (equal? (hash-ref config 'disablex11 #f) "yes")
+                                   "#define DISABLE_X11"))
++                       (line (and (equal? (hash-ref config 'disabledynload #f) "yes")
++                                  "#define DISABLE_DYNAMIC_LOAD"))
+                        (line (and (equal? (hash-ref config 'enablelibffi #f) "yes")
+                                   "#define ENABLE_LIBFFI"))
+                        (line (and (not (for-windows? config))
+@@ -484,12 +527,60 @@
+                          ;; entry in ;; .gitattributes for build.zuo.
+                          (display-to-file "$Format:%H$\ngit\n" (at-mach "revision") :truncate))])))]
+ 
++     ;; Static FFI: generate and compile registration code for statically linked libraries
++     ,@(if (pair? foreign-libs)
++           `([:target ,static-ffi.c (,(at-source "../tools/gen-static-ffi.sh"))
++                      ,(lambda (path token)
++                         (define p (shell (apply build-shell
++                                                 (cons (string->shell (at-source "../tools/gen-static-ffi.sh"))
++                                                       (map string->shell static-lib-inputs)))
++                                          (hash 'stdout 'pipe)))
++                         (define out (fd-read (hash-ref p 'stdout) eof))
++                         (process-wait (hash-ref p 'process))
++                         (fd-close (hash-ref p 'stdout))
++                         (display-to-file out path :truncate))]
++             [:target ,static-ffi.o (,static-ffi.c ,@kernel-hdrs)
++                      ,(lambda (.o token)
++                         (compile-one static-ffi.c .o))])
++           '())
++
++     ;; Static boot: embed boot files as C byte arrays using xxd -i
++     ,@(if static-build?
++           (let ([petite-boot (at-dir (build-path "../boot" m "petite.boot"))]
++                 [scheme-boot (at-dir (build-path "../boot" m "scheme.boot"))])
++             `([:target ,static-boot.c (,petite-boot ,scheme-boot)
++                        ,(lambda (path token)
++                           (define (xxd-array file-path var-name)
++                             (define p (shell (~a "xxd -i < " (string->shell file-path))
++                                              (hash 'stdout 'pipe)))
++                             (define out (fd-read (hash-ref p 'stdout) eof))
++                             (process-wait (hash-ref p 'process))
++                             (fd-close (hash-ref p 'stdout))
++                             (~a "static const unsigned char " var-name "[] = {\n"
++                                 out "};\n\n"))
++                           (define s
++                             (~a "/* Generated by build.zuo -- do not edit */\n"
++                                 "#include \"scheme.h\"\n\n"
++                                 (xxd-array petite-boot "petite_boot_data")
++                                 (xxd-array scheme-boot "scheme_boot_data")
++                                 "void static_boot_init(void) {\n"
++                                 "    Sregister_boot_file_bytes(\"petite.boot\",\n"
++                                 "        (void *)petite_boot_data, sizeof(petite_boot_data));\n"
++                                 "    Sregister_boot_file_bytes(\"scheme.boot\",\n"
++                                 "        (void *)scheme_boot_data, sizeof(scheme_boot_data));\n"
++                                 "}\n"))
++                           (display-to-file s path :truncate))]
++               [:target ,static-boot.o (,static-boot.c ,@kernel-hdrs)
++                        ,(lambda (.o token)
++                           (compile-one static-boot.c .o))]))
++           '())
++
+      ;; keep "_zuo.db" and "_zuo_tc.db" here instead of in "boot" and "bin" directories
+      [:db-dir ,(at-dir ".")]
+ 
+      [:target clean ()
+               ,(lambda (token)
+-                 (map rm* kernel-objs))]
++                 (map rm* (append kernel-objs static-objs)))]
+ 
+      #;(end-make-targets))))
+ 
+diff --git a/c/main.c b/c/main.c
+index bb46eefc..2f0c3a86 100644
+--- a/c/main.c
++++ b/c/main.c
+@@ -24,10 +24,22 @@
+   CUSTOM_INIT may be defined as a function with the signature shown to
+   perform boot-time initialization, e.g., registering foreign symbols.
+ ****/
+-#ifndef CUSTOM_INIT
++#ifdef CUSTOM_INIT
++extern void CUSTOM_INIT(void);
++#else
+ #define CUSTOM_INIT ((void (*)(void))0)
+ #endif /* CUSTOM_INIT */
+ 
++/****
++  STATIC_BOOT may be defined as a function that registers embedded boot
++  files via Sregister_boot_file_bytes() before Sbuild_heap() is called.
++  This is used for fully static builds where boot files are compiled
++  into the executable.
++****/
++#ifdef STATIC_BOOT
++extern void STATIC_BOOT(void);
++#endif
++
+ /****
+   ABNORMAL_EXIT may be defined as a function with the signature shown to
+   take some action, such as printing a special error message or performing
+@@ -79,6 +91,9 @@ int wmain(int argc, wchar_t* wargv[], wchar_t* wenvp[]) {
+ int main(int argc, const char *argv[]) {
+ #endif /* WIN32 */
+   int n, new_argc = 1;
++#ifdef STATIC_BOOT
++  int boot_given = 0;
++#endif
+ #ifdef SAVEDHEAPS
+   int compact = 1, savefile_level = 0;
+   const char *savefile = (char *)0;
+@@ -131,12 +146,18 @@ int main(int argc, const char *argv[]) {
+           exit(1);
+         }
+         Sregister_boot_executable_relative_file(execpath, argv[n]);
++#ifdef STATIC_BOOT
++        boot_given = 1;
++#endif
+       } else if (strcmp(arg,"-B") == 0 || strcmp(arg,"--Boot") == 0) {
+         if (++n == argc) {
+           (void) fprintf(stderr,"%s requires argument\n", arg);
+           exit(1);
+         }
+         Sregister_boot_relative_file(argv[n]);
++#ifdef STATIC_BOOT
++        boot_given = 1;
++#endif
+       } else if (strcmp(arg,"--eedisable") == 0) {
+   #ifdef FEATURE_EXPEDITOR
+         expeditor_enable = 0;
+@@ -291,6 +312,14 @@ int main(int argc, const char *argv[]) {
+     }
+   }
+ 
++ /* Register embedded boot files for static builds when no
++  * explicit boot files were given on the command line. */
++#ifdef STATIC_BOOT
++  if (!boot_given) {
++    STATIC_BOOT();
++  }
++#endif
++
+  /* must call Sbuild_heap after registering boot and heap files.
+   * Sbuild_heap() completes the initialization of the Scheme system
+   * and loads the boot or heap files.  If no boot or heap files have
+diff --git a/c/version.h b/c/version.h
+index 08a3b587..cd41231d 100644
+--- a/c/version.h
++++ b/c/version.h
+@@ -103,7 +103,9 @@ FORCEINLINE void store_unaligned_uptr(uptr *addr, uptr val) {
+ /* cosmo dylib support is experimental, disable when using cosmo libc
+    https://github.com/jart/cosmopolitan/blob/3.3.10/libc/dlopen/dlopen.c#L801 */
+ #ifndef __COSMOPOLITAN__
+-# define LOAD_SHARED_OBJECT
++# ifndef DISABLE_DYNAMIC_LOAD
++#  define LOAD_SHARED_OBJECT
++# endif
+ #endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+@@ -173,7 +175,9 @@ typedef int tputsputcchar;
+ 
+ #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__DragonFly__)
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+@@ -204,7 +208,9 @@ typedef int tputsputcchar;
+ # define NETBSD
+ #endif
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+@@ -236,7 +242,9 @@ typedef int tputsputcchar;
+ #define GETPAGESIZE() S_getpagesize()
+ #define GETWD(x) GETCWD(x, _MAX_PATH)
+ #define IEEE_DOUBLE
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_VIRTUAL_ALLOC
+ #define NAN_INCLUDE <math.h>
+ #define MAKE_NAN(x) { x = sqrt(-1.0); }
+@@ -306,7 +314,9 @@ struct timespec;
+ 
+ #if defined(__OpenBSD__) && !defined(__Bitrig__)
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+@@ -336,7 +346,9 @@ typedef int tputsputcchar;
+ #if defined(__APPLE__)
+ #define MACOSX
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+@@ -386,7 +398,9 @@ typedef int tputsputcchar;
+ 
+ #if defined(__QNX__)
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+@@ -414,7 +428,9 @@ typedef int tputsputcchar;
+ 
+ #if defined(sun)
+ #define NOBLOCK O_NONBLOCK
++#ifndef DISABLE_DYNAMIC_LOAD
+ #define LOAD_SHARED_OBJECT
++#endif
+ #define USE_MMAP
+ #define MMAP_HEAP
+ #define IEEE_DOUBLE
+diff --git a/configure b/configure
+index be2f9438..5669a4e2 100755
+--- a/configure
++++ b/configure
+@@ -86,6 +86,9 @@ installpetitename="petite"
+ installscriptname="scheme-script"
+ unamebits=""
+ relativeBootFiles=yes
++staticbuild=no
++disabledynload=no
++foreignLibs=""
+ disablex11=no
+ disablecurses=no
+ disableiconv=no
+@@ -402,6 +405,23 @@ while [ $# != 0 ] ; do
+     --disable-iconv)
+       disableiconv=yes
+       ;;
++    --static)
++      staticbuild=yes
++      disabledynload=yes
++      disablex11=yes
++      disablecurses=yes
++      disableiconv=yes
++      ;;
++    --foreign-libs=*)
++      foreignLibs=`echo $1 | sed -e 's/^--foreign-libs=//'`
++      if [ "$staticbuild" = "no" ]; then
++        staticbuild=yes
++        disabledynload=yes
++        disablex11=yes
++        disablecurses=yes
++        disableiconv=yes
++      fi
++      ;;
+     --enable-libffi)
+       libffi=yes
+       ;;
+@@ -684,6 +704,8 @@ if [ "$help" = "yes" ]; then
+   echo "  --disable-x11                     disable X11 support"
+   echo "  --disable-curses                  disable [n]curses support"
+   echo "  --disable-iconv                   disable iconv support"
++  echo "  --static                          build fully static binary"
++  echo "  --foreign-libs=<path:...>         statically link .a libs for FFI"
+   echo "  --enable-libffi                   enable libffi support for pb"
+   echo "  --disable-auto-flags              no auto additions to CFLAGS/LDFLAGS/LIBS"
+   echo "  --enable-warning-flags            add GCC warning flags to CFLAGS"
+@@ -891,23 +913,31 @@ fi
+ 
+ # Add automatic linking flags, unless suppressed by --disable-auto-flags
+ if [ "$addflags" = "yes" ] ; then
+-  case "${flagsm}" in
+-    *le|*gnu|*hk)
+-        LDFLAGS="${LDFLAGS} -rdynamic"
+-        ;;
+-    *fb|*nb)
+-        LDFLAGS="${LDFLAGS} -rdynamic -L/usr/local/lib"
+-        ;;
+-    *ob)
+-        LDFLAGS="${LDFLAGS} -rdynamic -Wl,--export-dynamic -Wl,-zwxneeded -L/usr/local/lib"
+-        ;;
+-    *)
+-        ;;
+-  esac
++  if [ "$staticbuild" = "yes" ] ; then
++    LDFLAGS="${LDFLAGS} -static"
++  else
++    case "${flagsm}" in
++      *le|*gnu|*hk)
++          LDFLAGS="${LDFLAGS} -rdynamic"
++          ;;
++      *fb|*nb)
++          LDFLAGS="${LDFLAGS} -rdynamic -L/usr/local/lib"
++          ;;
++      *ob)
++          LDFLAGS="${LDFLAGS} -rdynamic -Wl,--export-dynamic -Wl,-zwxneeded -L/usr/local/lib"
++          ;;
++      *)
++          ;;
++    esac
++  fi
+ 
+   case "${flagsm}" in
+     *le|*gnu)
+-        LIBS="${LIBS} -lm -ldl ${ncursesLib} -lrt"
++        if [ "$staticbuild" = "yes" ] ; then
++            LIBS="${LIBS} -lm -lrt"
++        else
++            LIBS="${LIBS} -lm -ldl ${ncursesLib} -lrt"
++        fi
+         ;;
+     *fb|*ob|*hk)
+         LIBS="${LIBS} ${iconvLib} -lm ${ncursesLib}"
+@@ -916,7 +946,11 @@ if [ "$addflags" = "yes" ] ; then
+         LIBS="${LIBS} -lm ${cursesLib} -lterminfo"
+         ;;
+     *s2)
+-        LIBS="${LIBS} -lnsl -ldl -lm ${cursesLib} -lrt"
++        if [ "$staticbuild" = "yes" ] ; then
++            LIBS="${LIBS} -lnsl -lm ${cursesLib} -lrt"
++        else
++            LIBS="${LIBS} -lnsl -ldl -lm ${cursesLib} -lrt"
++        fi
+         ;;
+     *osx)
+         LIBS="${LIBS} ${iconvLib} -lm ${ncursesLib}"
+@@ -1216,6 +1250,8 @@ installscriptname=$installscriptname
+ disablecurses=$disablecurses
+ disableiconv=$disableiconv
+ disablex11=$disablex11
++disabledynload=$disabledynload
++foreignLibs=$foreignLibs
+ enablelibffi=$libffi
+ preloadBootFiles=$preloadBootFiles
+ alwaysUseBootFile=$alwaysUseBootFile
+diff --git a/tools/gen-static-ffi.sh b/tools/gen-static-ffi.sh
+new file mode 100755
+index 00000000..3fb22e56
+--- /dev/null
++++ b/tools/gen-static-ffi.sh
+@@ -0,0 +1,61 @@
++#!/bin/sh
++# gen-static-ffi.sh
++# Generate a C source file that registers exported symbols from static
++# libraries (.a files) via Sforeign_symbol(), enabling FFI access to
++# statically linked libraries.
++#
++# Usage: gen-static-ffi.sh lib1.a [lib2.a ...] > static_ffi.c
++
++if [ $# -eq 0 ]; then
++    echo "Usage: $0 lib1.a [lib2.a ...] > static_ffi.c" >&2
++    exit 1
++fi
++
++# Verify all input files exist
++for lib in "$@"; do
++    if [ ! -f "$lib" ]; then
++        echo "Error: $lib not found" >&2
++        exit 1
++    fi
++done
++
++# Collect all exported text symbols from the .a files
++# nm --defined-only -g shows only globally defined symbols
++# Filter for type T (text/code) symbols
++syms=$(nm --defined-only -g "$@" 2>/dev/null | awk '$2 == "T" { print $3 }' | sort -u)
++
++if [ -z "$syms" ]; then
++    echo "Warning: no exported text symbols found in: $*" >&2
++fi
++
++cat <<'HEADER'
++/* Generated by gen-static-ffi.sh -- do not edit */
++#include "scheme.h"
++
++HEADER
++
++# Generate extern declarations
++for sym in $syms; do
++    # Skip compiler/runtime internal symbols (leading underscore on some platforms)
++    case "$sym" in
++        _*) continue ;;
++    esac
++    printf 'extern void %s();\n' "$sym"
++done
++
++cat <<'MIDDLE'
++
++void static_ffi_init(void) {
++MIDDLE
++
++# Generate Sforeign_symbol registrations
++for sym in $syms; do
++    case "$sym" in
++        _*) continue ;;
++    esac
++    printf '    Sforeign_symbol("%s", (void *)%s);\n' "$sym" "$sym"
++done
++
++cat <<'FOOTER'
++}
++FOOTER
+-- 
+2.39.5 (Apple Git-154)
+
diff --git a/vendor/ChezScheme-patches/0002-Harden-FASL-deserialization-FFI-and-build-scripts-ag.patch b/vendor/ChezScheme-patches/0002-Harden-FASL-deserialization-FFI-and-build-scripts-ag.patch
new file mode 100644
index 0000000..9898972
--- /dev/null
+++ b/vendor/ChezScheme-patches/0002-Harden-FASL-deserialization-FFI-and-build-scripts-ag.patch
@@ -0,0 +1,169 @@
+From b3f35405ff20b85ae270f279d9c84f5348e6cc44 Mon Sep 17 00:00:00 2001
+From: Jaime Fournier <jaimef@linbsd.org>
+Date: Sat, 21 Mar 2026 18:44:19 -0600
+Subject: [PATCH] Harden FASL deserialization, FFI, and build scripts against
+ malformed input
+
+Add bounds/overflow checks in fasl.c for compressed size calculation,
+uncompressed size, string building, and record field parsing to prevent
+heap corruption from crafted .so/.boot files. Add FFI array size overflow
+check, LZ4 size_t-to-INT truncation guards, sprintf->snprintf, strcpy->memcpy,
+and quote shell variables in configure script.
+
+Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
+---
+ c/compress-io.c |  5 +++--
+ c/fasl.c        | 17 ++++++++++++++---
+ c/ffi.c         |  2 ++
+ c/prim5.c       |  2 +-
+ c/scheme.c      |  2 +-
+ configure       | 10 +++++-----
+ 6 files changed, 26 insertions(+), 12 deletions(-)
+
+diff --git a/c/compress-io.c b/c/compress-io.c
+index fcbc989d..82cc9069 100644
+--- a/c/compress-io.c
++++ b/c/compress-io.c
+@@ -23,6 +23,7 @@
+ #include "lz4hc.h"
+ #include <fcntl.h>
+ #include <errno.h>
++#include <limits.h>
+ 
+ #ifdef WIN32
+ #include <io.h>
+@@ -429,8 +430,8 @@ static INT glzread_lz4(lz4File_in *lz4, void *buffer, UINT count) {
+           return -1;
+         }
+ 
+-        lz4->in_pos += (INT)in_len;
+-        lz4->out_len = (INT)out_len;
++        lz4->in_pos += (in_len > INT_MAX ? INT_MAX : (INT)in_len);
++        lz4->out_len = (out_len > INT_MAX ? INT_MAX : (INT)out_len);
+         lz4->out_pos = 0;
+       }
+     } else {
+diff --git a/c/fasl.c b/c/fasl.c
+index 1ef27694..a2370fa6 100644
+--- a/c/fasl.c
++++ b/c/fasl.c
+@@ -460,7 +460,12 @@ static ptr fasl_entry(ptr tc, IFASLCODE situation, faslFile f, ptr externals) {
+         case fasl_type_lz4: {
+           ptr result; INT bytes_consumed;
+           iptr dest_size = S_fasl_uptrin(f, &bytes_consumed);
+-          iptr src_size = size - (2 + bytes_consumed); /* adjust for u8 compression type, u8 fasl type, and uptr dest_size */
++          iptr src_size;
++          if (size < (iptr)(2 + bytes_consumed))
++            S_error1("", "malformed fasl-object found in ~a", f->uf.path);
++          src_size = size - (2 + bytes_consumed); /* adjust for u8 compression type, u8 fasl type, and uptr dest_size */
++          if (dest_size < 0)
++            S_error1("", "malformed fasl-object found in ~a", f->uf.path);
+ 
+           if ((uptr)src_size > (uptr)maximum_bytevector_length ||
+               (uptr)dest_size > (uptr)maximum_bytevector_length)
+@@ -483,6 +488,8 @@ static ptr fasl_entry(ptr tc, IFASLCODE situation, faslFile f, ptr externals) {
+         case fasl_type_uncompressed: {
+           in_f = f;
+           old_mode = f->buffer_mode;
++          if (size < 2)
++            S_error1("", "malformed fasl-object found in ~a", f->uf.path);
+           size -= 2;  /* adjust for u8 compression type and u8 fasl type */
+           if (size < 0)
+             S_error1("", "invalid fasl uncompressed size found in ~a", f->uf.path);
+@@ -1236,15 +1243,19 @@ static void faslin(ptr tc, ptr *x, ptr t, ptr *pstrbuf, faslFile f) {
+ # define unknown 3
+ #endif
+ static void fasl_record(ptr tc, ptr *x, ptr t, ptr *pstrbuf, faslFile f, iptr size) {
+-  iptr n; uptr addr; ptr p; UINT padty;
++  iptr n; uptr addr, addr_base, addr_limit; ptr p; UINT padty;
+ 
+   n = sizein(f);
+   *x = p = S_record(size_record_inst(size));
+   faslin(tc, &RECORDINSTTYPE(p), t, pstrbuf, f);
+-  addr = (uptr)TO_PTR(&RECORDINSTIT(p, 0));
++  addr_base = (uptr)TO_PTR(&RECORDINSTIT(p, 0));
++  addr_limit = addr_base + size;
++  addr = addr_base;
+   for (; n != 0; n -= 1) {
+     padty = bytein(f);
+     addr += padty >> 4;
++    if (addr < addr_base || addr >= addr_limit)
++      S_error1("", "malformed record in fasl object found in ~a", f->uf.path);
+     switch (padty & 0xf) {
+       case fasl_fld_ptr:
+         faslin(tc, TO_VOIDP(addr), t, pstrbuf, f);
+diff --git a/c/ffi.c b/c/ffi.c
+index c2aa7e79..79858793 100644
+--- a/c/ffi.c
++++ b/c/ffi.c
+@@ -253,6 +253,8 @@ ffi_type *decode_type(alloc_state *alloc, ptr type, ffi_abi abi, IBOOL *all_floa
+ 
+       ffi_prep_cif(&cif, abi, 0, elem_out, NULL);
+ 
++      if (len != 0 && elem_out->size > ((size_t)-1) / len)
++        S_error("decode_type", "ffi array size overflow");
+       out->size = elem_out->size * len;
+       out->alignment = elem_out->alignment;
+       out->type = FFI_TYPE_STRUCT;
+diff --git a/c/prim5.c b/c/prim5.c
+index e0fe02ec..f8089917 100644
+--- a/c/prim5.c
++++ b/c/prim5.c
+@@ -2228,7 +2228,7 @@ static ptr s_iconv_trouble(HMODULE h, const char *what) {
+   FreeLibrary(h);
+   n = strlen(what) + strlen(dll) + 17;
+   msg = (char *)malloc(n);
+-  sprintf(msg, "cannot find %s in %s", what, dll);
++  snprintf(msg, n, "cannot find %s in %s", what, dll);
+   free(dll);
+   r = Sstring_utf8(msg, -1);
+   free(msg);
+diff --git a/c/scheme.c b/c/scheme.c
+index 086e4237..d15c2eb6 100644
+--- a/c/scheme.c
++++ b/c/scheme.c
+@@ -651,7 +651,7 @@ static void add_boot(boot_desc *boot, const char *path) {
+     fprintf(stderr, "boot-file path is too long %s\n", path);
+     S_abnormal_exit();
+   }
+-  strcpy(boot->path, path);
++  memcpy(boot->path, path, strlen(path) + 1);
+ }
+ 
+ static IBOOL check_boot(faslFile f, IBOOL verbose, const char *path) {
+diff --git a/configure b/configure
+index 5669a4e2..5bc3802f 100755
+--- a/configure
++++ b/configure
+@@ -1190,23 +1190,23 @@ if [ "$skipImmediateMakefile" = "" ] ; then
+     else
+         makefile_in=Makefile.in
+     fi
+-    sed -e 's/$(w)/'$w'/g' "$srcdir"/makefiles/"$makefile_in" > Makefile
++    sed -e 's|$(w)|'"$w"'|g' "$srcdir"/makefiles/"$makefile_in" > Makefile
+ fi
+ 
+-mkdir -p $w
++mkdir -p "$w"
+ 
+ if [ "$skipImmediateMakefile" = "" ] ; then
+-    # Stub Zuo script to lanch the real one, using "Makefile"
++    # Stub Zuo script to launch the real one, using "Makefile"
+     # to locate the workarea:
+     cp "$srcdir"/makefiles/buildmain.zuo main.zuo
+ fi
+ 
+ # Some idea, but in the workarea, so it refers to "workarea.zuo" here:
+-cp "$srcdir"/makefiles/workmain.zuo $w/main.zuo
++cp "$srcdir"/makefiles/workmain.zuo "$w"/main.zuo
+ 
+ # The content of "$w/Mf-config" records configuration decisions,
+ # and the Zuo build script takes it from there
+-cat > $w/Mf-config << END
++cat > "$w"/Mf-config << END
+ srcdir=$srcdir
+ upsrcdir=$upsrcdir
+ m=$m
+-- 
+2.39.5 (Apple Git-154)
+
diff --git a/vendor/ChezScheme-patches/0003-Add-bounds-checks-in-vfasl-deserialization-fix-HPUX-.patch b/vendor/ChezScheme-patches/0003-Add-bounds-checks-in-vfasl-deserialization-fix-HPUX-.patch
new file mode 100644
index 0000000..90a3168
--- /dev/null
+++ b/vendor/ChezScheme-patches/0003-Add-bounds-checks-in-vfasl-deserialization-fix-HPUX-.patch
@@ -0,0 +1,81 @@
+From aaa82190e95cd09544733458563704af9330ebed Mon Sep 17 00:00:00 2001
+From: Jaime Fournier <jaimef@linbsd.org>
+Date: Sat, 21 Mar 2026 19:00:47 -0600
+Subject: [PATCH] Add bounds checks in vfasl deserialization, fix HPUX typo,
+ harden path_append
+
+- vfasl.c: bounds check in find_pointer_from_offset to prevent OOB read
+  from crafted vfasl offsets exceeding vspace_offsets array
+- vfasl.c: bounds check in lookup_singleton to reject invalid singleton
+  indices from malformed vfasl data
+- foreign.c: fix undefined variable 'name' -> 's' in HPUX branch
+- self-exe.c: integer overflow check in path_append malloc size calculation
+- findings.md: add round 2 findings including parallel GC races, signal
+  handler safety, and oblist resize race (unfixed design-level issues)
+
+Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
+---
+ c/foreign.c  | 2 +-
+ c/self-exe.c | 6 +++++-
+ c/vfasl.c    | 7 ++++++-
+ 3 files changed, 12 insertions(+), 3 deletions(-)
+
+diff --git a/c/foreign.c b/c/foreign.c
+index 42c7a6e6..14896d93 100644
+--- a/c/foreign.c
++++ b/c/foreign.c
+@@ -191,7 +191,7 @@ void Sforeign_symbol(const char *s, void *v) {
+     tc_mutex_acquire();
+ 
+ #ifdef HPUX
+-    v = proc2entry(v,name);
++    v = proc2entry(v,s);
+ #endif
+ 
+     if ((x = lookup(s)) == addr_to_ptr(0)) {
+diff --git a/c/self-exe.c b/c/self-exe.c
+index 500b9441..3ebe5098 100644
+--- a/c/self-exe.c
++++ b/c/self-exe.c
+@@ -254,7 +254,11 @@ static char *get_self_path_platform() { return NULL; }
+ static char *path_append(const char *s1, const char *s2) {
+   size_t l1 = strlen(s1);
+   size_t l2 = strlen(s2);
+-  char *r = (char *)malloc(l1 + l2 + 2);
++  size_t alloc_size = l1 + l2 + 2;
++  char *r;
++  if (alloc_size < l1 || alloc_size < l2)
++    return NULL;
++  r = (char *)malloc(alloc_size);
+   if (r == NULL) {
+     return NULL;
+   }
+diff --git a/c/vfasl.c b/c/vfasl.c
+index 066b993a..c321b846 100644
+--- a/c/vfasl.c
++++ b/c/vfasl.c
+@@ -582,8 +582,11 @@ static ptr find_pointer_from_offset(uptr p_off, ptr *vspaces, uptr *vspace_offse
+   ITYPE t = TYPEBITS(p_off);
+ 
+   p_off = (uptr)UNTYPE(p_off, t);
+-  while (p_off >= vspace_offsets[s+1])
++  while (p_off >= vspace_offsets[s+1]) {
+     s++;
++    if (s >= vspaces_count)
++      S_error("vfasl-read", "invalid pointer offset");
++  }
+ 
+   return TYPE(ptr_add(vspaces[s], p_off - vspace_offsets[s]), t);
+ }
+@@ -610,6 +613,8 @@ static ptr *singleton_refs[] = { &S_G.null_string,
+ static ptr lookup_singleton(iptr which) {
+   ptr v;
+ 
++  if (which < 1 || which > (iptr)(sizeof(singleton_refs) / sizeof(singleton_refs[0])))
++    S_error("vfasl-read", "invalid singleton index");
+   v = *(singleton_refs[which-1]);
+ 
+   if (v == Sfalse) {
+-- 
+2.39.5 (Apple Git-154)
+
diff --git a/vendor/ChezScheme-patches/0004-hard-links-are-for-suckers.patch b/vendor/ChezScheme-patches/0004-hard-links-are-for-suckers.patch
new file mode 100644
index 0000000..040496a
--- /dev/null
+++ b/vendor/ChezScheme-patches/0004-hard-links-are-for-suckers.patch
@@ -0,0 +1,30 @@
+From 5f9c697ccbe7b37b6ad27bd5315afee9ac68793c Mon Sep 17 00:00:00 2001
+From: Jaime Fournier <jaimef@linbsd.org>
+Date: Fri, 3 Apr 2026 17:44:46 -0600
+Subject: [PATCH] hard links are for suckers
+
+---
+ makefiles/install.zuo | 6 +-----
+ 1 file changed, 1 insertion(+), 5 deletions(-)
+
+diff --git a/makefiles/install.zuo b/makefiles/install.zuo
+index d0a89a09..804734e3 100644
+--- a/makefiles/install.zuo
++++ b/makefiles/install.zuo
+@@ -179,12 +179,8 @@
+     (shell/wait* "rm" "-f" f))
+   (define (rm-rf d)
+     (shell/wait* "rm" "-rf" d))
+-  (define haiku? (glob-match? "*hk" m))
+   (define (ln-f from to)
+-    (if haiku?
+-        ;; no hard links on BeFS
+-        (shell/wait* "ln" "-s" from to)
+-        (shell/wait* "ln" "-f" from to)))
++    (shell/wait* "cp" "-f" from to))