Skip to content

Fix R01-R06: remaining findings from the 2026-09-30 re-audit - #12

Merged
MuffinUser merged 6 commits into
mainfrom
worktree-audit-fixes-r01-r06
Sep 30, 2026
Merged

MuffinUser merged 6 commits into
mainfrom
worktree-audit-fixes-r01-r06

Conversation

@MuffinUser

Copy link
Copy Markdown
Owner

Summary

Fixes all six remaining findings (R01-R06) from audits/2026-09-30-repository-reanalysis.md, verified independently before implementation:

  • R01 — escapeGitattributesPattern backslash-escaped spaces, which git's gitattributes parser doesn't honor as an escape; it now quotes the whole pattern instead (verified against real git check-attr).
  • R02 — DirtyFiles only diffed the worktree against HEAD, missing a change that exists only in the index; it now also diffs the index against HEAD.
  • R03 — rotate's auto-generated password was only shown at the very end; it's now shown immediately after generation, before any state that could still fail, and failure messages now point at git-based recovery instead of leaving the user guessing.
  • R04 — the local key file was written via a truncating os.WriteFile with no locking; it's now written atomically (temp file + rename) with an in-process mutex around Set's read-modify-write.
  • R05 — LooksEncrypted accepted any ENC[<known-algo>,...]-shaped string regardless of what followed; it now structurally validates every field, while still accepting the legacy keyless whole-file tag shape.
  • R06 — Clean could encrypt a value that Smudge could never render back (an unescapable control character, or a JSON integer outside int64's range); it now runs the same validation Smudge already runs, up front, before encrypting.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... -count=1 (all 9 packages pass)
  • Each fix has a new or updated regression test reproducing its specific failure mode

🤖 Generated with Claude Code

MuffinUser and others added 6 commits September 30, 2026 10:04
escapeGitattributesPattern replaced a space with backslash-space, but
git's own gitattributes line parser still splits on that whitespace —
verified directly against `git check-attr`. Quote the whole pattern
instead (the same C-quoted form git already honors), escaping only
what the quoting layer itself needs escaped. Also closes the test gap
the audit flagged: the new integration test writes the produced line
into a real .gitattributes and asks git whether it actually resolves
the filter, instead of only checking the helper's string output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DirtyFiles only compared the working tree against HEAD, so a file
whose worktree content was restored back to HEAD while the index still
held a different, staged version was reported as clean. CheckoutAll's
subsequent `git checkout HEAD -- <path>` then silently discarded that
staged change. DirtyFiles now also diffs the index against HEAD and
returns the union, so either kind of drift is caught before it's
overwritten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The auto-generated password was only ever printed at the very end of
rotate, after registering the new key, saving the config, and
re-encrypting everything under it. A failure at any of those steps
left the new key already active with its password never shown. Print
it immediately once it exists, before anything that could still fail.
Failures partway through now also point out that nothing has been
committed yet, so `git checkout -- .` (or `git reset --hard HEAD`)
fully undoes the attempt, and re-running `nebel rotate` converges the
rest instead of leaving anything half-migrated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
writeKeyFile truncated the key file in place, so an interruption
mid-write could leave it empty or partial, and Set's read-modify-write
had no synchronization, so two concurrent calls could silently lose an
update. writeKeyFile now writes to a temp file in the same directory
and renames it into place, an atomic replace a reader never observes
partially. Set is now also guarded by an in-process mutex. Cross-process
locking remains out of scope, documented as a deliberate trade-off
rather than a silent gap, to avoid a unix-only flock on a project that
also supports Windows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LooksEncrypted only checked the ENC[...] delimiters and a recognized
algorithm name, so a plaintext value like
ENC[AES256_SIV,not-a-real-tag] was treated as already-encrypted and
left uncommitted-as-plaintext by Clean instead of being encrypted.
LooksEncrypted now structurally validates every field (key version,
base64 data, value type), rejecting anything malformed, while still
accepting a keyless legacy pre-rotation whole-file tag with no key
field at all, since tag.Parse itself requires one and would otherwise
have broken that still-supported legacy shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cleanValues encrypted a scalar's plaintext without checking whether it
could actually be restored later, so a YAML string containing an
unescapable control character, or a JSON integer outside int64's
range, was accepted and committed as ciphertext that permanently fails
on smudge. cleanValues now runs the same validTypeLiteral and
handler.Render checks smudgeValues already runs on the way out, up
front on the plaintext before it's ever sealed, so an unrestorable
value is a rejected clean instead of unrecoverable ciphertext.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MuffinUser
MuffinUser marked this pull request as ready for review September 30, 2026 08:23
@MuffinUser
MuffinUser merged commit b58fce9 into main Sep 30, 2026
6 checks passed
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