Skip to content

Fix false-success, a crash, and a dead storage watch found in review - #74

Merged
heznpc merged 1 commit into
mainfrom
fix/review-false-success-and-crash
Aug 13, 2026
Merged

Fix false-success, a crash, and a dead storage watch found in review#74
heznpc merged 1 commit into
mainfrom
fix/review-false-success-and-crash

Conversation

@heznpc

@heznpc heznpc commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Adversarial review of this session's work (#60#73) using six parallel reviewers with different scopes. Every finding below was reproduced by hand before being fixed — none is taken on the reviewers' word.

1. login_items.sh reported removal SUCCESS when the verification itself failed

login_item_exists returned the same exit status for "this is not a login item" and "the System Events query failed", so a failed read was silently read as absence. Two consumers turned that into a success report:

  • pre-delete check fails → already_gone, exit 0
  • post-delete recheck fails → ok, exit 0 (the delete's own exit code is discarded)

Reproduced end to end with a stateful osascript stand-in: preview issues a token normally, System Events then fails, execute reports status ok / exit 0 — while the login item is still installed. LoginItemService maps both to success and the app announces removal. A security tool affirmatively telling the owner a persistence mechanism is gone while it is still there is the worst version of this bug class.

login_item_exists is now tri-state (present / confirmed absent / could not determine). Only a successful read may assert absence; an unreadable list is blocked, and a failed post-delete recheck is failed, never ok.

2. SpaceGoalView hard-crashed the 목표 tab

Slider(value:in: 1...max(achievableGB, 1), step: 1) collapses to 1...1 whenever the cleanable total is above zero but at or below 1GB — a single small npm cache is enough. SwiftUI divides the range by step and calls fatalError:

SwiftUI/Slider.swift:635: Fatal error: max stride must be positive

Reproduced as a genuine crash (signal 5) in a hosted view, not inferred from reading. The eligibleCandidates.isEmpty guard doesn't cover it, because candidates do exist. The bound is now provably wider than its lower bound, and below 1GB the slider is replaced by the achievable total (a whole-GB goal picker is meaningless there anyway). Five rendering regression tests cover 0 / sub-1GB / exactly-1GB / multi-GB / no-candidates; they crash against the old view.

3. The storage watch could never be turned on — two independent causes

Both predate this review range (introduced with the watch-notification work), but neither was diagnosable without the other:

  1. PCH_STORAGE_WATCH_APP_BUNDLE was passed by setStorageWatchEnabled but never added to allowedEnvironmentOverrides. sanitizedEnvironment throws on any non-allowlisted key, so the spawn was refused before schedule.sh ever ran.
  2. With that fixed, schedule.sh runs and installs a real plist — but StorageWatchService.runtimeState's expectedArguments still omitted the PCH_STORAGE_WATCH_APP_BUNDLE= entry that schedule.sh writes unconditionally. The exact-array comparison therefore always failed, every fresh install was judged .stale, and the UI reported failure and showed the watch as off while launchd had actually loaded the job.

The Swift unit test could not catch (2) because its fixture built the plist from the same wrong list the implementation used — the test encoded the bug.

Verified on a live machine: the toggle now turns on, and the app's reported state matches launchctl for the first time. (The LaunchAgent installed during verification was removed afterward.) A new cross-language contract test compares schedule.sh's and Swift's argument lists so they cannot drift apart again — it fails against the pre-fix Swift.

4. network_watch.sh reported a false all-clear on an unread closing sample

lsof exits non-zero with no output both on failure and on zero matches, and || true erases the difference — so a closing sample that was never read emitted newEstablished 0 / newListen 0, indistinguishable from a genuinely quiet window. The closing LISTEN set is the usable liveness probe (every real Mac has listening sockets); an empty one now reports an error instead of a count. Existing tests were passing unrealistically empty listen samples and are corrected to a real baseline.

Test plan

  • pytest tests/ — 366 passed
  • swift test — 162 passed
  • release_smoke.py --check-only — ok
  • Every new test proven against the pre-fix code (4 pytest failures, the SwiftUI crash, and the drift guard all confirmed first)
  • Live verification in the real signed build for the storage-watch fix

Not included

Lower-severity findings from the same review (pinned Homebrew packages dropped by the brew outdated regex; comma-in-name login items; non-\x20 lsof escapes; goal-selection tie-break determinism and float-epsilon; missing isBusy guards) are real but none produces a crash or a false success. They're deliberately left for a follow-up rather than mixed into this one.

🤖 Generated with Claude Code

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.
@heznpc
heznpc merged commit 2ee2f3a into main Aug 13, 2026
8 checks passed
@heznpc
heznpc deleted the fix/review-false-success-and-crash branch August 13, 2026 11:57
heznpc added a commit that referenced this pull request Aug 13, 2026
* Fix accuracy defects from the review follow-up batch

The lower-severity half of the same adversarial review as #74. None of
these crashes or claims a false success, but each reports something
that is not quite true.

Login item names are now read one per line instead of relying on
osascript's ", " list serialization. That default is ambiguous: an item
named "Backup, Inc." serialized exactly like two items "Backup" and
"Inc.", so it could never be matched and was permanently unremovable
through this tool; and a surviving "Foo, Bar" kept producing a "Foo"
fragment, so removing a genuinely separate "Foo" re-matched on the
post-delete recheck and reported failed after a successful removal.

lsof escapes every byte it deems unprintable, not just the space that
motivated the earlier fix -- a tab arrives as \x09 and a non-ASCII name
as a run of \xNN. Both the awk and the JXA decoders now handle whole
runs, so a multi-byte UTF-8 name reassembles instead of leaking
literal escapes into the UI and into anything matching on the name;
control bytes become a space because they would break the TSV.

Pinned Homebrew packages were dropped outright by the devtoolUpdates
regex, which anchored the latest version to end-of-line while brew
appends " [pinned at X]". The row vanished while the collector's line
count still included it, so the displayed total disagreed with the
list and a pinned package's update was invisible. Rows now survive and
say they are pinned.

SpaceGoalSelection's documented "same set regardless of scan order"
promise did not hold when size and label both tie -- label falls back
to kind, so two same-size rows of one kind tied completely and the
result depended on emit order. Path breaks the remaining tie. The
goal comparison also gained a tolerance: 2.4 + 0.3 + 0.3 is exactly
3.0 in decimal but accumulates to 2.9999999999999996, so the greedy
walked past the set that met the goal, took an extra item, and then
called the result short. A non-finite sizeGB is clamped at the parse
boundary, where it used to poison every sum and trip a range
precondition.

Login item removal and the observation window now respect isBusy in
both directions: removal could previously start a second concurrent
ScanPipeline over the same output files during a scan (untracked by
scanTask, so 검사 취소 could not stop it), and a scan started under an
observation landed in that observation's own results.

Mothball used scan() rather than scanReport(), discarding inspection
failures -- MothballCore's own API comment warns that this makes
"found but could not inspect" indistinguishable from "nothing found".
The page now says which it is.

* Skip the JXA-driven pinned-line test on non-macOS CI

Same class of miss as the network_watch skipif: the probe runs the
real regex through /usr/bin/osascript, which Linux runners lack.

---------

Co-authored-by: Heznpc <[email protected]>
heznpc added a commit that referenced this pull request Aug 13, 2026
Six items the #74/#75 reviewers classified low/informational --
nothing here is reachable in a shipped build today; each is a seam a
future change could silently reopen.

privacy.sh's PCH_TCC_DB_PATH test seam now opens only under
PCH_TEST_MODE=1, matching PCH_TEST_BREW_BIN and PCH_TEST_OSASCRIPT_BIN.
It was the one injection point in the new modules without the gate,
and ungated it doubles as an arbitrary-sqlite-read redirect for any
future caller that forwards the variable.

login_items.sh now refuses a manifest whose protocol version differs
from its own -- fields this version reads could mean something else
under another one. cleanup.sh cross-checks its whole manifest; this is
the minimum equivalent.

The bundled-runtime completeness guard now also covers non-.sh runtime
dependencies (rules/*.json, data/*.json, report_i18n, *.jxa.js, and
shipped .py), with an explicit build/reference-only exclusion list
mirroring the .sh guard's. Proven the same way: removing
rules/network.json from RUNTIME_FILES trips it.

RuntimeWorkspace's bundled branch now demands the development opt-in
whenever the injected resourceURL is not the running app's own signed
bundle. That combination skips the code-signature payload comparison
and is constructible only through parameter injection; without the
gate, a future refactor decoupling the parameters would run unsigned
code with no test noticing. The revalidation tests declare the opt-in
they were implicitly relying on.

Raw pinned-file values now refuse on key collision instead of
silently clobbering whichever module payload merged first -- the
exact asymmetry behind the approval_token/approval_token_module
near-miss fixed in #67.

Observation timeout slack grows from 20s to 60s past the window (the
snapshots run exactly when the machine is busiest), and a timeout now
says it timed out instead of "status 124".

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