Port to FreeBSD with security parity and static binary support

ober

925900b4df4d12fe2bb6f74f39ec43a078e5880f

diff --git a/CLAUDE.md b/CLAUDE.md
index 205ec7e..b52b0c9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -16,8 +16,9 @@ If the Docker build fails, fix it before doing anything else.
 ```bash
 make                     # Show help / list targets
 make build               # Compile all .sls → .so modules
-make docker              # Build fully static binary in Docker
-make secmon-musl-local   # Build static binary locally (needs musl)
+make secmon-static       # Build static binary on FreeBSD
+make docker              # Build fully static binary in Docker (Linux)
+make secmon-musl-local   # Build static binary locally (Linux, needs musl)
 make clean               # Remove all build artifacts
 ```
 
@@ -31,6 +32,9 @@ Reimplementation of the Rust security monitoring agent (~/mine/secmon) in Scheme
 - Crypto: Uses `(std crypto native-rust)` — Rust `ring` library via FFI. **Never use `(std crypto digest)` — it shells out to `openssl`.**
 - SQLite: Uses `(std db sqlite-native)` — Rust `rusqlite` via FFI
 - Prelude: Always use `(jerboa prelude clean)`, never `(jerboa prelude)` (has a bug with `while` export)
+- Platform: FreeBSD uses `(secmon platform freebsd)`, Linux uses `(secmon platform linux)`
+- Monitors use `on-freebsd?` runtime check: `(string-contains (symbol->string (machine-type)) "fb")`
+- FreeBSD errno: use `__error` (not `__errno_location`), no `prctl`, no `/proc/self/status`
 
 ## Architecture
 
@@ -38,7 +42,7 @@ Reimplementation of the Rust security monitoring agent (~/mine/secmon) in Scheme
 lib/secmon/
 ├── crypto/        # ECIES encryption, PSK auth, key generation
 ├── monitor/       # 16 security monitors + events + suspicious patterns
-├── platform/      # Linux /proc abstraction
+├── platform/      # Platform abstraction (linux.sls, freebsd.sls)
 ├── buffer/        # Encrypted event ring buffer
 ├── server/        # TCP listener + wire protocol
 ├── storage/       # SQLite event store
diff --git a/Makefile b/Makefile
index c0394b2..5c09665 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,10 @@
 SCHEME ?= scheme
-JERBOA ?= $(HOME)/mine/jerboa/lib
+JERBOA ?= $(HOME)/jerboa/lib
 LIBDIRS = lib:$(JERBOA)
 NATIVE_RS ?= $(HOME)/mine/jerboa/jerboa-native-rs/target/release
 export LD_LIBRARY_PATH := $(NATIVE_RS):$(LD_LIBRARY_PATH)
 
-.PHONY: all help build compile clean test keygen agent collector analyze docker secmon-musl secmon-musl-local verify-harden
+.PHONY: all help build compile clean test keygen agent collector analyze docker secmon-musl secmon-musl-local secmon-static verify-harden
 
 all: help
 
@@ -13,8 +13,9 @@ help:
 	@echo ""
 	@echo "Build:"
 	@echo "  make build             Compile all .sls → .so modules"
-	@echo "  make docker            Build fully static binary in Docker"
-	@echo "  make secmon-musl-local Build static binary locally (needs musl)"
+	@echo "  make secmon-static     Build static binary on FreeBSD"
+	@echo "  make docker            Build fully static binary in Docker (Linux)"
+	@echo "  make secmon-musl-local Build static binary locally (Linux, needs musl)"
 	@echo "  make clean             Remove all build artifacts"
 	@echo ""
 	@echo "Run:"
@@ -49,6 +50,12 @@ collector:
 analyze:
 	$(SCHEME) --libdirs $(LIBDIRS) --script bin/analyze.ss $(ARGS)
 
+# ─── FreeBSD Static Binary ───────────────────────────────────────────────────
+
+secmon-static:
+	@echo "=== Building static secmon-agent for FreeBSD ==="
+	./build-secmon-static.sh
+
 # ─── musl Static Binary ─────────────────────────────────────────────────────
 # Default: build via Docker for reproducibility and correct toolchain deps.
 # Use `make secmon-musl-local` to build directly on the host (requires musl-gcc,
diff --git a/bin/agent.ss b/bin/agent.ss
index 10da94a..db4ba4c 100644
--- a/bin/agent.ss
+++ b/bin/agent.ss
@@ -10,7 +10,7 @@
   (secmon server listener)
   (secmon monitor events)
   (secmon platform provider)
-  (secmon platform linux)
+  (secmon platform freebsd)
   (secmon monitor process)
   (secmon monitor network)
   (secmon monitor files)
@@ -61,8 +61,8 @@
                [psk-auth (make-psk-auth psk)]
                [buffer (make-event-buffer encryptor
                          (agent-config-max-buffer-size config))]
-               [proc-provider (create-linux-process-provider)]
-               [net-provider (create-linux-network-provider)]
+               [proc-provider (create-freebsd-process-provider)]
+               [net-provider (create-freebsd-network-provider)]
                [hostname ((process-provider-get-hostname proc-provider))]
                [poll-ms (agent-config-poll-interval-ms config)])
 
diff --git a/build-all.ss b/build-all.ss
index 66de070..0994f2e 100644
--- a/build-all.ss
+++ b/build-all.ss
@@ -9,7 +9,7 @@
   (secmon monitor suspicious)
   ;; Platform
   (secmon platform provider)
-  (secmon platform linux)
+  (secmon platform freebsd)
   ;; Buffer
   (secmon buffer ring)
   ;; Server
