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

  1. Re-run the command once the concurrent writer finishes; this is a race, not a policy denial
  2. Stop whatever process is mutating the target path during CLI execution
  3. Use a stable copy of the file in a directory no other process touches
  4. 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

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


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