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 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-28 14:38:31 -07:00
parent 88fc104c54
commit 9289352454
6 changed files with 103 additions and 15 deletions

View File

@ -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 <file>`. High bytes (0x800xFF) 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 <name>` 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

View File

@ -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

View File

@ -1 +1 @@
0.8.24
0.8.25

View File

@ -78,7 +78,7 @@ set -o pipefail
# ─────────────────────────────────────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.8.24"
LARRY_VERSION="0.8.25"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ─────────────────────────────────────────────────────────────────────────────

View File

@ -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
}

View File

@ -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 <state>`
# 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/tty 2>/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/tty 2>/dev/null || stty "$_rh_saved" 2>/dev/null; trap - INT TERM HUP' INT TERM HUP
fi
stty -echo </dev/tty 2>/dev/null || stty -echo 2>/dev/null || true
IFS= read -r _rh_val </dev/tty 2>/dev/null || IFS= read -r _rh_val || true
if [ -n "$_rh_saved" ]; then
stty "$_rh_saved" </dev/tty 2>/dev/null || stty "$_rh_saved" 2>/dev/null || true
else
stty echo </dev/tty 2>/dev/null || stty echo 2>/dev/null || true
fi
trap - INT TERM HUP
else
IFS= read -r _rh_val </dev/tty 2>/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/tty || true
stty echo 2>/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/tty || true
stty echo 2>/dev/null
_read_hidden pw
echo "" >&2
if [ -n "$pw" ]; then
umask 077