Skip to content

Bug-hunt round 37: unsanitised device roster, scoped network claim, undocumented 16 MiB cap - #54

Merged
REPPL merged 10 commits into
mainfrom
bughunt-37
Aug 8, 2026
Merged

Bug-hunt round 37: unsanitised device roster, scoped network claim, undocumented 16 MiB cap#54
REPPL merged 10 commits into
mainfrom
bughunt-37

Conversation

@REPPL

@REPPL REPPL commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Round 37 of the autonomous bug-hunt loop (state tracked on #24).

Confirmed findings

Substantive (3)

record's detected audio-device roster reached the terminal unsanitisedinternal/record/record.go (startRecorders, previously inline; now formatAudioRoster in internal/record/recorders.go). The roster prints ffmpeg's raw avfoundation device-name strings so an operator can spot a virtual audio driver shadowing the real microphone. Every other consumer of the identical probeSink bytes — outputTail and lockedBuffer.tail — already passes through session.SafeTextLines; this print site did not. A crafted device name (a malicious USB or virtual-audio driver) carrying ESC/ANSI bytes or an invisible-Unicode bidi override could inject terminal escape sequences or, more pointedly, use bidi reordering to hide its own entry from the operator the roster exists to warn — defeating the feature's own purpose. Fixed by factoring the print into formatAudioRoster, matching this file's other pure, unit-tested builders, and routing it through session.SafeText (the single-line variant suffices; the literal newlines in the format string never pass through the sanitiser). New test TestFormatAudioRosterSanitisesDeviceNames, confirmed to fail before the fix and pass after.

spc-2-analysis-findings.md's absolute "no network anywhere in the CLI" claim (.abcd/development/specs/open/spc-2-analysis-findings.md:25 and its previously-unnoticed sibling in the Decisions section) — flagged as a known, deferred defect in round 36's own decision-log entry. The Summary section already states a correctly-scoped claim two sentences earlier ("the CLI never calls a model, holds no keys, and adds no network dependency" — about analyze adding nothing new), so its own absolute restatement is removed outright as redundant; the Decisions section's separate "the CLI never calls a model or the network" is reworded to the same scoped form used everywhere else in the repo. record's live capture, the demo page's CDN-loaded rrweb recorder (internal/demo/assets/index.html:112), and the ASR engine's model fetch (docs/explanation/privacy.md, docs/how-to/transcribe-a-recording.md) each falsify the absolute version.

The 16 MiB JSONL total-size limit was undocumentedsession.MaxJSONLBytes (internal/session/session.go:433) has bounded timeline.jsonl/transcript.jsonl reads and writes since round 33, with findings.jsonl (round 35's analyze -ingest, round 36's review) and interactions.jsonl (round 36's demo capture endpoint) closing the last write-side gaps. No user-facing doc named it: docs/reference/cli.md's and docs/how-to/instrument-your-own-app.md's 413/refusal enumerations omitted it, and docs/reference/session-directory.md never mentioned a size limit on any file. A user hitting the refusal (e.g. demo's "session's captured interactions are at the file size limit; start a new session") had no documented cause or remedy. All three pages now name it, describing the write/load-refusal outcome rather than claiming every internal reader enforces it — analyze's internal re-ingest probe (holdsVerdicts) deliberately does not itself bound the total (see Considered and rejected below).

Nitpick (1)

  • install.sh's version-stamp comment wrongly claimed v0.1.0 is refused there as an unstamped "testimony dev" build. It isn't: v0.1.0 was hand-built and hand-stamped before the release workflow existed, so it prints its own tag and passes that gate like every later release — it is refused earlier, correctly, at the preceding attestation check, for predating the workflow that creates attestations. Comment corrected; no behaviour change (the refusal itself is intentional, fail-closed, and correct — see Considered and rejected below).

Considered and rejected

  • transcript.jsonl/timeline.jsonl's id field marked "Required" in session-directory.md despite merge not enforcing its presence. Both adversarial reviews killed this: merge's empty-id skip (internal/timeline/timeline.go, landed round 10) is deliberate and documented in three places (timeline.go, analyze/ingest.go, analyze_test.go), and the table's "Required" column tracking shape rather than enforcement was already explicitly litigated in round 27 for a neighbouring field — several other "yes"-marked, unenforced fields (session, text) establish the same pattern.
  • A macOS-specific ELOOP-vs-EMLINK symlink-refusal gap in internal/session/session.go's O_NOFOLLOW check. Refuted by reading Go's own stdlib source: internal/syscall/unix/nofollow_posix.go explicitly cites Apple's open(2) and puts darwin in the ELOOP bucket; EMLINK is confined to FreeBSD/DragonFly, neither a platform testimony ships.
  • A release.yml private-repo attestation-fallback comment, re-raised as inaccurate. This is a precedent duplicate of a claim already refuted in round 20 for the identical reason: an unauthenticated tarball fetch against a private repo 404s before the attestation branch is ever reached, so the claimed failure mode cannot occur.
  • An intents-README "always they/them" persona rule read as contradicting 03-personas.md's gendered narrative pronouns. A precedent duplicate refuted in rounds 14, 24, and 28: different registers for the same names, co-authored in one commit, and the intents rule is honoured with zero exceptions across all nine intent drafts.
  • release.yml's installer-flag-handling comment's "four early-return flag paths" read against ci.yml's "six" invocations. A precedent duplicate of round 24's identical count read against install.sh itself, already settled there as both accurate at their own granularity (four code paths, six invocations exercising them) — reverted to the original comment rather than re-litigated.
  • install.sh --version claimed broken for pre-attestation releases under a modern, authenticated gh. Spun off from the private-repo investigation above. Both refuters agreed the refusal is correct, security-required, fail-closed behaviour against a release with no attestation to verify (a true negative, not a false positive) — the actual, narrower defect was the stale comment describing it, fixed above.
  • Two nitpicks refuted outright: internal/record/proc.go's dropped flag on an exact-boundary write (unobservable — every code path that could set it wrongly also guarantees the truncation branch that reads it correctly prefixes the ellipsis); demo not handling SIGHUP unlike record (no data-loss risk — demo's stream writes are already durable O_APPEND, unlike the ffmpeg-container-finalisation risk SIGHUP handling protects in record).
  • Two doc-drift candidates refuted: session-directory.md's illustrative manifest example differing from the bundled fixture (the page's examples are consistently abridged illustrations throughout, not fixture excerpts — the transcript and findings examples abridge the same way); AGENTS.md omitting release.yml's tag-time-only gates (the section is explicitly scoped to locally-runnable commands plus ci.yml's push/PR gates, never claiming to enumerate every workflow).

