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

  1. Re-run the whole validate+open cycle against the current file; if the swap was a one-off, the next attempt passes
  2. Open the file exclusively or coordinate with the writer so the path is not replaced during open
  3. Take your own fresh Stat immediately before open and pass it as pre, minimizing the race window
  4. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/8a8155b4339cffe4. Report an issue: GitHub.