larksuite/cli · error
file changed between validation and open
Error message
file changed between validation and open
What it means
The file the policy validated (pre-open Stat) and the file actually opened (post-open fd Stat) are different inodes (os.SameFile fails). The library detects TOCTOU races: an attacker or racing writer swapped the path (e.g. replaced a symlink or renamed a file) between validation and open. It fails closed rather than acting on the wrong file.
Source
Thrown at internal/vfs/localfileio/openvalidated_unix.go:64
if err := inspectOpenedFile(f, pre, true); err != nil {
f.Close()
// An unusable target is a bad argument, not an internal fault: callers
// map ErrPathValidation to a typed validation error, and the fd checks
// are the same verdict the path checks make, one layer later.
return nil, &fileio.PathValidationError{Err: err}
}
return f, nil
}
// inspectOpenedFile validates the opened fd and restores blocking mode. pre is
// nil when there is no prior Stat to compare against.
func inspectOpenedFile(f *os.File, pre os.FileInfo, rejectHardLinks bool) error {
post, err := f.Stat()
if err != nil {
return fmt.Errorf("cannot stat opened file: %w", err)
}
if pre != nil && !os.SameFile(pre, post) {
return fmt.Errorf("file changed between validation and open")
}
if !post.Mode().IsRegular() {
return fmt.Errorf("not a regular file (directories, devices, FIFOs, and sockets are refused)")
}
if rejectHardLinks {
if st, ok := post.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 {
return fmt.Errorf("file has multiple hard links, so the other names it can be reached by " +
"cannot be checked (hint: copy the file and use the copy instead)")
}
}
if err := syscall.SetNonblock(int(f.Fd()), false); err != nil {
return fmt.Errorf("cannot restore blocking mode: %w", err)
}
return nil
}
View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Re-run the command once the concurrent writer finishes; this is a race, not a policy denial
- Stop whatever process is mutating the target path during CLI execution
- Use a stable copy of the file in a directory no other process touches
- If recurring, target a file not managed by tools that rename-swap (atomic-replace editors/write temp+rename patterns)
Defensive patterns
Strategy: validation
Validate before calling
before, err := os.Stat(path)
if err != nil { return err }
// ... run the operation ...
after, err := os.Stat(path)
if err != nil || !os.SameFile(before, after) { return errors.New("path is being mutated concurrently") } Try / catch
var pve *fileio.PathValidationError
if errors.As(err, &pve) && strings.Contains(pve.Error(), "file changed between validation and open") { /* re-run after concurrent writer finishes */ } Prevention
- Freeze concurrent writers (build tools, editors) before running the CLI
- Avoid paths in shared tmp dirs rewritten by other processes
- Prefer atomic-replace-aware tooling awareness: editors and build tools frequently rename-swap files
- Re-run the command; the race window is microseconds so one retry usually succeeds
When it happens
Trigger: The path is renamed, replaced, or its final component swapped (e.g. symlink retargeted, file deleted and recreated) between vfs.Stat and vfs.OpenFile inside openValidated. O_NOFOLLOW already blocks symlink-swap of the last component, so this fires for rename/recreate races of non-symlink components.
Common situations: Build tools, test runners, or package managers (tmpdirs rewritten concurrently, pnpm/nix store churn) racing with the CLI; two processes operating on the same path; /tmp-style directories where another user replaces files.
Related errors
- file changed between validation and open
- %s: cannot stat resolved path %q: %w
- %w (lock: %s, syscall: %v)
- failed to read proxy plugin config %q: %w
- cannot stat opened file: %w
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/86e2d46f13784710.
Report an issue: GitHub.