{"record":{"id":"d5f46c34a3a32202","repo":"JuliusBrussee/caveman","slug":"file-changed-while-securing","errorCode":null,"errorMessage":"file changed while securing","messagePattern":"file changed while securing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/sqlite_file_security_unix.go","lineNumber":18,"sourceCode":"//go:build !linux && !windows && !js\n\npackage ccr\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc chmodSQLiteFile(path string, info os.FileInfo) error {\n\tcurrent, err := os.Lstat(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !current.Mode().IsRegular() || !os.SameFile(info, current) {\n\t\treturn fmt.Errorf(\"file changed while securing\")\n\t}\n\t// Do not open/close the inode: close would discard SQLite's process-wide\n\t// POSIX locks. Native nofollow chmod cannot follow a swapped symlink; the\n\t// caller also checks inode identity afterwards. Unsupported systems fail\n\t// closed rather than using a path-following chmod or an ordinary descriptor.\n\treturn unix.Fchmodat(unix.AT_FDCWD, path, 0o600, unix.AT_SYMLINK_NOFOLLOW)\n}\n","sourceCodeStart":1,"sourceCodeEnd":26,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/sqlite_file_security_unix.go#L1-L26","documentation":"On non-Linux Unix systems, engine/ccr secures the SQLite file by re-Lstat'ing the path and verifying it is still a regular file with the same inode as originally observed, then chmod'ing 0600 with AT_SYMLINK_NOFOLLOW (never opening the inode, to preserve SQLite's process-wide POSIX locks). If the path changed identity in between, it throws 'file changed while securing' instead of chmod'ing the wrong file.","triggerScenarios":"Between the original file creation/Stat and the chmodSQLiteFile call on the unix path, the database path is renamed-over, symlink-swapped, deleted and recreated, or replaced by a non-regular file, so os.SameFile(info, current) fails.","commonSituations":"Symlink attacks against the db path; concurrent Store instances where one replaces the file; backup/sync tools (rsync, Dropbox) rewriting the file in place via rename; tests exercising TOCTOU hardening.","solutions":["Ensure nothing else writes to or replaces the database path while the store is open — one owner process per path, no in-place rewriting by sync tools","Retry store creation after the path is stable; the check is intentionally fail-closed","If it reproduces spuriously, look for a second process (stale daemon, test parallelism) operating on the same ccr sqlite path and eliminate it"],"exampleFix":"// before\n// rsync/dropbox syncing the directory replaces db.sqlite mid-open\n// after\n// exclude the ccr data dir from sync tools, or relocate the store:\nstore, err := engine.OpenCCRStore(filepath.Join(privateDataDir, \"ccr.sqlite\"))","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"store, err := engine.OpenCCRStore(path)\nif err != nil && strings.Contains(err.Error(), \"file changed while securing\") {\n\t// path was swapped: ensure single owner/no sync tools, then retry once\n\tstore, err = engine.OpenCCRStore(path)\n}","preventionTips":["One process owns the database path; serialize store creation","Exclude the data dir from rsync/Dropbox-style tools that replace files via rename","Keep the store on a stable local filesystem"],"tags":["go","unix","sqlite","security","toctou","race"],"backgroundTag":"file-changed-during-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}