Development record
The transcript store never narrows what an earlier binary laid out wider: a chain level that already exists keeps its mode (fsutil.EnsureRealDir creates at 0o700 and leaves an existing directory alone, internal/core/history/location.go, Resolve), and a record already on disk at 0o644 stays at 0o644 until Migrate happens to rewrite it. Capture now writes every new record 0o600 (iss-2609012029343438), which protects nothing already stored: on a machine where an earlier layout created ~/.abcd/transcripts or a level below it 0o755, every record captured before the fix stays readable by other local accounts. The remedy is a narrowing pass on the store's own levels only (the base, the root-sha level and records/, never ~/.abcd or a checkout's .abcd/.work.local), each opened O_NOFOLLOW|O_DIRECTORY, owner checked with fstat against this account, and narrowed with fchmod on the descriptor, the shape of fsutil's unexported tightenLock for lock files. No exported fsutil primitive does that for a directory, so it is a new primitive plus a stance change for EnsureRealDir's never-narrow contract, not a mode constant.
The transcript store never narrows what an earlier binary laid out wider: a chain level that already exists keeps its mode (fsutil.EnsureRealDir creates at 0o700 and leaves an existing directory alone, internal/core/history/location.go, Resolve), and a record already on disk at 0o644 stays at 0o644 until Migrate happens to rewrite it. Capture now writes every new record 0o600 (iss-2609012029343438), which protects nothing already stored: on a machine where an earlier layout created ~/.abcd/transcripts or a level below it 0o755, every record captured before the fix stays readable by other local accounts. The remedy is a narrowing pass on the store's own levels only (the base, the root-sha level and records/, never ~/.abcd or a checkout's .abcd/.work.local), each opened O_NOFOLLOW|O_DIRECTORY, owner checked with fstat against this account, and narrowed with fchmod on the descriptor, the shape of fsutil's unexported tightenLock for lock files. No exported fsutil primitive does that for a directory, so it is a new primitive plus a stance change for EnsureRealDir's never-narrow contract, not a mode constant.
Grounds
- pursued: a 0o755 records leaf is 0o700 after the next capture while every ancestor stays 0o755, a foreign-owned leaf is refused unchanged, a symlinked leaf is refused and its target unchanged, and a 0o644 legacy record lands 0o600; any of those modes differing in records_leaf_mode_test.go or legacy_move_mode_test.go would show it wrong
Frontmatter
| id | iss-2609291610432030 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609291610432030-the-transcript-store-never-narrows-what.md
open on GitHub ↗ · commit history ↗
mentions1
- iss-2609012029343438Transcript records are written world-readable: history.Capture writes each redacted record with mode 0644 into a transcripts chain ahoy creates 0755 (apply.go), while the unredacted staging tier is 0700/0600 by stated design. Noted by GHSA-gmp7-9rvm-qcr3 as the amplifier of the PEM leak, it is a separate question from the redaction defect: whether a redacted transcript store under the caller home should be private to the account (records 0600 and the chain 0700) or is meant to be readable by other local accounts. No ADR or decision records the choice, the tightening spans the ahoy-owned directory chain, and no consumer is known to rely on a world-readable store. Fork to decide: tighten both (one mode change in history plus one in ahoy, with a test on the resulting mode) or record the world-readable posture as deliberate.