Merge-gate corrections

Two rounds of post-fix adversarial review ran against this PR's own diff before merge. The correctness reviewer caught an inverted claim (a pre-attestation release "passes the version-stamp gate, then is refused later by attestation" — install.sh actually runs attestation first) and an overclaimed enforcement date ("every reader/writer ... since round 33", when the last two writers landed in round 36); both corrected. The docs-accuracy reviewer caught that the "four vs six" installer-comment change re-litigated a precedent round 24 had already settled — reverted — plus two round-citation errors (a nonexistent "round 29", and "round 27" misattributed for a fix that landed in round 10) and two overstated claims in the spc-2 entry (a deleted sentence described as "reworded"; the correct phrasing misattributed to a "Design section" it doesn't appear in) — all corrected.

Verification

go build, gofmt -l ., go vet ./..., go test ./..., go test -race ./... all clean; pipeline smoke (merge + report against a scratch copy of examples/sample-session) succeeded; sh -n install.sh && bash -n install.sh clean; release.yml re-parsed as YAML after the revert. Working tree otherwise untouched.


Assisted-by: Claude:claude-sonnet-5

REPPL added 10 commits August 8, 2026 12:23
startRecorders logs the detected avfoundation microphone roster so an
operator can spot a virtual audio driver shadowing the real mic.
Device names come from the same unsanitised ffmpeg listing as
outputTail's input, but unlike outputTail and lockedBuffer.tail (both
wrapped in SafeText/SafeTextLines), this print reached the terminal
raw — a crafted device name (a malicious USB/virtual-audio driver)
carrying ESC/ANSI bytes or invisible-Unicode bidi overrides could
inject terminal escapes or, more pointedly, use bidi reordering to
disguise its own roster entry, defeating the very feature meant to
reveal it.

Factor the roster line into formatAudioRoster (mirroring this file's
other pure, unit-testable builders) and route it through
session.SafeText, matching every other ffmpeg-derived string this
package prints.

