Repository navigation
Description
Problem
pkgs/mcpx/internal/cache's Scan treats a file entry's lease file as a cache entry in its own right. A lease for a directory entry lives inside the entry (invisible to a scan of the parent), but a lease for a single-file entry lives beside it, named <entry>.lease-<random> (leaseLocation, cache.go:222-231). Scan (cache.go:265-310) only skips names starting with the bare lease prefix .lease- (cache.go:276) or .tmp- — a beside-file lease name starts with the entry's name, so it passes the skip, passes a nil match, and is returned as an Entry with Leased: false.
Consequences, all in the mechanism whose core promise is "eviction never removes a leased entry … the lease is what makes that safe" (cache.go:14-17):
List/mcpx cacheand every eviction accounting count phantom entries: a leased file entry reports as two entries, and its size/count budget is charged for the lease file too.- Worse,
Prune/Evictcan select the lease file itself as a victim (it is notkeep, andLeased()on the lease file's own path finds no<leasefile>.lease-*, so the mcpx cache: three Codex findings on merged PR #2555 — path traversal in cache remove, Put self-eviction, eviction/lease TOCTOU #2566-finding-3 recheck inremoveAll, cache.go:349-361, also passes) and delete it — silently stripping the pin from a live, in-use entry, which the next eviction pass can then delete mid-run.
Evidence (verified this run, main 69b23978)
Verbatim-copy probe on the laptop (cache.go sha256 2782f988… identical in both clones; probe dir ~/git/fleet/probe-2026-10-31-laneB-cachelease/ on the laptop):
Scan(dir, "kind", 0, nil)over a directory holding one leased file entry returned 2 entries:key="entry.bin" size=7 leased=trueandkey="entry.bin.lease-3358895815" size=0 leased=false.- With the lease file aged oldest and the entry newest,
Prune(dir, Limits{MaxEntries: 1}, "", nil)removed exactly one entry:entry.bin.lease-566081972— the live lease file itself — leaving the in-use entry unpinned.
The nil-match form is not hypothetical: Cache.List (cache.go:254) and the runner's pruneWorkDirs (internal/runner/runner.go:839-846) both scan with no pattern, and the package documents entries as "either a directory or a single file" (cache.go:8-9) with Acquire explicitly supporting file entries (cache.go:195-198). The runner's patterned pruners escape only because their suffix patterns (.ts, .mcpx-entry.ts) happen not to match a lease suffix.
Why it matters
The lease is the cache mechanism's only protection against concurrent eviction (#1908, #2566). A scan that cannot tell a lease from an entry both corrupts eviction accounting and can destroy the pin itself, re-opening the exact failure #2566 finding 3 closed (an in-use entry deleted from under a running exec) through the lease file rather than the entry.
Acceptance criteria
-
Scannever returns a lease file as an entry: beside-file leases (<name>.lease-*) are skipped (or otherwise recognised) regardless of thematchpattern, including the nil-pattern form. -
Prune/Evict/removeAllcan never delete a lease file; a victim list containing one is impossible, not just rechecked. - Regression tests in
internal/cache: a leased file entry scans as exactly one entry withLeased: true, and a prune sized to force a victim leaves the lease file and the entry in place. - Existing
internal/cacheand runner tests still pass (laptop or CI).
Dedup
Distinct from closed #2566 (path traversal in Remove, Put self-eviction, scan→remove lease TOCTOU recheck): this is lease files being enumerated as entries by Scan, which neither the traversal validation nor the removeAll recheck addresses. Searched open + closed issues for cache/Scan/lease/Prune terms; no existing issue covers it. Parent mechanism: #1908.
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
81 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · parked
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3728 from developing-today/queue/5637-3112-cache-…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done