You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AI-discovered findings. These bugs were surfaced by an automated multi-agent code review run by Claude (Fable 5) while reviewing Windows-native build work, and have not been fully human-verified. Three of the four were reproduced empirically on Windows 11 with a locally built binary; the fourth is a source-level trace. Details (especially line numbers) may be imprecise — treat with appropriate skepticism.
All four appear to be pre-existing on main (confirmed via git diff that recent branch work did not introduce them). Line numbers below are from the windows-native-builds branch and may be slightly offset on main.
1. Cross-volume drift link silently stores an absolute path in drift.lock (Windows)
When the doc or target resolves to a different volume than the repo root, std.fs.path.relativeWindows returns the canonicalized absolute path of to rather than a relative path (its error set is Allocator.Error only — there is no error to catch). The normalizeDocPath/normalizeTargetPath helpers pass that result straight through, so the absolute path is written into drift.lock as if it were repo-relative.
Repro (reproduced with a built binary): repo on C:, run drift link docs/auth.md Z:\shared\util.ts → exit 0, lockfile records target = "Z:/shared/util.ts". The next drift lint joins root_path with the stored path, the open fails, and the anchor is permanently stale (file not readable) on every machine. A blanket relink (drift link docs/auth.md --doc-is-still-accurate) exits 0 but can never clear it, because link's resolveInputPath honors absolute stored paths while lint's join does not.
Affected sites: src/commands/link.zig (normalizeDocPath, normalizeTargetPath), src/commands/unlink.zig, src/commands/refs.zig, and lint.zig's normalizeChangedPrefix.
Suggested fix: after computing relative, reject a non-relative result (drive/UNC-prefixed, or optionally ..-prefixed on POSIX for the same in-repo invariant) with an explicit error: path is outside the repository instead of writing it to the lockfile.
2. Wrong-case input is recorded verbatim as a binding's canonical identity
resolveInputPath only does path.resolve + an existence check — no case canonicalization, no comparison against git ls-files. On a case-insensitive filesystem (default NTFS, default macOS), wrong-case input passes the existence check and its spelling becomes the binding's identity in the committed lockfile.
Repro (reproduced with a built binary): git tracks docs/auth.md; drift link Docs\auth.md src\Session.zig exits 0 and records doc = "Docs/auth.md". Consequences:
drift lint splits one physical doc into two doc groups (git ls-files output vs. binding doc path are merged case-sensitively) — the phantom spelling holds the anchor; corruption is silent on the machine that created it.
Correct-case drift unlink docs/auth.md src/session.zig is a silent no-op (exit 0, binding left behind), so the stuck identity is invisible.
Any case-sensitive checkout (Linux CI, WSL, NTFS case-sensitive dirs — verified with fsutil setCaseSensitiveInfo) reports the anchor stale file not found.
Suggested fix: canonicalize the on-disk case of the full path before computing the repo-relative identity (realpath-style resolution of the whole path, not just cwd), and/or adopt the git ls-files spelling at link time. Independently, unlink should report "no matching binding" with a nonzero exit instead of silently succeeding.
In src/commands/lint.zig (classifyLinkTask, ~line 641), wire.target = link.target stores a slice into the FileCache's child arena. checkOneDocInner's defer file_cache.deinit() frees those chunks back to the parent arena before the main thread renders JSON, so the emitted links[].target reads semantically freed memory.
It is byte-correct today by coincidence: the std ArenaAllocator's free only rolls back the tail chunk, and in this code path later allocations always follow the doc-content chunk, so the bytes survive rendering in every schedule. It is undetectable by DebugAllocator (the rawFree stops at the parent arena — no poisoning) and by the existing JSON integration test (which spawns the binary as a subprocess). Any change to allocation order or the std arena's free/reuse policy turns it into silent output corruption.
Suggested fix: one line — dupe link.target into run_arena when storing it in the row.
4. drift link fails from repo subdirectories where unlink/refs succeed
link's run() reads the doc with a cwd-relative readFileAlloc on the raw CLI argument before normalizeDocPath/resolveInputPath ever runs, so the root fallback (cwd first, then lockfile root) that every other command applies never gets a chance.
Repro (reproduced with a built binary): cd <repo>/src && drift link docs/auth.md src/auth.ts → exit 1, error: cannot read 'docs/auth.md': FileNotFound — while drift refs src/auth.ts and drift unlink docs/auth.md src/auth.ts from the same directory both succeed with the identical spelling (the target argument resolves fine; only the doc read fails).
Suggested fix: resolve the doc path once (via resolveInputPath) before reading, use the resolved absolute path for the read, and keep the raw spelling only for user-facing messages.
Warning
AI-discovered findings. These bugs were surfaced by an automated multi-agent code review run by Claude (Fable 5) while reviewing Windows-native build work, and have not been fully human-verified. Three of the four were reproduced empirically on Windows 11 with a locally built binary; the fourth is a source-level trace. Details (especially line numbers) may be imprecise — treat with appropriate skepticism.
All four appear to be pre-existing on
main(confirmed viagit diffthat recent branch work did not introduce them). Line numbers below are from thewindows-native-buildsbranch and may be slightly offset onmain.1. Cross-volume
drift linksilently stores an absolute path indrift.lock(Windows)When the doc or target resolves to a different volume than the repo root,
std.fs.path.relativeWindowsreturns the canonicalized absolute path oftorather than a relative path (its error set isAllocator.Erroronly — there is no error to catch). ThenormalizeDocPath/normalizeTargetPathhelpers pass that result straight through, so the absolute path is written intodrift.lockas if it were repo-relative.Repro (reproduced with a built binary): repo on
C:, rundrift link docs/auth.md Z:\shared\util.ts→ exit 0, lockfile recordstarget = "Z:/shared/util.ts". The nextdrift lintjoinsroot_pathwith the stored path, the open fails, and the anchor is permanently stale (file not readable) on every machine. A blanket relink (drift link docs/auth.md --doc-is-still-accurate) exits 0 but can never clear it, because link'sresolveInputPathhonors absolute stored paths while lint's join does not.Affected sites:
src/commands/link.zig(normalizeDocPath,normalizeTargetPath),src/commands/unlink.zig,src/commands/refs.zig, andlint.zig'snormalizeChangedPrefix.Suggested fix: after computing
relative, reject a non-relative result (drive/UNC-prefixed, or optionally..-prefixed on POSIX for the same in-repo invariant) with an expliciterror: path is outside the repositoryinstead of writing it to the lockfile.2. Wrong-case input is recorded verbatim as a binding's canonical identity
resolveInputPathonly doespath.resolve+ an existence check — no case canonicalization, no comparison againstgit ls-files. On a case-insensitive filesystem (default NTFS, default macOS), wrong-case input passes the existence check and its spelling becomes the binding's identity in the committed lockfile.Repro (reproduced with a built binary): git tracks
docs/auth.md;drift link Docs\auth.md src\Session.zigexits 0 and recordsdoc = "Docs/auth.md". Consequences:drift lintsplits one physical doc into two doc groups (git ls-filesoutput vs. binding doc path are merged case-sensitively) — the phantom spelling holds the anchor; corruption is silent on the machine that created it.drift unlink docs/auth.md src/session.zigis a silent no-op (exit 0, binding left behind), so the stuck identity is invisible.fsutil setCaseSensitiveInfo) reports the anchor stalefile not found.Suggested fix: canonicalize the on-disk case of the full path before computing the repo-relative identity (realpath-style resolution of the whole path, not just cwd), and/or adopt the
git ls-filesspelling at link time. Independently,unlinkshould report "no matching binding" with a nonzero exit instead of silently succeeding.3. Latent use-after-free:
drift check --format jsonlinks[].targetslices a freed arenaIn
src/commands/lint.zig(classifyLinkTask, ~line 641),wire.target = link.targetstores a slice into the FileCache's child arena.checkOneDocInner'sdefer file_cache.deinit()frees those chunks back to the parent arena before the main thread renders JSON, so the emittedlinks[].targetreads semantically freed memory.It is byte-correct today by coincidence: the std
ArenaAllocator's free only rolls back the tail chunk, and in this code path later allocations always follow the doc-content chunk, so the bytes survive rendering in every schedule. It is undetectable byDebugAllocator(therawFreestops at the parent arena — no poisoning) and by the existing JSON integration test (which spawns the binary as a subprocess). Any change to allocation order or the std arena's free/reuse policy turns it into silent output corruption.Suggested fix: one line — dupe
link.targetintorun_arenawhen storing it in the row.4.
drift linkfails from repo subdirectories whereunlink/refssucceedlink'srun()reads the doc with a cwd-relativereadFileAllocon the raw CLI argument beforenormalizeDocPath/resolveInputPathever runs, so the root fallback (cwd first, then lockfile root) that every other command applies never gets a chance.Repro (reproduced with a built binary):
cd <repo>/src && drift link docs/auth.md src/auth.ts→ exit 1,error: cannot read 'docs/auth.md': FileNotFound— whiledrift refs src/auth.tsanddrift unlink docs/auth.md src/auth.tsfrom the same directory both succeed with the identical spelling (the target argument resolves fine; only the doc read fails).Suggested fix: resolve the doc path once (via
resolveInputPath) before reading, use the resolved absolute path for the read, and keep the raw spelling only for user-facing messages.