Document and gate worktree seed provenance, 0700 default path - #1037
DROOdotFOO wants to merge 11 commits into
Conversation
Unified Regression Test ResultsWorkflow: Unified Regression Testing Performance ResultsFound 1 performance result(s) Memory ResultsFound 3 memory result(s) Targets
|
Adversarial Review: PR #1037 (worktree-provenance)Three personas ran independently (Saboteur, New Hire, Security Auditor), all three driving the branch's script against scratch repos in /tmp. CRITICAL
HIGH
MEDIUM
LOW
Verified negatives
Cross-persona overlaps
Verdict: BLOCKBefore merge: refuse symlinked |
`add` takes the branch from $1 before the option loop runs, so `add --fresh feature/x` binds branch=--fresh and path=feature/x. A boolean flag is naturally written first, so `--fresh` widens a footgun that git then reports as `git branch` usage text naming neither argument. Reject a leading `-` in the branch position with a message that shows where options go, and route -h/--help to usage instead, on `add`, `sync` and the top-level dispatch.
`rmdir "$path" 2>/dev/null || true` discarded its own failure, so a `git worktree add` that failed after materialising or registering anything exited with no `worktree:` line and left a 0700 temp directory and a stale registration behind, one set per failure. The repo rule is to log or rethrow, so collect what could not be undone and say so. `created_branch` was also only set once git returned success, but `git worktree add -b` creates refs/heads/<branch> BEFORE the checkout: a later failure leaked the ref, and the next run then took the "already exists" path and died about --from being ignored, describing the previous failure rather than the current command. Decide the ref by show-ref during the unwind instead. Asking show-ref for the branch before the path is created also keeps the "--from would be ignored" refusal from leaving a fresh mktemp directory behind.
The gate lived inside seed_caches, which `sync` also calls, so `sync` always exited 3 -- and its documented job is to re-seed after a dependency bump in this checkout, which IS a lock difference. The die message told the caller to re-run with `add --fresh`, a flag `sync` does not have (cmd_sync reads only $1). Split the check out and give it a policy: `add` refuses, since no worktree exists yet and --fresh is a correct answer, while `sync` warns and proceeds, which is what it exists to do. Refuse symlinks rather than comparing through them. `-f`, `cmp` and the packages glob all follow links, so a target mix.lock symlinked at this checkout's own lock compares byte-identical while Mix reads something else, and a symlinked packages/<pkg> sends replace_dir outside the worktree. The branch under review controls both, which is exactly the threat the header names. Say which side moved. The comparison reads working-tree files, so an uncommitted mix.lock here -- mid `mix deps.update`, or a bump not yet committed -- differs from every worktree at once, and "bring this checkout to that lock first" is then advice to discard the newer lock. Ask git which copy is the edited one and advise per direction. Check the warm cache before the locks. A source with no _build/deps cannot seed whatever the locks say, and reporting exit 3 there sent the caller after the second blocker while the first still stood.
usage() extracts help with sed between `# Usage:` and `# ---8<---`, and the TRUST BOUNDARY block sat above `# Usage:`, so the one contract the printed text tells the caller to go read -- "the trust boundary in this file's header" -- was the one thing it never printed. Move the block inside the extracted range and point at it there. While the header is open, state the default path's actual guarantee. "Unpredictable" and "derivable" are not properties this script can promise: how much entropy mktemp spends on eight characters is libc's business. What it does promise is an atomic exclusive create at 0700, so the path is never known-and-unowned, which is the part that answers CWE-377. Lead with that, and document the per-subcommand gate, the symlink refusal and the check ordering.
CLAUDE.md attributed the gate to `add` alone while it also fired for `sync`, and omitted the header's own caveat that the lock comparison is a staleness check and not an integrity one, so a reader who never opens the script concluded the seed was verified. State both, plus the symlink refusal, and that only --fresh buys checksum verification. The default-path paragraph sold predictability as the security property; the guarantee is mktemp's atomic 0700 create. Reword. scripts/README.md listed only the speed claim, with none of the caveats: add a pointer to `add --help`.
Summary
Hardens
scripts/worktree.shso cache seeding cannot silently cross trust or filesystem boundaries.--fromvalues and resolve revisions to commit OIDs before directory creation.mktempdirectories./tmpwhenTMPDIRis empty, invalid, or/.mix.exsandmix.lockprovenance.Manual testing
bash -n scripts/worktree.shadd --fresh/rmsmoke completed and cleaned its branch/path--fromwas rejected before path creationTMPDIR=/smokes were exercised during implementation