Assisted-by: Claude:claude-sonnet-5
The Summary and Decisions sections each stated "No LLM and no network
anywhere in the CLI", contradicting the spec's own Design section
("the CLI never calls a model, holds no keys, and adds no network
dependency" — a claim about analyze adding nothing new) and the
network use record's live capture, the demo page's CDN-loaded rrweb
recorder, and the ASR engine's model fetch each independently carry.
Reword both to the scoped form already used everywhere else in the
repo, without touching the correctly-scoped carriers.

Assisted-by: Claude:claude-sonnet-5
timeline.jsonl, transcript.jsonl, interactions.jsonl, and
findings.jsonl have each carried a 16 MiB total-size limit since
round 33 (session.MaxJSONLBytes), enforced by every reader and writer
for those four files. No user-facing doc named it: cli.md's and the
instrument-your-own-app how-to's 413 lists for the demo capture
endpoints omitted the total-size refusal, the ingest and review
sections omitted their own, and session-directory.md never mentioned
a size limit on any file. A user hitting the refusal had no
documented cause or remedy.

Assisted-by: Claude:claude-sonnet-5
release.yml's installer-flag-handling comment said "four" early-return
paths; both it and ci.yml actually run six invocations (four flags,
two of them also tested with an empty value) — ci.yml's own comment
already said six.

install.sh's version-stamp comment claimed v0.1.0 predates the version
stamp and is refused there, reporting "testimony dev". It doesn't:
v0.1.0 was hand-built and hand-stamped before the release workflow
existed, so it prints its own tag and passes that gate like every
later release. It is refused later, correctly, for lacking a build
attestation the workflow did not yet exist to create — a comment
correction, not a behaviour change.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
Correctness review of this round's own PR caught two inaccuracies
introduced by its own commits: CHANGELOG.md claimed every reader and
writer for the four capped JSONL files "has enforced it since round
33", contradicting the same section's own round-35/36 entries — the
last writers (review's verdict append, demo's interactions endpoint)
landed in round 36. And CHANGELOG.md/DECISIONS.md both said a
pre-attestation release like v0.1.0 "passes [the version-stamp] gate,
and is refused later ... by the attestation check" — install.sh runs
attestation verification before the version-stamp gate, not after.

Also tightened, on the same review's non-blocking notes:
session-directory.md's "every reader and writer ... refuses" absolute
(analyze's internal re-ingest probe deliberately does not bound the
total, though the overall ingest path still refuses); the roster fix's
CHANGELOG entry no longer echoes round 27's own "the one class"
framing, which it now contradicts; and formatAudioRoster's comment
names SafeTextLines as what its siblings actually call, rather than
implying they share the exact same call.

Assisted-by: Claude:claude-sonnet-5
Docs-accuracy review of this round's own PR caught three defects:

- The "four" vs "six" installer-flag-path wording in release.yml was
  already litigated in round 24 (.abcd/work/DECISIONS.md, "both
  accurate at their own granularity") and explicitly not changed.
  This round re-raised and "fixed" the same wording without checking
  that precedent — reverted release.yml to its original comment and
  moved the item from this round's nitpicks to its refuted list, as a
  precedent duplicate of round 24.
- The persona-pronoun refutation cited "rounds 14, 24, and 29" as
  prior occurrences; round 29 does not exist. The third occurrence is
  round 28, which excluded the identical claim as a precedent
  duplicate of round 24.
- The id-required refutation attributed the timeline package's
  empty-id skip to "round 27" — round 27 only litigates the
  Required-column semantics the skip relies on; the skip itself landed
  in round 10 (merged as PR #27, not round 27).

Also tightened two overstated claims in the spc-2 network-claim entry:
the Summary's absolute sentence was removed outright (a scoped form
already sits two sentences earlier in the same paragraph), not
"reworded", and the corrected phrasing is not attributed to a "Design
section" it does not appear in.

Assisted-by: Claude:claude-sonnet-5
Round-2 docs-accuracy review caught that "two sentences earlier"
undercounted: the correctly-scoped sentence is the paragraph's second
sentence, the removed one its seventh — five apart, not two. Drop the
specific count rather than restate it wrong a second time, matching
this file's own round-36 precedent for an unreliable count.

Assisted-by: Claude:claude-sonnet-5
Round-2 correctness review caught "see Refuted below" pointing at
this entry's Refuted list, which contains no item about analyze's
internal re-ingest probe not bounding the total — the reasoning
actually lives in rounds 35 and 36's own entries above. Repoint it
there.

Assisted-by: Claude:claude-sonnet-5
@REPPL
REPPL merged commit c066bf0 into main Aug 8, 2026
6 checks passed
@REPPL
REPPL deleted the bughunt-37 branch August 8, 2026 12:56
@REPPL REPPL mentioned this pull request Aug 8, 2026
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