{"record":{"id":"6efc51bef29af702","repo":"JuliusBrussee/caveman","slug":"file-changed-while-opening-sqlite-security-linux","errorCode":null,"errorMessage":"file changed while opening","messagePattern":"file changed while opening","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/sqlite_file_security_linux.go","lineNumber":38,"sourceCode":"\treturn unix.Fchmodat(fd, \"\", 0o600, unix.AT_EMPTY_PATH)\n}\n\nfunc chmodSQLiteFile(path string, info os.FileInfo) error {\n\t// Closing an ordinary descriptor for the database or -shm drops ALL POSIX\n\t// locks held by SQLite in this process. O_PATH pins the inode without opening\n\t// it for I/O; closing this metadata-only descriptor does not release locks.\n\tfd, err := unix.Open(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfile := os.NewFile(uintptr(fd), path)\n\tdefer file.Close()\n\topened, err := file.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !opened.Mode().IsRegular() || !os.SameFile(info, opened) {\n\t\treturn fmt.Errorf(\"file changed while opening\")\n\t}\n\tif err := fchmodatEmptyPath(fd); !errors.Is(err, unix.EOPNOTSUPP) && !errors.Is(err, unix.EINVAL) {\n\t\treturn err\n\t}\n\t// Kernels before fchmodat2/AT_EMPTY_PATH require procfs. This is the pinned\n\t// descriptor's kernel-controlled link, NOT the swappable database pathname.\n\t// chmod performs no open/close, so SQLite's locks remain intact. Fail closed\n\t// if procfs is unavailable; never fall back to opening the database for I/O.\n\treturn unix.Chmod(\"/proc/self/fd/\"+strconv.Itoa(fd), 0o600)\n}\n","sourceCodeStart":20,"sourceCodeEnd":49,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/sqlite_file_security_linux.go#L20-L49","documentation":"On Linux, engine/ccr secures the SQLite database file by chmod'ing it to 0600 through a pinned descriptor (fchmodat with AT_EMPTY_PATH, or procfs fallback) so a swapped pathname cannot be attacked. Before doing so it re-stats the open file and requires it to still be a regular file and the same inode as when first observed; otherwise it throws 'file changed while opening' and aborts the securing step.","triggerScenarios":"Between the initial Stat of path and the pinned open, the path is replaced (symlink swap, rename-over, delete-and-recreate) or becomes a non-regular file, so os.SameFile(info, opened) returns false inside chmodSQLiteFile on Linux.","commonSituations":"A concurrent attacker or racing process swapping the database path; two Store instances racing on the same path where one deletes/recreates the file; tmp-once semantics where a cleanup routine removes the file mid-open; tests simulating TOCTOU attacks on the db path.","solutions":["Treat this as a deliberate fail-closed security abort: check for concurrent writers/replacers of the database path and ensure only one process instance uses it","Retry the operation once the path is stable — the caller creates the file with exclusive semantics, so a clean re-open usually succeeds","If it reproduces without an attacker, audit for duplicate Store instances or cleanup jobs (tmp file sweepers) racing the open, and serialize them"],"exampleFix":"// before\n// two goroutines open the same ccr sqlite path concurrently\n// after\nvar openOnce sync.Once\nopenOnce.Do(func() { store, err = engine.OpenCCRStore(path) }) // serialize store creation per path","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"store, err := engine.OpenCCRStore(path)\nif err != nil && strings.Contains(err.Error(), \"file changed while opening\") {\n\t// something replaced the path mid-open: ensure a single owner, then retry once\n\tstore, err = engine.OpenCCRStore(path)\n}","preventionTips":["Create the store once per process/path (sync.Once or singleton)","Exclude the data directory from sync/backup tools that rename-over files","Never point the store at a path other processes may delete or recreate"],"tags":["go","linux","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"}