From 92893524542272a50bc5810e894475d7ae121765 Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Thu, 28 May 2026 14:38:31 -0700 Subject: [PATCH] v0.8.25: fix terminal corruption from larry tools (control-byte + tty leaks) nc-document.sh now sanitizes C0 control bytes (except tab/LF/CR) at the single output sink, so raw ESC sequences embedded in NetConfig/.tcl content can no longer flip the terminal's mode and break line editing when output isn't redirected. ssh-helper.sh password prompts save/restore termios via stty -g + trap so a ^C mid-prompt no longer leaves echo off. UTF-8 preserved; portable. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ MANIFEST | 10 +++++----- VERSION | 2 +- larry.sh | 2 +- lib/nc-document.sh | 26 ++++++++++++++++++++++++-- lib/ssh-helper.sh | 43 +++++++++++++++++++++++++++++++++++++------ 6 files changed, 103 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad1710..09d1a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here. Versioning is loose-semver; bumps trigger the in-process self-update on every running client via `LARRY_BASE_URL` + `MANIFEST`. +## v0.8.25 — 2026-05-28 + +**★ FIX: terminal line-editing corruption after `larry tools …` runs (Bryan, +reproduced on his live Cloverleaf box).** After running `larry tools nc-document` +(and `tbn`-class searches), his interactive shell's line editing broke — +backspace printed a literal `^H` instead of erasing, arrow keys died, and he had +to run `stty sane`/`reset` to recover. Two independent causes, both fixed +belt-and-suspenders: + +- **Cause 1 — CONTROL-BYTE OUTPUT LEAK (the one Bryan hit).** `lib/nc-document.sh` + reads arbitrary NetConfig and `.tcl` proc source — author comments, the + `--raw-tcl` verbatim appendix (`cat "$apath"`), and xlate bodies — and emits it + to stdout unsanitised. Any raw C0 control byte in that content, **especially ESC + (0x1B)** and DECSET mode-switch sequences (`ESC[?1049h`, `ESC[?25l`, etc.), flips + the terminal into a different mode when the doc is printed (i.e. NOT redirected), + wrecking line editing. **Fix:** a single sanitiser at the one output choke-point + (`out_target` → new `_sanitize_ctl`) strips C0 controls except TAB/LF/CR via + `LC_ALL=C tr -d '\001-\010\013\014\016-\037\177'`. Applies to BOTH stdout and + `--out `. High bytes (0x80–0xFF) are untouched, so UTF-8 names/comments + survive. Portable to AIX/Linux/BSD/Cygwin (POSIX `tr` octal ranges; no GNU-only + flags). The non-interactive `tools ` dispatch in `larry.sh` itself touches + NO termios (it `exec`s the lib tool before any trap/mouse-mode is installed), + confirming the leak was the tool's own output bytes. + +- **Cause 2 — TTY-MODE LEAK on SIGINT (latent, hardened).** `lib/ssh-helper.sh` + read hidden passwords with a bare `stty -echo … read … stty echo`. A Ctrl-C (or + EOF/signal) BETWEEN the two `stty` calls left echo permanently OFF — terminal + corrupted until `stty sane`/`reset`. **Fix:** new `_read_hidden` saves the full + prior termios with `stty -g` and restores it via a `trap … INT TERM HUP` plus an + explicit restore on normal return, so every interrupt path restores the tty. + Reads from `/dev/tty`. Both hidden-password prompts (`cmd_pass`, `cmd_setup` + re-prompt) now route through it. + +`bash -n` clean on all touched files. MANIFEST regenerated. + ## v0.8.24 — 2026-05-28 **★ PLAIN-TEXT output rewrite of `lib/nc-document.sh` (Bryan's priority — OneNote diff --git a/MANIFEST b/MANIFEST index f7617f2..4e89300 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,16 +23,16 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh b3af140dcb8518ea98327b33177bcef8973ca223ad1f46f90ad6dba0e9f7ded7 +larry.sh dd9fdba88d20c4472826ef9355a7a0aa3d6bcf1d9d040aff9f07a2bb1351a287 larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423 # Metadata -VERSION 05d77ce62e5abe7212c0fa5ca747f16348d012c39016080e99c10f8f7f5e20bf +VERSION 7a2e873644ed9f1015114f43072f374eb0e1716bb1a97e73daa1337474d3e1fd MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde -CHANGELOG.md 5432e98a75500cf4cb7d1fa171124d64e693865215d4612f5071294a4f871f6a +CHANGELOG.md c659506dea3dea5b1cc53ef41f978219d6e9894b4fc4a05df07df660a56f79be # Agent personas (system-prompt overlays) agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 @@ -52,7 +52,7 @@ lib/fetch-safe.sh abecf0045b9856f63ffa346119443c11de56547344be32bddaed9fbae6b021 lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b # Secure SSH with ControlMaster (password hidden from Larry-the-LLM) -lib/ssh-helper.sh bd205aa87bc9e53821cac45888faa9434c1e182bee2bf16d6d838dcb79bfac3e +lib/ssh-helper.sh b8442e1e086eed7afebc77e1948c9c688301a8ef4b14f563470396aceccdddd4 # v0.8.6: work-box → Mac headers.log sync (tsk-2026-05-27-023). Incremental, # offset-tracked push of $LARRY_HOME/log/headers.log to a daemon-watched path @@ -102,7 +102,7 @@ lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79 lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c -lib/nc-document.sh 56808d5ba86ca6b355f405bf33db7de62e8894a5582987e9de1eeee77a8ab3a8 +lib/nc-document.sh 13aef8f6335ee63966fdd74678c506fb4ed7ed749f750e7daad142258e518f41 lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324 lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a diff --git a/VERSION b/VERSION index 0a08824..4039074 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.24 +0.8.25 diff --git a/larry.sh b/larry.sh index 6082443..fcbd61d 100755 --- a/larry.sh +++ b/larry.sh @@ -78,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.24" +LARRY_VERSION="0.8.25" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── diff --git a/lib/nc-document.sh b/lib/nc-document.sh index 683f1b3..5f743b3 100755 --- a/lib/nc-document.sh +++ b/lib/nc-document.sh @@ -249,12 +249,34 @@ _locate_thread() { # ───────────────────────────────────────────────────────────────────────────── # Output sink # ───────────────────────────────────────────────────────────────────────────── +# +# v0.8.25 — CONTROL-BYTE SANITIZER (terminal-corruption fix). +# This tool reads arbitrary NetConfig and .tcl source (author comments, raw proc +# bodies via --raw-tcl, xlate bodies). That content can contain raw C0 control +# bytes — most dangerously ESC (0x1B). When the doc is printed to a terminal +# (NOT redirected to a file), a stray ESC sequence flips the terminal into a +# different mode and wrecks line editing (backspace prints ^H, arrows die), +# forcing `stty sane`/`reset` to recover. We neutralise it at the single output +# choke-point so EVERY emitted byte is clean, whether bound for stdout or --out. +# +# Strip set (octal, POSIX `tr` ranges — portable to AIX/Linux/BSD/Cygwin): +# \001-\010 SOH..BS (drops BS ^H, the literal-backspace culprit) +# [keep \011 TAB, \012 LF] +# \013\014 VT, FF +# [keep \015 CR — legit in MobaXterm/Windows-tainted content] +# \016-\037 SO..US (drops ESC 0x1B, the mode-flip culprit) +# \177 DEL +# High bytes (0x80-0xFF) are NOT touched, so UTF-8 names/comments survive intact. +# LC_ALL=C forces byte-wise operation (AIX `tr` is locale-sensitive otherwise). +_sanitize_ctl() { + LC_ALL=C tr -d '\001-\010\013\014\016-\037\177' 2>/dev/null || cat +} out_target() { if [ -n "$OUT" ]; then mkdir -p "$(dirname "$OUT")" 2>/dev/null - cat > "$OUT" + _sanitize_ctl > "$OUT" else - cat + _sanitize_ctl fi } diff --git a/lib/ssh-helper.sh b/lib/ssh-helper.sh index c5ca1f1..ee47270 100755 --- a/lib/ssh-helper.sh +++ b/lib/ssh-helper.sh @@ -77,6 +77,41 @@ die() { printf 'ssh-helper: %s\n' "$*" >&2; exit 1; } warn() { printf 'ssh-helper: warn: %s\n' "$*" >&2; } ok() { printf 'ssh-helper: %s\n' "$*"; } +# v0.8.25 — SAFE HIDDEN-PASSWORD READ (terminal-corruption fix). +# Previously the hidden-password prompts did a bare `stty -echo` ... read ... +# `stty echo`. If the user hit Ctrl-C (or the read got an EOF/signal) BETWEEN +# those two stty calls, echo was never re-enabled and the terminal was left +# corrupted (typing invisible) — recoverable only with `stty sane`/`reset`. +# This wrapper SAVES the full prior termios state with `stty -g` and restores +# it via a trap on EXIT/INT/TERM/HUP, so any interrupt path restores the tty. +# Reads from /dev/tty (the real terminal), not stdin. Portable: `stty -g`/`stty ` +# is POSIX and works on AIX/Linux/BSD/Cygwin; no GNU-only flags. +# _read_hidden VARNAME +# Sets the named variable to the line read (no echo). Returns 0 always (empty +# input is the caller's "abort" signal). +_read_hidden() { # varname + local _rh_var="$1" _rh_val="" _rh_saved="" + if command -v stty >/dev/null 2>&1 && { [ -t 0 ] || [ -e /dev/tty ]; }; then + _rh_saved=$(stty -g /dev/null || stty -g 2>/dev/null || true) + # Restore on ANY exit from the prompt window — normal return OR ^C/kill/HUP. + if [ -n "$_rh_saved" ]; then + trap 'stty "$_rh_saved" /dev/null || stty "$_rh_saved" 2>/dev/null; trap - INT TERM HUP' INT TERM HUP + fi + stty -echo /dev/null || stty -echo 2>/dev/null || true + IFS= read -r _rh_val /dev/null || IFS= read -r _rh_val || true + if [ -n "$_rh_saved" ]; then + stty "$_rh_saved" /dev/null || stty "$_rh_saved" 2>/dev/null || true + else + stty echo /dev/null || stty echo 2>/dev/null || true + fi + trap - INT TERM HUP + else + IFS= read -r _rh_val /dev/null || IFS= read -r _rh_val || true + fi + # Assign back to the caller's variable name without eval-injection risk. + printf -v "$_rh_var" '%s' "$_rh_val" +} + # v0.7.5: shared CR-safety primitives. pull/push use `wc -c | tr -d ' '` to # verify byte counts — Cygwin wc.exe can pass through \r and tank the # `[ "$got" != "$local_size" ]` comparison. @@ -296,9 +331,7 @@ cmd_pass() { ensure_layout printf 'Password for %s (input is hidden; press Enter when done): ' "$alias" >&2 local pw="" - stty -echo 2>/dev/null - IFS= read -r pw /dev/null + _read_hidden pw echo "" >&2 [ -n "$pw" ] || die "no password entered" umask 077 @@ -431,9 +464,7 @@ cmd_setup() { printf 'ssh-helper: looks like the stored password is stale (this host rotates ~every 12h).\n' >&2 printf 'Enter a FRESH password for %s (input hidden; Enter to abort): ' "$alias" >&2 local pw="" - stty -echo 2>/dev/null - IFS= read -r pw /dev/null + _read_hidden pw echo "" >&2 if [ -n "$pw" ]; then umask 077