diff --git a/build-secmon-static.sh b/build-secmon-static.sh
new file mode 100755
index 0000000..83e54f2
--- /dev/null
+++ b/build-secmon-static.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+# build-secmon-static.sh — Build static secmon-agent binary on FreeBSD
+set -eu
+
+JERBOA_LIB="${JERBOA_DIR:-$HOME/jerboa/lib}"
+NATIVE_RS="${NATIVE_RS:-$HOME/jerboa/jerboa-native-rs/target/release}"
+
+echo "=== Building secmon-agent (FreeBSD static) ==="
+echo "  Jerboa lib: $JERBOA_LIB"
+echo "  Native RS:  $NATIVE_RS"
+echo ""
+
+export LD_LIBRARY_PATH="${NATIVE_RS}:${LD_LIBRARY_PATH:-}"
+
+scheme -q --libdirs "lib:${JERBOA_LIB}" < build-secmon-static.ss
+
+echo ""
+echo "=== Build complete ==="
+if [ -f secmon-agent ]; then
+    echo "  Size: $(ls -lh secmon-agent | awk '{print $5}')"
+    file secmon-agent
+fi
diff --git a/build-secmon-static.ss b/build-secmon-static.ss
new file mode 100644
index 0000000..64048b7
--- /dev/null
+++ b/build-secmon-static.ss
@@ -0,0 +1,490 @@
+#!chezscheme
+;;; build-secmon-static.ss — Build a fully static secmon-agent binary on FreeBSD
+;;;
+;;; Usage: scheme -q --libdirs lib:<jerboa-lib> < build-secmon-static.ss
+;;;
+;;; This script:
+;;;   1. Stages jerboa modules with load-shared-object patched out
+;;;   2. Compiles all jerboa + secmon modules from staged/patched sources
+;;;   3. Creates boot file + optimized program .so
+;;;   4. Generates C files with embedded boot data + FFI symbol registration
+;;;   5. Compiles C with cc against Chez's scheme.h
+;;;   6. Links fully static binary with libkernel.a + libjerboa_native.a
+;;;
+;;; The resulting secmon-agent binary has zero runtime dependencies.
+
+(import
+  (except (chezscheme) void box box? unbox set-box!
+          andmap ormap iota last-pair find
+          1+ 1- fx/ fx1+ fx1-
+          error error? raise with-exception-handler identifier?
+          hash-table? make-hash-table)
+  (jerboa build))
+
+;; ========== Locate directories ==========
+
+(define chez-lib-dir
+  (or (getenv "CHEZ_LIB_DIR")
+      "/usr/local/lib/csv10.3.0/ta6fb"))
+
+(printf "Chez lib dir: ~a~n" chez-lib-dir)
+
+(define jerboa-dir
+  (or (getenv "JERBOA_DIR")
+      (let ([home (getenv "HOME")])
+        (or (let ([p (format "~a/jerboa/lib" home)])
+              (and (file-exists? p) p))
+            (format "~a/mine/jerboa/lib" home)))))
+
+(define native-lib-path
+  (or (getenv "NATIVE_LIB")
+      (format "~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a"
+              (getenv "HOME"))))
+
+(define has-native-lib? (file-exists? native-lib-path))
+(unless has-native-lib?
+  (printf "ERROR: libjerboa_native.a not found at ~a~n" native-lib-path)
+  (printf "Build with: cd ~/jerboa/jerboa-native-rs && cargo build --release~n")
+  (exit 1))
+
+(printf "Native lib: ~a~n~n" native-lib-path)
+
+;; ========== Step 0: Stage and patch jerboa modules for static builds ==========
+
+(printf "[0/7] Patching jerboa modules for static build (no dlopen)...~n")
+
+(define jerboa-stage (format "~a/jerboa-stage" (current-directory)))
+(system (format "rm -rf '~a'" jerboa-stage))
+(system (format "mkdir -p '~a'" jerboa-stage))
+
+;; Copy the jerboa lib tree contents to staging
+(system (format "cp -a '~a/.' '~a/'" jerboa-dir jerboa-stage))
+
+;; Patch all load-shared-object calls in staged copies
+(system (format "find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object [^)]*)/(void)/g' {} +" jerboa-stage))
+;; Delete pre-compiled .so/.wpo files to force recompilation from patched sources
+(system (format "find '~a' -name '*.so' -delete" jerboa-stage))
+(system (format "find '~a' -name '*.wpo' -delete" jerboa-stage))
+
+(printf "  Patched jerboa sources staged in ~a~n" jerboa-stage)
+
+;; ========== Override library-directories ==========
+
+(define build-lib-dirs
+  (list (cons "lib" "lib")
+        (cons jerboa-stage jerboa-stage)))
+
+;; ========== Step 1: Compile jerboa boot modules from staged sources ==========
+
+(printf "~n[1/7] Compiling jerboa modules (patched, from stage)...~n")
+
+(define jerboa-boot-modules
+  '("jerboa/core" "jerboa/runtime"
+    "std/error" "std/format" "std/sort" "std/pregexp" "std/sugar"
+    "std/misc/string" "std/misc/list" "std/misc/alist" "std/misc/thread"
+    "std/misc/ports"
+    "std/foreign"
+    "std/os/path" "std/os/file-info"
+    "std/text/hex" "std/text/json"
+    "std/match2"
+    "std/crypto/native-rust"
+    "std/db/sqlite-native"
+    "std/gambit-compat"
+    "jerboa/ffi"
+    "jerboa/prelude/clean"))
+
+(parameterize ([compile-imported-libraries #t]
+               [optimize-level 2]
+               [generate-inspector-information #f]
+               [library-directories build-lib-dirs])
+  (for-each
+    (lambda (m)
+      (let ([sls (format "~a/~a.sls" jerboa-stage m)])
+        (when (file-exists? sls)
+          (printf "  Compiling ~a~n" m)
+          (compile-library sls))))
+    jerboa-boot-modules))
+
+;; ========== Step 2: Compile all secmon modules ==========
+
+(printf "~n[2/7] Compiling secmon modules...~n")
+
+(parameterize ([optimize-level 2]
+               [generate-inspector-information #f]
+               [compile-imported-libraries #t]
+               [library-directories build-lib-dirs])
+  ;; Obfuscate macro (needed by many modules — compile first)
+  (printf "  Compiling lib/secmon/stealth/obfuscate.sls~n")
+  (compile-library "lib/secmon/stealth/obfuscate.sls")
+
+  ;; Crypto modules (dependency order)
+  (for-each
+    (lambda (m)
+      (let ([path (format "lib/secmon/crypto/~a.sls" m)])
+        (printf "  Compiling ~a~n" path)
+        (compile-library path)))
+    '("keys" "ecies" "psk"))
+
+  ;; Monitor events (base types first)
+  (printf "  Compiling lib/secmon/monitor/events.sls~n")
+  (compile-library "lib/secmon/monitor/events.sls")
+  (printf "  Compiling lib/secmon/monitor/suspicious.sls~n")
+  (compile-library "lib/secmon/monitor/suspicious.sls")
+
+  ;; Platform — FreeBSD instead of Linux
+  (for-each
+    (lambda (m)
+      (let ([path (format "lib/secmon/platform/~a.sls" m)])
+        (printf "  Compiling ~a~n" path)
+        (compile-library path)))
+    '("provider" "freebsd"))
+
+  ;; Buffer
+  (printf "  Compiling lib/secmon/buffer/ring.sls~n")
+  (compile-library "lib/secmon/buffer/ring.sls")
+
+  ;; Server
+  (for-each
+    (lambda (m)
+      (let ([path (format "lib/secmon/server/~a.sls" m)])
+        (printf "  Compiling ~a~n" path)
+        (compile-library path)))
+    '("protocol" "listener"))
+
+  ;; Config
+  (printf "  Compiling lib/secmon/config.sls~n")
+  (compile-library "lib/secmon/config.sls")
+
+  ;; Storage
+  (printf "  Compiling lib/secmon/storage/store.sls~n")
+  (compile-library "lib/secmon/storage/store.sls")
+
+  ;; All monitor modules
+  (for-each
+    (lambda (m)
+      (let ([path (format "lib/secmon/monitor/~a.sls" m)])
+        (printf "  Compiling ~a~n" path)
+        (compile-library path)))
+    '("process" "network" "files" "auth" "kernel" "cron"
+      "container" "dns" "rootkit" "persistence" "revshell"
+      "lateral" "logtamper" "webshell" "podman" "selinux"))
+
+  ;; Stealth modules
+  (for-each
+    (lambda (m)
+      (let ([path (format "lib/secmon/stealth/~a.sls" m)])
+        (printf "  Compiling ~a~n" path)
+        (compile-library path)))
+    '("anti-debug" "masquerade" "env-sanitize" "integrity" "init")))
+
+;; ========== Step 3: Compile agent program ==========
+
+(printf "~n[3/7] Compiling bin/agent.ss (optimize-level 3)...~n")
+
+(parameterize ([compile-imported-libraries #t]
+               [optimize-level 3]
+               [cp0-effort-limit 500]
+               [cp0-score-limit 50]
+               [cp0-outer-unroll-limit 1]
+               [commonization-level 4]
+               [enable-unsafe-application #t]
+               [enable-unsafe-variable-reference #t]
+               [enable-arithmetic-left-associative #t]
+               [debug-level 0]
+               [generate-inspector-information #f]
+               [library-directories build-lib-dirs])
+  (compile-program "bin/agent.ss"))
+
+;; ========== Step 4: Create libs-only boot file ==========
+
+(printf "~n[4/7] Creating boot file...~n")
+
+(define boot-libs
+  (append
+    ;; Jerboa runtime + stdlib (from STAGE dir — patched, no load-shared-object)
+    (map (lambda (m) (format "~a/~a.so" jerboa-stage m))
+      '("jerboa/core"
+        "jerboa/runtime"
+        "jerboa/ffi"
+        "jerboa/prelude/clean"
+        "std/error"
+        "std/format"
+        "std/sort"
+        "std/pregexp"
+        "std/sugar"
+        "std/match2"
+        "std/misc/string"
+        "std/misc/list"
+        "std/misc/alist"
+        "std/misc/thread"
+        "std/misc/ports"
+        "std/foreign"
+        "std/os/path"
+        "std/os/file-info"
+        "std/text/hex"
+        "std/text/json"
+        "std/gambit-compat"
+        "std/crypto/native-rust"
+        "std/db/sqlite-native"))
+    ;; Secmon modules (obfuscate first — many modules depend on it)
+    (list "lib/secmon/stealth/obfuscate.so")
+    (map (lambda (m) (format "lib/secmon/crypto/~a.so" m))
+      '("keys" "ecies" "psk"))
+    (list "lib/secmon/monitor/events.so"
+          "lib/secmon/monitor/suspicious.so")
+    (map (lambda (m) (format "lib/secmon/platform/~a.so" m))
+      '("provider" "freebsd"))
+    (list "lib/secmon/buffer/ring.so"
+          "lib/secmon/server/protocol.so"
+          "lib/secmon/server/listener.so"
+          "lib/secmon/config.so"
+          "lib/secmon/storage/store.so")
+    (map (lambda (m) (format "lib/secmon/monitor/~a.so" m))
+      '("process" "network" "files" "auth" "kernel" "cron"
+        "container" "dns" "rootkit" "persistence" "revshell"
+        "lateral" "logtamper" "webshell" "podman" "selinux"))
+    (map (lambda (m) (format "lib/secmon/stealth/~a.so" m))
+      '("anti-debug" "masquerade" "env-sanitize" "integrity" "init"))))
+
+;; Verify all .so files exist
+(for-each
+  (lambda (so)
+    (unless (file-exists? so)
+      (printf "ERROR: Missing boot file dependency: ~a~n" so)
+      (exit 1)))
+  boot-libs)
+
+(apply make-boot-file "secmon-agent.boot" '("scheme" "petite") boot-libs)
+(printf "  Created secmon-agent.boot~n")
+
+;; ========== Step 5: Generate C code ==========
+
+(printf "~n[5/7] Generating C code with embedded boot files + FFI symbols...~n")
+
+(define petite-boot-path (format "~a/petite.boot" chez-lib-dir))
+(define scheme-boot-path (format "~a/scheme.boot" chez-lib-dir))
+
+;; Generate the C source
+(call-with-output-file "secmon-program.c"
+  (lambda (out)
+    ;; Headers
+    (display "#include \"scheme.h\"\n" out)
+    (display "#include <string.h>\n" out)
+    (display "#include <stdlib.h>\n" out)
+    (display "#include <stdio.h>\n" out)
+    (display "#include <signal.h>\n" out)
+    (display "#include <unistd.h>\n" out)
+    (display "#include <errno.h>\n" out)
+    (display "#include <fcntl.h>\n" out)
+    (display "#include <sys/types.h>\n" out)
+    (display "#include <sys/socket.h>\n" out)
+    (display "#include <sys/wait.h>\n" out)
+    (display "#include <sys/mman.h>\n" out)
+    (display "#include <sys/ptrace.h>\n" out)
+    (display "#include <sys/sysctl.h>\n" out)
+    (display "#include <netinet/in.h>\n" out)
+    (display "#include <arpa/inet.h>\n\n" out)
+
+    ;; Embed boot files as C arrays
+    (display (file->c-array petite-boot-path "petite_boot") out)
+    (newline out)
+    (display (file->c-array scheme-boot-path "scheme_boot") out)
+    (newline out)
+    (display (file->c-array "secmon-agent.boot" "app_boot") out)
+    (newline out)
+
+    ;; Embed the compiled program
+    (display (file->c-array "bin/agent.so" "program_so") out)
+    (newline out)
+
+    ;; Extern declarations for Rust native library symbols
+    (display "/* Rust native library symbols (libjerboa_native.a) */\n" out)
+    (for-each
+      (lambda (sym)
+        (fprintf out "extern int ~a();\n" sym))
+      '("jerboa_last_error"
+        "jerboa_sha1" "jerboa_sha256" "jerboa_sha384" "jerboa_sha512"
+        "jerboa_random_bytes"
+        "jerboa_hmac_sha256" "jerboa_hmac_sha256_verify"
+        "jerboa_timing_safe_equal"
+        "jerboa_aead_seal" "jerboa_aead_open"
+        "jerboa_chacha20_seal" "jerboa_chacha20_open"
+        "jerboa_scrypt"
+        "jerboa_pbkdf2_derive" "jerboa_pbkdf2_verify"
+        "jerboa_x25519_generate_keypair"
+        "jerboa_x25519_public_from_private"
+        "jerboa_x25519_diffie_hellman"
+        "jerboa_hkdf_sha256"
+        ;; SQLite
+        "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec"
+        "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset"
+        "jerboa_sqlite_bind_int" "jerboa_sqlite_bind_double"
+        "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob"
+        "jerboa_sqlite_bind_null"
+        "jerboa_sqlite_step"
+        "jerboa_sqlite_column_count" "jerboa_sqlite_column_type"
+        "jerboa_sqlite_column_int" "jerboa_sqlite_column_double"
+        "jerboa_sqlite_column_text" "jerboa_sqlite_column_blob"
+        "jerboa_sqlite_column_name"
+        "jerboa_sqlite_last_insert_rowid" "jerboa_sqlite_changes"
+        "jerboa_sqlite_errmsg"
+        ;; Process control
+        "jerboa_proc_self_exe" "jerboa_kill_probe"
+        "jerboa_prctl_set_name" "jerboa_mlockall"
+        ;; FreeBSD security
+        "jerboa_freebsd_is_traced" "jerboa_freebsd_process_count"
+        "jerboa_setproctitle"))
+    (newline out)
+
+    ;; FFI symbol registration function
+    (display "static void register_ffi_symbols(void) {\n" out)
+    ;; libc symbols (needed by secmon monitors and jerboa std)
+    (for-each
+      (lambda (sym)
+        (fprintf out "    Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym))
+      '("socket" "connect" "setsockopt" "bind" "listen" "accept"
+        "fork" "ptrace" "waitpid" "_exit" "kill"
+        "getpid" "getppid" "mlockall"
+        "readlink" "unsetenv" "access"
+        "__error"
+        "read" "write" "close"))
+    ;; Rust native symbols
+    (for-each
+      (lambda (sym)
+        (fprintf out "    Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym))
+      '("jerboa_last_error"
+        "jerboa_sha1" "jerboa_sha256" "jerboa_sha384" "jerboa_sha512"
+        "jerboa_random_bytes"
+        "jerboa_hmac_sha256" "jerboa_hmac_sha256_verify"
+        "jerboa_timing_safe_equal"
+        "jerboa_aead_seal" "jerboa_aead_open"
+        "jerboa_chacha20_seal" "jerboa_chacha20_open"
+        "jerboa_scrypt"
+        "jerboa_pbkdf2_derive" "jerboa_pbkdf2_verify"
+        "jerboa_x25519_generate_keypair"
+        "jerboa_x25519_public_from_private"
+        "jerboa_x25519_diffie_hellman"
+        "jerboa_hkdf_sha256"
+        "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec"
+        "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset"
+        "jerboa_sqlite_bind_int" "jerboa_sqlite_bind_double"
+        "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob"
+        "jerboa_sqlite_bind_null"
+        "jerboa_sqlite_step"
+        "jerboa_sqlite_column_count" "jerboa_sqlite_column_type"
+        "jerboa_sqlite_column_int" "jerboa_sqlite_column_double"
+        "jerboa_sqlite_column_text" "jerboa_sqlite_column_blob"
+        "jerboa_sqlite_column_name"
+        "jerboa_sqlite_last_insert_rowid" "jerboa_sqlite_changes"
+        "jerboa_sqlite_errmsg"
+        ;; FreeBSD-specific
+        "jerboa_proc_self_exe" "jerboa_kill_probe"
+        "jerboa_prctl_set_name" "jerboa_mlockall"
+        "jerboa_freebsd_is_traced" "jerboa_freebsd_process_count"
+        "jerboa_setproctitle"))
+    (display "}\n\n" out)
+
+    ;; dlopen/dlsym stubs — needed because Chez tries to use them
+    ;; even when all symbols are pre-registered via Sforeign_symbol
+    (display "/* dlopen/dlsym stubs for static builds */\n" out)
+    (display "void *dlopen(const char *file, int mode) {\n" out)
+    (display "    (void)file; (void)mode;\n" out)
+    (display "    return (void*)1; /* non-NULL = success */\n" out)
+    (display "}\n\n" out)
+
+    (display "void *dlsym(void *handle, const char *name) {\n" out)
+    (display "    (void)handle; (void)name;\n" out)
+    (display "    return NULL;\n" out)
+    (display "}\n\n" out)
+
+    (display "int dlclose(void *handle) {\n" out)
+    (display "    (void)handle;\n" out)
+    (display "    return 0;\n" out)
+    (display "}\n\n" out)
+
+    (display "char *dlerror(void) {\n" out)
+    (display "    return \"dlopen not supported in static build\";\n" out)
+    (display "}\n\n" out)
+
+    ;; Main function: bootstrap Chez and run the agent
+    ;; Uses Sscheme_script for threading support (unlike Sscheme_program
+    ;; which evaluates at heap build time before threads are ready).
+    ;; Writes program .so to a temp file, loads it, then unlinks.
+    (display "int main(int argc, const char *argv[]) {\n" out)
+    (display "    Sscheme_init(NULL);\n" out)
+    (display "    Sregister_boot_file_bytes(\"petite\", petite_boot, petite_boot_len);\n" out)
+    (display "    Sregister_boot_file_bytes(\"scheme\", scheme_boot, scheme_boot_len);\n" out)
+    (display "    Sregister_boot_file_bytes(\"secmon\", app_boot, app_boot_len);\n" out)
+    (display "    Sbuild_heap(NULL, NULL);\n\n" out)
+    (display "    /* Register FFI symbols after heap is built */\n" out)
+    (display "    register_ffi_symbols();\n\n" out)
+    ;; Write program .so to temp file and load via Sscheme_script
+    (display "    /* Write program to temp file for Sscheme_script (threading support) */\n" out)
+    (display "    char tmppath[] = \"/tmp/.secmon-XXXXXX\";\n" out)
+    (display "    int fd = mkstemp(tmppath);\n" out)
+    (display "    if (fd >= 0) {\n" out)
+    (display "        write(fd, program_so, program_so_len);\n" out)
+    (display "        close(fd);\n" out)
+    (display "        const char *script_args[] = { argv[0] };\n" out)
+    (display "        Sscheme_script(tmppath, 1, script_args);\n" out)
+    (display "        unlink(tmppath);\n" out)
+    (display "    }\n\n" out)
+    (display "    Sscheme_deinit();\n" out)
+    (display "    return 0;\n" out)
+    (display "}\n" out))
+  'replace)
+
+(printf "  Generated secmon-program.c~n")
+
+;; ========== Step 6: Compile C ==========
+
+(printf "~n[6/7] Compiling C...~n")
+
+(define scheme-h-dir chez-lib-dir)
+
+(let ([cmd (format "cc -c -O2 -I'~a' -o secmon-program.o secmon-program.c" scheme-h-dir)])
+  (printf "  ~a~n" cmd)
+  (unless (= (system cmd) 0)
+    (printf "ERROR: C compilation failed~n")
+    (exit 1)))
+
+;; ========== Step 7: Link static binary ==========
+
+(printf "~n[7/7] Linking static binary...~n")
+
+(define libkernel (format "~a/libkernel.a" chez-lib-dir))
+(define libz (let ([p (format "~a/libz.a" chez-lib-dir)])
+               (if (file-exists? p) p #f)))
+(define liblz4 (let ([p (format "~a/liblz4.a" chez-lib-dir)])
+                 (if (file-exists? p) p #f)))
+
+(let* ([libs (filter values
+               (list libkernel libz liblz4 native-lib-path))]
+       [lib-flags (apply string-append
+                    (map (lambda (l) (format " '~a'" l)) libs))]
+       [cmd (format "cc -static secmon-program.o~a -lncursesw -lm -lthr -lc -o secmon-agent"
+                    lib-flags)])
+  (printf "  ~a~n" cmd)
+  (unless (= (system cmd) 0)
+    (printf "ERROR: Linking failed~n")
+    (exit 1)))
+
+;; Strip the binary
+(printf "~n  Stripping...~n")
+(system "strip secmon-agent")
+
+;; Generate integrity hash
+(printf "  Generating SHA256 hash...~n")
+(system "sha256 secmon-agent > secmon-agent.sha256")
+
+;; Verify
+(printf "~n=== Build complete ===~n")
+(system "ls -lh secmon-agent")
+(system "file secmon-agent")
+
+;; Cleanup
+(printf "~n  Cleaning up staging directory...~n")
+(system (format "rm -rf '~a'" jerboa-stage))
+(system "rm -f secmon-program.c secmon-program.o secmon-agent.boot")
+
+(printf "~nDone.~n")
diff --git a/lib/secmon/monitor/auth.sls b/lib/secmon/monitor/auth.sls
index 1770225..57b9b0e 100644
--- a/lib/secmon/monitor/auth.sls
+++ b/lib/secmon/monitor/auth.sls
@@ -33,6 +33,7 @@
     (cond
       [(file-exists? "/var/log/auth.log") "/var/log/auth.log"]
       [(file-exists? "/var/log/secure") "/var/log/secure"]
+      [(file-exists? "/var/log/messages") "/var/log/messages"]  ;; FreeBSD fallback
       [else #f]))
 
   (define (file-size-safe path)
@@ -166,8 +167,40 @@
           (vector-for-each (lambda (k) (hashtable-delete! known-users k)) keys))
         (for-each (lambda (u) (hashtable-set! known-users u #t)) current-users))))
 
-  ;; Read utmp binary file (384-byte records, ut_type=7 means USER_PROCESS)
+  ;; Read logged-in users. Uses `who` command on FreeBSD (different utmp format),
+  ;; binary utmp parsing on Linux.
+  (define on-freebsd?
+    (string-contains (symbol->string (machine-type)) "fb"))
+
   (define (read-utmp-users)
+    (if on-freebsd?
+      (read-utmp-users-who)
+      (read-utmp-users-linux)))
+
+  ;; Cross-platform: parse `who` output
+  (define (read-utmp-users-who)
+    (guard (e [#t '()])
+      (let ([output (shell-output "who 2>/dev/null")])
+        (filter-map
+          (lambda (line)
+            (let ([parts (filter (lambda (s) (not (string=? s "")))
+                           (string-split (string-trim line) #\space))])
+              (and (not (null? parts)) (car parts))))
+          (filter (lambda (s) (not (string=? s "")))
+            (string-split output #\newline))))))
+
+  (define (shell-output cmd)
+    (guard (e [#t ""])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (current-transcoder))])
+        (close-port to-stdin)
+        (let ([output (get-string-all from-stdout)])
+          (close-port from-stdout)
+          (close-port from-stderr)
+          output))))
+
+  ;; Linux: binary utmp parsing (384-byte records, ut_type=7 means USER_PROCESS)
+  (define (read-utmp-users-linux)
     (guard (e [#t '()])
       (let ([path (if (file-exists? "/var/run/utmp") "/var/run/utmp" "/run/utmp")])
         (if (not (file-exists? path)) '()
@@ -183,7 +216,6 @@
                     (let ([ut-type (bytevector-s32-ref bv offset (endianness little))])
                       (if (= ut-type 7)  ;; USER_PROCESS
                         (let ([user (extract-utmp-string bv (+ offset 8) 32)])
-                          (loop (+ offset record-size))
                           (cons user (loop (+ offset record-size))))
                         (loop (+ offset record-size)))))))))))))
 
diff --git a/lib/secmon/monitor/container.sls b/lib/secmon/monitor/container.sls
index a22d98f..7c655ab 100644
--- a/lib/secmon/monitor/container.sls
+++ b/lib/secmon/monitor/container.sls
@@ -7,6 +7,9 @@
     (secmon monitor suspicious)
     (secmon stealth obfuscate))
 
+  (define on-freebsd?
+    (string-contains (symbol->string (machine-type)) "fb"))
+
   ;; Container escape monitor: detects escape attempts from containers/jails
   (define (spawn-container-escape-monitor emit! poll-ms hostname)
     (fork-thread
@@ -40,49 +43,72 @@
                   (list (obfstr "/var/run/docker.sock")
                         (obfstr "/run/docker.sock")
                         (obfstr "/var/run/containerd/containerd.sock")))
-                ;; Check capabilities
-                (let ([caps (read-effective-caps)])
-                  (when caps
-                    ;; Full caps = privileged container
-                    (when (and (not reported-privileged)
-                               (or (= caps #x3fffffffff)
-                                   (= caps #xffffffffffffffff)))
-                      (set! reported-privileged #t)
-                      (emit! (make-escape-event hostname "privileged_container"
-                               "" 'critical)))
-                    ;; Check individual dangerous caps
-                    (for-each
-                      (lambda (cap-pair)
-                        (let ([bit (car cap-pair)] [name (cdr cap-pair)])
-                          (when (and (not (zero? (bitwise-and caps
-                                                   (bitwise-arithmetic-shift-left 1 bit))))
-                                     (not (hashtable-ref reported-caps name #f)))
-                            (hashtable-set! reported-caps name #t)
-                            (emit! (make-escape-event hostname "dangerous_capability"
-                                     name 'high)))))
-                      dangerous-capabilities))))
+                ;; Check capabilities / jail restrictions
+                (if on-freebsd?
+                  ;; FreeBSD: check jail security restrictions via sysctl
+                  (check-jail-restrictions emit! hostname reported-caps)
+                  ;; Linux: check Linux capabilities
+                  (let ([caps (read-effective-caps)])
+                    (when caps
+                      ;; Full caps = privileged container
+                      (when (and (not reported-privileged)
+                                 (or (= caps #x3fffffffff)
+                                     (= caps #xffffffffffffffff)))
+                        (set! reported-privileged #t)
+                        (emit! (make-escape-event hostname "privileged_container"
+                                 "" 'critical)))
+                      ;; Check individual dangerous caps
+                      (for-each
+                        (lambda (cap-pair)
+                          (let ([bit (car cap-pair)] [name (cdr cap-pair)])
+                            (when (and (not (zero? (bitwise-and caps
+                                                     (bitwise-arithmetic-shift-left 1 bit))))
+                                       (not (hashtable-ref reported-caps name #f)))
+                              (hashtable-set! reported-caps name #t)
+                              (emit! (make-escape-event hostname "dangerous_capability"
+                                       name 'high)))))
+                        dangerous-capabilities)))))
               (sleep (make-time 'time-duration (* (mod poll-ms 1000) 1000000) (quotient poll-ms 1000)))
               (loop)))))))
 
   (define (is-containerized?)
-    ;; Check for container indicators
-    (or (file-exists? (obfstr "/.dockerenv"))
-        (file-exists? (obfstr "/run/.containerenv"))
-        (guard (e [#t #f])
-          (let ([cgroup (read-file-safe (obfstr "/proc/1/cgroup"))])
-            (and cgroup
-                 (or (string-contains cgroup (obfstr "docker"))
-                     (string-contains cgroup (obfstr "lxc"))
-                     (string-contains cgroup (obfstr "kubepods"))))))))
+    (if on-freebsd?
+      ;; FreeBSD: check if running inside a jail
+      (guard (e [#t #f])
+        (let ([output (shell-output "sysctl -n security.jail.jailed 2>/dev/null")])
+          (string=? (string-trim output) "1")))
+      ;; Linux: check for container indicators
+      (or (file-exists? (obfstr "/.dockerenv"))
+          (file-exists? (obfstr "/run/.containerenv"))
+          (guard (e [#t #f])
+            (let ([cgroup (read-file-safe (obfstr "/proc/1/cgroup"))])
+              (and cgroup
+                   (or (string-contains cgroup (obfstr "docker"))
+                       (string-contains cgroup (obfstr "lxc"))
+                       (string-contains cgroup (obfstr "kubepods")))))))))
 
   (define (read-mounts)
     (guard (e [#t '()])
-      (let ([lines (local-read-file-lines (obfstr "/proc/mounts"))])
-        (filter-map
-          (lambda (line)
-            (let ([parts (string-split line #\space)])
-              (and (>= (length parts) 2) (cadr parts))))
-          lines))))
+      (if on-freebsd?
+        ;; FreeBSD: parse mount command output
+        (let ([lines (shell-output-lines "/sbin/mount 2>/dev/null")])
+          (filter-map
+            (lambda (line)
+              ;; Format: /dev/ada0p2 on / (ufs, local, ...)
+              (let ([on-pos (string-contains line " on ")])
+                (and on-pos
+                     (let ([rest (substring line (+ on-pos 4) (string-length line))])
+                       (let ([space (string-contains rest " (")])
+                         (if space (substring rest 0 space)
+                           (string-trim rest)))))))
+            lines))
+        ;; Linux: parse /proc/mounts
+        (let ([lines (local-read-file-lines (obfstr "/proc/mounts"))])
+          (filter-map
+            (lambda (line)
+              (let ([parts (string-split line #\space)])
+                (and (>= (length parts) 2) (cadr parts))))
+            lines)))))
 
   (define (suspicious-mount? mount)
     (or (string-prefix? (obfstr "/host") mount)
@@ -90,6 +116,32 @@
         (string-contains mount (obfstr "docker.sock"))
         (string-contains mount (obfstr "containerd.sock"))))
 
+  ;; FreeBSD jail restriction checks via sysctl security.jail.*
+  ;; Jails that have relaxed restrictions are dangerous — equivalent to
+  ;; Linux's dangerous capabilities.
+  (define jail-restriction-checks
+    ;; (sysctl-name . description) — value "1" means ALLOWED (dangerous)
+    '(("security.jail.allow_raw_sockets" . "raw_sockets")
+      ("security.jail.mount_allowed" . "mount_allowed")
+      ("security.jail.chflags_allowed" . "chflags_allowed")
+      ("security.jail.sysvipc_allowed" . "sysvipc_allowed")))
+
+  (define (check-jail-restrictions emit! hostname reported-caps)
+    (guard (e [#t (void)])
+      (for-each
+        (lambda (check)
+          (let ([sysctl-name (car check)]
+                [cap-name (cdr check)])
+            (guard (e [#t (void)])
+              (let ([output (shell-output
+                              (format "/sbin/sysctl -n ~a 2>/dev/null" sysctl-name))])
+                (when (string=? (string-trim output) "1")
+                  (unless (hashtable-ref reported-caps cap-name #f)
+                    (hashtable-set! reported-caps cap-name #t)
+                    (emit! (make-escape-event hostname "dangerous_jail_permission"
+                             cap-name 'high))))))))
+        jail-restriction-checks)))
+
   (define (read-effective-caps)
     (guard (e [#t #f])
       (let ([lines (local-read-file-lines (obfstr "/proc/self/status"))])
@@ -115,6 +167,20 @@
             (string-split content #\newline))
           '()))))
 
+  (define (shell-output cmd)
+    (guard (e [#t ""])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (current-transcoder))])
+        (close-port to-stdin)
+        (let ([output (get-string-all from-stdout)])
+          (close-port from-stdout)
+          (close-port from-stderr)
+          output))))
+
+  (define (shell-output-lines cmd)
+    (filter (lambda (s) (not (string=? s "")))
+      (string-split (shell-output cmd) #\newline)))
+
   (define (make-escape-event hostname detection-type detail severity)
     (let ([data (make-hashtable string-hash string=?)])
       (hashtable-set! data "detection_type" detection-type)
diff --git a/lib/secmon/monitor/kernel.sls b/lib/secmon/monitor/kernel.sls
index 4d76974..0ee458a 100644
--- a/lib/secmon/monitor/kernel.sls
+++ b/lib/secmon/monitor/kernel.sls
@@ -6,12 +6,15 @@
     (secmon monitor events)
     (secmon monitor suspicious))
 
+  (define on-freebsd?
+    (string-contains (symbol->string (machine-type)) "fb"))
+
   ;; Kernel module monitor: detects module loads/unloads
   (define (spawn-kernel-module-monitor emit! poll-ms hostname)
     (fork-thread
       (lambda ()
         (let ([known-modules (make-hashtable string-hash string=?)])
-          ;; Baseline from /proc/modules
+          ;; Baseline
           (for-each
             (lambda (mod)
               (hashtable-set! known-modules (car mod) (cadr mod)))
@@ -45,14 +48,57 @@
 
   (define (read-kernel-modules)
     (guard (e [#t '()])
-      (let ([lines (local-read-file-lines "/proc/modules")])
+      (if on-freebsd?
+        (read-kernel-modules-freebsd)
+        (read-kernel-modules-linux))))
+
+  ;; Linux: parse /proc/modules
+  (define (read-kernel-modules-linux)
+    (let ([lines (local-read-file-lines "/proc/modules")])
+      (filter-map
+        (lambda (line)
+          (let ([parts (string-split line #\space)])
+            (and (>= (length parts) 2)
+                 (list (car parts)
+                       (or (string->number (cadr parts)) 0)))))
+        lines)))
+
+  ;; FreeBSD: parse kldstat output
+  ;; Format: Id Refs Address            Size     Name
+  ;;          1    1 0xffffffff80200000 1a3b2f0  kernel
+  (define (read-kernel-modules-freebsd)
+    (let ([lines (shell-output-lines "/sbin/kldstat 2>/dev/null")])
+      (if (null? lines) '()
         (filter-map
           (lambda (line)
-            (let ([parts (string-split line #\space)])
-              (and (>= (length parts) 2)
-                   (list (car parts)
-                         (or (string->number (cadr parts)) 0)))))
-          lines))))
+            (let ([parts (filter (lambda (s) (not (string=? s "")))
+                           (string-split (string-trim line) #\space))])
+              (and (>= (length parts) 5)
+                   ;; Skip header (first field is "Id")
+                   (string->number (car parts))
+                   (let ([name (list-ref parts 4)]
+                         [size-hex (list-ref parts 3)])
+                     (list name
+                           (or (string->number size-hex 16) 0))))))
+          (cdr lines)))))
+
+  (define (shell-output-lines cmd)
+    (guard (e [#t '()])
+      (let ([output (shell-output cmd)])
+        (if (string=? output "")
+          '()
+          (filter (lambda (s) (not (string=? s "")))
+            (string-split output #\newline))))))
+
+  (define (shell-output cmd)
+    (guard (e [#t ""])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (current-transcoder))])
+        (close-port to-stdin)
+        (let ([output (get-string-all from-stdout)])
+          (close-port from-stdout)
+          (close-port from-stderr)
+          output))))
 
   (define (local-read-file-lines path)
     (guard (e [#t '()])
diff --git a/lib/secmon/monitor/rootkit.sls b/lib/secmon/monitor/rootkit.sls
index 1fb7e94..7606b77 100644
--- a/lib/secmon/monitor/rootkit.sls
+++ b/lib/secmon/monitor/rootkit.sls
@@ -7,6 +7,9 @@
     (secmon stealth obfuscate)
     (secmon platform provider))
 
+  (define on-freebsd?
+    (string-contains (symbol->string (machine-type)) "fb"))
+
   ;; Rootkit monitor: detects hidden processes via /proc vs kill() discrepancy
   (define (spawn-rootkit-monitor proc-provider emit! poll-ms hostname)
     (fork-thread
@@ -22,19 +25,20 @@
                 ;; Build visible set
                 (for-each (lambda (pid) (hashtable-set! visible-set pid #t))
                   visible-pids)
-                ;; Collect known thread IDs
-                (for-each
-                  (lambda (pid)
-                    (guard (e [#t (void)])
-                      (let ([task-dir (format "/proc/~a/task" pid)])
-                        (when (file-exists? task-dir)
-                          (for-each
-                            (lambda (tid-str)
-                              (let ([tid (string->number tid-str)])
-                                (when tid
-                                  (hashtable-set! thread-set tid #t))))
-                            (directory-list task-dir))))))
-                  visible-pids)
+                ;; Collect known thread IDs (Linux only — FreeBSD threads share PIDs)
+                (unless on-freebsd?
+                  (for-each
+                    (lambda (pid)
+                      (guard (e [#t (void)])
+                        (let ([task-dir (format "/proc/~a/task" pid)])
+                          (when (file-exists? task-dir)
+                            (for-each
+                              (lambda (tid-str)
+                                (let ([tid (string->number tid-str)])
+                                  (when tid
+                                    (hashtable-set! thread-set tid #t))))
+                              (directory-list task-dir))))))
+                    visible-pids))
                 ;; Probe PID ranges
                 (let ([hidden-pids '()])
                   ;; 1. PIDs 1-1000
@@ -71,7 +75,7 @@
                         (emit! (make-rootkit-event hostname pid
                                  "hidden_process" 'critical))))
                     hidden-pids))
-                ;; Check /proc/loadavg discrepancy
+                ;; Check loadavg discrepancy
                 (check-loadavg-discrepancy visible-pids emit! hostname)))
             (sleep (make-time 'time-duration (* (mod poll-ms 1000) 1000000) (quotient poll-ms 1000)))
             (loop))))))
@@ -82,34 +86,60 @@
     (let ([result (kill-fn pid 0)])
       (or (= result 0)
           ;; Check errno for EPERM (exists but permission denied)
-          (let ([errno-fn (foreign-procedure "__errno_location" () uptr)])
-            (let ([errno-ptr (errno-fn)])
-              (= (foreign-ref 'int errno-ptr 0) 1))))))  ;; EPERM = 1
+          (if on-freebsd?
+            (let ([errno-fn (foreign-procedure "__error" () uptr)])
+              (let ([errno-ptr (errno-fn)])
+                (= (foreign-ref 'int errno-ptr 0) 1)))  ;; EPERM = 1
+            (let ([errno-fn (foreign-procedure "__errno_location" () uptr)])