Files
David Gageot c4dc536ead fix(cache): address TOCTOU race in maybeReload and improve documentation
- Fix TOCTOU race condition in Cache.maybeReload by re-stating the file
  under the write lock. The previous implementation stat'd the file before
  acquiring the lock, then used the stale FileInfo after the lock, which
  could lead to storing an outdated mtime that doesn't match the loaded
  content. This could cause subsequent Lookups to miss updates in
  high-concurrency scenarios.

- Clarify Windows lock semantics: the mandatory LockFileEx lock is on the
  separate .lock file (not the data file), so both Unix and Windows achieve
  the same advisory-lock effect for the cache operations.

- Document mtimeOf zero-value semantics: the zero time.Time{} is a sentinel
  for 'file not found' and is safe to compare with time.Time.Equal.

- Add comment explaining Store's update-after-persist ordering: the in-memory
  map is updated after the disk write so that persistence failures still keep
  the entry in memory for the current process.

- Document resolveCachePath's lexical-only path traversal check: symlinks are
  not resolved, which is acceptable for the threat model (cache paths come
  from agent configs, not untrusted user input).

All tests pass with -race. No functional changes to the cache behavior.

Assisted-By: docker-agent
2026-04-28 09:23:27 +02:00

45 lines
1.1 KiB
Go

//go:build windows
package cache
import (
"os"
"golang.org/x/sys/windows"
)
// maxRange asks LockFileEx / UnlockFileEx to cover the whole file by
// passing 0xFFFFFFFF for both the low and high 32 bits of the range.
const maxRange = ^uint32(0)
// lockExclusive blocks until an exclusive lock is held on the file.
// Windows has no flock, so we use LockFileEx with LOCKFILE_EXCLUSIVE_LOCK.
//
// The lock is mandatory (kernel-enforced) on Windows, unlike the advisory
// flock used on Unix. However, since we lock a separate .lock file (not
// the data file itself), both platforms achieve the same effect: serializing
// the read-modify-write window without preventing direct reads of cache.json.
func lockExclusive(f *os.File) error {
var ol windows.Overlapped
return windows.LockFileEx(
windows.Handle(f.Fd()),
windows.LOCKFILE_EXCLUSIVE_LOCK,
0,
maxRange,
maxRange,
&ol,
)
}
// unlockFile releases the lock previously acquired with lockExclusive.
func unlockFile(f *os.File) error {
var ol windows.Overlapped
return windows.UnlockFileEx(
windows.Handle(f.Fd()),
0,
maxRange,
maxRange,
&ol,
)
}