security: validate terminal raw restore state

ober

29b9c7d2043839670922a22f055c5e6234573b1d

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 59d222b..051b068 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -1142,7 +1142,8 @@ fix must add its scanner rule in the same commit (write it into
   and fails closed for legacy mutable Git uninstalls instead of constructing an
   `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)`.
+  `(std pkg commands)`. Terminal raw-mode restore now accepts only validated
+  `stty -g` state tokens before invoking the restore command.
 
 ### K3-P2-06 — Documentation consistency pass
 **Serves:** G5. **Effort:** 2 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 1c78fc1..236b3d1 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -1007,6 +1007,7 @@ These are known gaps documented as current limitations, not implementation promi
   creates registry directories without shelling out and disables legacy mutable
   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.
 - **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 6b1fb80..c06c30b 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. | 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. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
 
 ## Compatibility Notes
 
diff --git a/lib/std/misc/terminal.ss b/lib/std/misc/terminal.ss
index e8b245b..c55fb3e 100644
--- a/lib/std/misc/terminal.ss
+++ b/lib/std/misc/terminal.ss
@@ -250,6 +250,24 @@
 
   ;; ========== Raw mode ==========
 
+  (def (stty-state-token? s)
+    ;; Portable `stty -g` output is a single colon/equal/hex-style token.
+    ;; Reject whitespace and shell metacharacters before using it in restore.
+    (and (string? s)
+         (> (string-length s) 0)
+         (let loop ([i 0])
+           (or (= i (string-length s))
+               (let ([ch (string-ref s i)])
+                 (and (or (char-alphabetic? ch)
+                          (char-numeric? ch)
+                          (char=? ch #\:)
+                          (char=? ch #\=)
+                          (char=? ch #\,)
+                          (char=? ch #\.)
+                          (char=? ch #\-)
+                          (char=? ch #\_))
+                      (loop (+ i 1))))))))
+
   (def (with-raw-mode thunk)
     ;; Save terminal settings, switch to raw mode, run thunk, restore.
     ;; Uses stty since we don't want to depend on FFI/ioctl.
@@ -261,7 +279,9 @@
                           (open-process-ports "stty -g </dev/tty"
                                               'line (native-transcoder))])
               (close-port to)
-              (set! saved (string-trim (read-all from)))
+              (let ([token (string-trim (read-all from))])
+                (when (stty-state-token? token)
+                  (set! saved token)))
               (close-port from)
               (close-port err))
          (catch (exn) (void)))
@@ -271,8 +291,8 @@
         thunk
         (lambda ()
           ;; Restore saved settings
-          (when (and saved (> (string-length saved) 0))
-            (try (system (string-append "stty " saved " </dev/tty 2>/dev/null"))
+          (when saved
+            (try (system (string-append "stty " saved " </dev/tty 2>/dev/null")) ; jerboa-security: suppress system-command-string-concat -- saved is accepted only after stty-state-token? rejects whitespace and shell metacharacters
          (catch (exn) (void))))))))
 
   ;; ========== Alternate screen ==========