security: restrict native loader system candidates

ober

f485a7077ca9e785866f62b3bb142cbe6dbf2397

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 051b068..2b0b7a8 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -1143,7 +1143,9 @@ fix must add its scanner rule in the same commit (write it into
   `rm -rf` command. `jpkg env -- COMMAND` is disabled until the package CLI has
   an argv exec/status API, removing the last shell-concatenation path from
   `(std pkg commands)`. Terminal raw-mode restore now accepts only validated
-  `stty -g` state tokens before invoking the restore command.
+  `stty -g` state tokens before invoking the restore command. The shared native
+  loader now makes fixed-system-library bootstrap loads scanner-visible and
+  rejects non-literal system-library candidates at macro expansion time.
 
 ### K3-P2-06 — Documentation consistency pass
 **Serves:** G5. **Effort:** 2 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 236b3d1..2256e12 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -1008,6 +1008,10 @@ These are known gaps documented as current limitations, not implementation promi
   Git uninstall. `jpkg env -- COMMAND` fails closed until the package CLI has a
   real argv exec/status API instead of a shell-concatenated compatibility path.
   Terminal raw-mode restore accepts only validated `stty -g` state tokens.
+- **Native loader system paths are literal-only.** `(std native-loader)` probes
+  libc from the process image or fixed operating-system paths, requires a clean
+  dynamic-loader environment before any fallback load, and rejects non-literal
+  candidates to `native-loader-try-system-symbol!` at macro expansion time.
 - **No red team evaluation.** No independent adversarial testing has been performed.
 - **Secure memory still exposes a raw region escape hatch.** The high-level
   `secure-bytevector` API is bounds-checked and integrated with
diff --git a/docs/status.md b/docs/status.md
index c06c30b..58c7bad 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -29,7 +29,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
 | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. |
 | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup for requested axes, macOS Seatbelt deny-default path/exec/no-network setup for supported axes, standard worker-eval Capsicum entry on FreeBSD, explicit sandbox-axis refusal, egress proxy env wiring, and platform CI smoke for Linux/macOS/FreeBSD sandbox paths. | Keep Linux/macOS/FreeBSD parity tests current; finish arbitrary-command/proxy-aware Capsicum worker paths. |
 | Fuzzing | `tests/fuzz/corpus/` has 15 checked-in seed inputs, `tests/fuzz/regression/` has 11 crash/rejection regressions, `make fuzz-smoke` runs the deterministic regression gate first, and GitHub CI runs smoke fuzzing normally plus deep fuzzing on scheduled daily runs. | Keep adding minimized corpus and regression inputs for every parser/security bug found. |
-| Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. Core compatibility helpers avoid shell construction for recursive directory creation and process-status cleanup; the retired `(jerboa registry)` surface fails closed for mutable Git uninstall; `jpkg env -- COMMAND` is disabled until there is an argv exec/status API; terminal raw-mode restore validates `stty -g` state tokens. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
+| Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. Core compatibility helpers avoid shell construction for recursive directory creation and process-status cleanup; the retired `(jerboa registry)` surface fails closed for mutable Git uninstall; `jpkg env -- COMMAND` is disabled until there is an argv exec/status API; terminal raw-mode restore validates `stty -g` state tokens; native-loader system candidates are literal fixed paths. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
 
 ## Compatibility Notes
 
diff --git a/lib/std/native-loader.ss b/lib/std/native-loader.ss
index 6eea2c6..ae5d33e 100644
--- a/lib/std/native-loader.ss
+++ b/lib/std/native-loader.ss
@@ -87,7 +87,7 @@
     (when path
       (native-loader-require-clean-environment! 'std/native-loader))
     (guard (condition [else #f])
-      (load-shared-object path)
+      (load-shared-object path) ; jerboa-security: suppress native-fasl-load-from-dynamic-path -- path is either #f or a fixed OS libc candidate from system-libc-paths; caller-controlled native paths use native-loader-validate-library!
       (bootstrap-symbols-available?)))
 
   (define system-libc-loaded
@@ -136,24 +136,39 @@
   ;; a new symbol visible to another library, so safe system-library loading is
   ;; exported as a macro and expands at each call site.
   (define-syntax native-loader-try-system-symbol!
-    (syntax-rules ()
-      [(_ who symbol candidate ...)
-       (let ([required-symbol symbol])
-         (define (try-candidate path)
-           (unless (or (not path)
-                       (and (string? path)
-                            (> (string-length path) 1)
-                            (char=? (string-ref path 0) #\/)))
-             (error who
-                    "system-library candidates must be #f or absolute paths"
-                    path))
-           (when path
-             (native-loader-require-clean-environment! who))
-           (guard (condition [else #f])
-             (load-shared-object path)
-             (foreign-entry? required-symbol)))
-         (or (foreign-entry? required-symbol)
-             (try-candidate candidate) ...))]))
+    (lambda (stx)
+      (define (literal-system-candidate? datum)
+        (or (not datum)
+            (and (string? datum)
+                 (> (string-length datum) 1)
+                 (char=? (string-ref datum 0) #\/))))
+      (syntax-case stx ()
+        [(_ who symbol candidate ...)
+         (let ([candidates (syntax->datum #'(candidate ...))])
+           (let loop ([remaining candidates])
+             (unless (null? remaining)
+               (unless (literal-system-candidate? (car remaining))
+                 (syntax-violation
+                   'native-loader-try-system-symbol!
+                   "system-library candidates must be literal #f or absolute path strings"
+                   stx))
+               (loop (cdr remaining))))
+           #'(let ([required-symbol symbol])
+               (define (try-candidate path)
+                 (unless (or (not path)
+                             (and (string? path)
+                                  (> (string-length path) 1)
+                                  (char=? (string-ref path 0) #\/)))
+                   (error who
+                          "system-library candidates must be #f or absolute paths"
+                          path))
+                 (when path
+                   (native-loader-require-clean-environment! who))
+                 (guard (condition [else #f])
+                   (load-shared-object path) ; jerboa-security: suppress native-fasl-load-from-dynamic-path -- candidate is a macro-verified literal #f or absolute system-library path string; project native libraries use native-loader-validate-library!
+                   (foreign-entry? required-symbol)))
+               (or (foreign-entry? required-symbol)
+                   (try-candidate candidate) ...)))])))
 
   (define-syntax native-loader-ensure-system-symbol!
     (syntax-rules ()
diff --git a/tests/test-native-loader-policy.sh b/tests/test-native-loader-policy.sh
index e79a470..bcd5f84 100755
--- a/tests/test-native-loader-policy.sh
+++ b/tests/test-native-loader-policy.sh
@@ -219,13 +219,20 @@ cat > "$tmp/system-macro-negative.ss" <<'EOF'
   (exit 1))
 EOF
 rm -f "$marker"
-(
+if (
   cd "$tmp/cwd"
   JERBOA_LOADER_BARE_CANDIDATE="$(basename "$library")" \
     JERBOA_SENTINEL_MARKER="$marker" \
     "$scheme" --libdirs "$root/lib:$root/vendor/jsqlite/src" \
-      --script "$tmp/system-macro-negative.ss"
-)
+      --script "$tmp/system-macro-negative.ss" \
+      >"$tmp/system-macro-negative.out" \
+      2>"$tmp/system-macro-negative.err"
+); then
+  echo "system loader accepted a runtime candidate expression" >&2
+  exit 1
+fi
+grep -q 'system-library candidates must be literal #f or absolute path strings' \
+  "$tmp/system-macro-negative.err"
 test ! -e "$marker"
 
 if test "$(id -u)" -ne 0; then