Skip to content

Unescape lsof \x20 process names; fix empty-first-sample suppression - #73

Merged
heznpc merged 1 commit into
mainfrom
fix/lsof-escaped-command-names
Aug 13, 2026
Merged

Unescape lsof \x20 process names; fix empty-first-sample suppression#73
heznpc merged 1 commit into
mainfrom
fix/lsof-escaped-command-names

Conversation

@heznpc

@heznpc heznpc commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Two bugs in lsof output handling, both confirmed against this machine's real output rather than hypothesized. Found while preparing review targets after #72 merged -- re-reading the real lsof capture from that PR's verification (Codex\x20 1142 ...) surfaced the first, and the first's new test fixture incidentally surfaced the second.

1. \x20-escaped process names leaked verbatim into the UI

lsof escapes a space inside a COMMAND name as literal \x20 ("Codex "Codex\x20) -- that escaping is what keeps whitespace field-splitting correct, but passed through verbatim it leaked into user-visible output in two places:

  • The main scan's network/listeningPorts sections -- present since the JXA port, i.e. long before this session. This machine's real scan_result.json contained Codex\x20 and Manus\x20 as process names, which render as-is on the security page and feed rule matching against process names.
  • network_watch.sh's observation rows (Add on-demand CPU/network observation window (Phase 5-1) #72, this session).

Both now unescape \x20 and drop the trailing space left by lsof's 9-character COMMAND truncation (invisible in the UI while still splitting dedup keys). Confirmed against real output that lsof drops a partial escape rather than truncating mid-sequence, so the complete-\x20 replacement is sufficient.

2. An empty first sample silently suppressed all new-connection reports

network_watch.sh's awk used the FNR==NR idiom to separate the two samples. With an empty first sample -- a real production shape, since lsof exits 1 with no output both on failure and on zero matches, and the sampler's || true swallows it -- FNR==NR misreads the second file as the first: every connection made during the window got registered as "already seen" and reporting went silent. Exactly the silent-failure class this whole effort has been closing. Replaced with POSIX inter-file variable assignment (building=1 file1 building=0 file2), which holds even for empty files. With no baseline a row can't be distinguished from a genuinely new one, and over-reporting is the safe direction.

Test plan

  • pytest tests/ -- 361 passed (3 new tests: escaped-name unescaping in network_watch.sh and in the JXA scan parsers, plus a dedicated empty-first-sample regression test)
  • swift test -- 157 passed; release_smoke.py --check-only -- ok
  • All three new tests proven against the pre-fix code via stash/restore: each fails with the exact expected wrong output (escaped names passed through; new connection suppressed)
  • Ran a real full scan after the fix: this machine's raw_facts.json network section now reads Codex/Manus where it previously read Codex\x20/Manus\x20, with no escaped or trailing-space names remaining

🤖 Generated with Claude Code

Two bugs in lsof output handling, both confirmed against this
machine's real output rather than hypothesized:

1. lsof escapes a space inside a COMMAND name as literal "\x20"
   ("Codex " -> "Codex\x20") -- that escaping is what keeps whitespace
   field-splitting correct, but passed through verbatim it leaked into
   the UI: the main scan's network/listeningPorts sections (present
   since the JXA port, "Codex\x20"/"Manus\x20" in this machine's real
   scan_result.json) and network_watch.sh's rows (#72). Both now
   unescape it, and drop the trailing space left by lsof's 9-character
   truncation -- invisible in the UI while still splitting dedup keys.

2. network_watch.sh's awk used the FNR==NR idiom to separate the two
   samples. With an empty first sample -- a real shape, since lsof
   exits 1 with no output both on failure and on zero matches, and
   `|| true` swallows it -- FNR==NR misreads the second file as the
   first, so every connection made during the window was registered as
   "already seen" and reporting went silent. Found because the new
   escaping test's fixture happened to use an empty first listen
   sample. Replaced with POSIX inter-file variable assignment
   (building=1 file1 building=0 file2), which holds even for empty
   files; with no baseline, over-reporting is the safe direction.

All three new tests proven against the pre-fix code via stash/restore.
@heznpc
heznpc merged commit 2399e16 into main Aug 13, 2026
8 checks passed
@heznpc
heznpc deleted the fix/lsof-escaped-command-names branch August 13, 2026 10:50
heznpc added a commit that referenced this pull request Aug 13, 2026
…74)

Adversarial review of #60-#73 (six parallel reviewers, findings then
reproduced by hand before fixing). Four defects that made the app
either crash or state something untrue:

login_items.sh reported removal SUCCESS when the check itself failed.
login_item_exists returned the same status for "not a login item" and
"the System Events query failed", so a failed read at execute time
emitted already_gone/ok with exit 0. Reproduced end to end: preview
issues a token normally, System Events then fails, execute reports
"ok" -- while the login item is still installed. The app maps that to
success and announces removal, so a security tool affirmatively told
the owner a persistence mechanism was gone while it was still there.
Now tri-state: only a successful read may assert absence; an
unreadable list is blocked, and a failed post-delete recheck is
failed, never ok.

SpaceGoalView hard-crashed the 목표 tab. Slider(in: 1...max(x, 1))
collapses to 1...1 whenever the cleanable total is above zero but at
or below 1GB -- one small npm cache -- and SwiftUI fatals with
"max stride must be positive". Reproduced as a real crash in a hosted
view, not inferred. The range is now provably wider than its lower
bound, and below 1GB the slider is replaced by the achievable total.

The storage watch could never be turned on, from two independent
causes, both predating this range but only diagnosable together:
PCH_STORAGE_WATCH_APP_BUNDLE was never in allowedEnvironmentOverrides,
so sanitizedEnvironment threw and the spawn was refused; and once that
was fixed, StorageWatchService's expectedArguments still omitted the
entry schedule.sh actually writes, so the installed plist failed the
exact-array comparison and every install was judged stale. The Swift
unit test could not catch that because its fixture was built from the
same wrong list the implementation used. Verified on a live machine:
the toggle now turns on and the app's reported state matches launchd
for the first time. A cross-language guard now compares schedule.sh's
and Swift's argument lists so they cannot drift apart again.

network_watch.sh reported a false all-clear when the closing lsof
sample failed: `|| true` cannot distinguish failure from zero matches,
so an unread sample looked like a quiet window. The closing LISTEN set
is the liveness probe (every real Mac has listening sockets); an empty
one now reports an error instead of "0 new connections".

Every new test was proven against the pre-fix code first.

Co-authored-by: Heznpc <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant