larksuite/cli · error
file changed between validation and open
Error message
file changed between validation and open
What it means
The file's identity changed between the pre-open path validation and the opened handle: os.SameFile(pre, post) returned false. This library enforces a TOCTOU-safe open — the file you validated must be the file you got — so it refuses the handle instead of operating on a swapped file. Typically the path was replaced (delete+recreate, rename over) by another process in that window.
Source
Thrown at internal/vfs/localfileio/openvalidated_windows.go:47
if err := inspectOpenedFile(f, pre); 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 handle. pre is nil when there is no
// prior Stat to compare against.
func inspectOpenedFile(f *os.File, pre os.FileInfo) 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)")
}
var handleInfo syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(syscall.Handle(f.Fd()), &handleInfo); err != nil {
return fmt.Errorf("cannot inspect opened file links: %w", err)
}
if handleInfo.NumberOfLinks > 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)")
}
return nil
}
View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Re-run the whole validate+open cycle against the current file; if the swap was a one-off, the next attempt passes
- Open the file exclusively or coordinate with the writer so the path is not replaced during open
- Take your own fresh Stat immediately before open and pass it as pre, minimizing the race window
- If replacement is expected, validate the new file's content instead of insisting on the old identity
Example fix
// before: stale pre-stat from long ago
pre := statTakenMinutesAgo
f, err := openValidated(path, pre)
// after: re-stat right before opening
pre, err := os.Stat(path)
if err != nil { return err }
f, err := openValidated(path, pre) Defensive patterns
Strategy: validation
Validate before calling
pre, err := os.Stat(path)
if err != nil { return err }
// pass pre immediately to openValidated; do not hold it across long operations Try / catch
f, err := openValidated(path, pre)
if err != nil && strings.Contains(err.Error(), "file changed between validation and open") {
pre, serr := os.Stat(path)
if serr != nil { return serr }
f, err = openValidated(path, pre)
}
return err Prevention
- Re-stat immediately before opening; never cache os.FileInfo across long stretches
- Coordinate with writers (watchers, sync clients) so the path is not rename-replaced during open
- Prefer in-place writes by a single owner process over delete-and-recreate publishing
When it happens
Trigger: Calling openValidated with a non-nil pre os.FileInfo on Windows when the path is deleted and recreated, renamed over, or its volume/device identity changes between the initial Stat and the successful open.
Common situations: Another process (editor save, build tool, sync client like Dropbox/OneDrive, deploy script) rewrites the file atomically via rename while you open it; symlink retargeting; config files regenerated in place by watchers.
Related errors
- file changed between validation and open
- cannot stat opened file: %w
- %s: cannot stat resolved path %q: %w
- %s: cannot stat %q: %w
- failed to read proxy plugin config %q: %w
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/8a8155b4339cffe4.
Report an issue: GitHub.