{"record":{"id":"86e2d46f13784710","repo":"larksuite/cli","slug":"file-changed-between-validation-and-open","errorCode":null,"errorMessage":"file changed between validation and open","messagePattern":"file changed between validation and open","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/vfs/localfileio/openvalidated_unix.go","lineNumber":64,"sourceCode":"\tif err := inspectOpenedFile(f, pre, true); err != nil {\n\t\tf.Close()\n\t\t// An unusable target is a bad argument, not an internal fault: callers\n\t\t// map ErrPathValidation to a typed validation error, and the fd checks\n\t\t// are the same verdict the path checks make, one layer later.\n\t\treturn nil, &fileio.PathValidationError{Err: err}\n\t}\n\treturn f, nil\n}\n\n// inspectOpenedFile validates the opened fd and restores blocking mode. pre is\n// nil when there is no prior Stat to compare against.\nfunc inspectOpenedFile(f *os.File, pre os.FileInfo, rejectHardLinks bool) error {\n\tpost, err := f.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot stat opened file: %w\", err)\n\t}\n\tif pre != nil && !os.SameFile(pre, post) {\n\t\treturn fmt.Errorf(\"file changed between validation and open\")\n\t}\n\tif !post.Mode().IsRegular() {\n\t\treturn fmt.Errorf(\"not a regular file (directories, devices, FIFOs, and sockets are refused)\")\n\t}\n\tif rejectHardLinks {\n\t\tif st, ok := post.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 {\n\t\t\treturn fmt.Errorf(\"file has multiple hard links, so the other names it can be reached by \" +\n\t\t\t\t\"cannot be checked (hint: copy the file and use the copy instead)\")\n\t\t}\n\t}\n\tif err := syscall.SetNonblock(int(f.Fd()), false); err != nil {\n\t\treturn fmt.Errorf(\"cannot restore blocking mode: %w\", err)\n\t}\n\treturn nil\n}\n","sourceCodeStart":46,"sourceCodeEnd":80,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/vfs/localfileio/openvalidated_unix.go#L46-L80","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"before, err := os.Stat(path)\nif err != nil { return err }\n// ... run the operation ...\nafter, err := os.Stat(path)\nif err != nil || !os.SameFile(before, after) { return errors.New(\"path is being mutated concurrently\") }","typeGuard":null,"tryCatchPattern":"var pve *fileio.PathValidationError\nif errors.As(err, &pve) && strings.Contains(pve.Error(), \"file changed between validation and open\") { /* re-run after concurrent writer finishes */ }","preventionTips":["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"],"tags":["filesystem","race-condition","toctou","unix"],"backgroundTag":"file-changed-under-us","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}