{"record":{"id":"42cd892db3e42e2f","repo":"JuliusBrussee/caveman","slug":"w-database-parent-changed","errorCode":null,"errorMessage":"%w: database parent changed","messagePattern":"%w: database parent changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/store_generation.go","lineNumber":46,"sourceCode":"var errStorageUnverifiable = errors.New(\"storage identity could not be verified\")\n\n// inspectSQLiteGeneration requires the canonical path PrepareSQLitePathCanonical\n// returned. The parent check below is a re-verification that no component became\n// a symlink since; it compares spellings on purpose, because following a swapped\n// intermediate symlink yields the same directory identity and so cannot be\n// detected by os.SameFile. A non-canonical spelling is reported as a change.\nfunc inspectSQLiteGeneration(path string) (sqliteGeneration, error) {\n\tvar files sqliteGeneration\n\tif path == \":memory:\" {\n\t\treturn files, nil\n\t}\n\tparent := filepath.Dir(path)\n\tresolved, err := filepath.EvalSymlinks(parent)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\treturn files, fmt.Errorf(\"%w: %w: inspect database parent: %v\", ErrStorageChanged, errStorageUnverifiable, err)\n\t}\n\tif err != nil || resolved != parent {\n\t\treturn files, fmt.Errorf(\"%w: database parent changed\", ErrStorageChanged)\n\t}\n\tinfo, err := os.Stat(parent)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn files, fmt.Errorf(\"%w: database parent changed\", ErrStorageChanged)\n\t}\n\tif err != nil {\n\t\treturn files, fmt.Errorf(\"%w: %w: inspect database parent: %v\", ErrStorageChanged, errStorageUnverifiable, err)\n\t}\n\tif err := validateSQLiteParentSecurity(parent, info); err != nil {\n\t\treturn files, fmt.Errorf(\"%w: %w: %v\", ErrStorageChanged, errStorageUnverifiable, err)\n\t}\n\tfor i, suffix := range sqliteSuffixes {\n\t\tinfo, err := inspectSQLiteFile(path + suffix)\n\t\tif errors.Is(err, os.ErrNotExist) && i != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn files, fmt.Errorf(\"%w: inspect database%s: %v\", ErrStorageChanged, suffix, err)","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/store_generation.go#L28-L64","documentation":"inspectSQLiteGeneration (engine/ccr/store_generation.go:46) rejects the database when its parent directory's symlink-resolved path differs from the expected spelling, or when EvalSymlinks reports the parent missing. This is a deliberate security check: it catches the parent directory (or an intermediate component) being swapped for a symlink since the path was canonicalized, which os.SameFile alone cannot detect. Unlike error 1038, this means the environment HAS changed (or the parent vanished) — the store is treated as ErrStorageChanged without the errStorageUnverifiable qualifier.","triggerScenarios":"Calling checkGeneration (via openWithBudgetHooks, secureSQLiteGeneration flows, or the inspection test) when: (a) filepath.EvalSymlinks returns os.ErrNotExist because the parent directory was deleted/moved after the store was opened, or (b) the resolved path string differs from the canonical parent — i.e. some component became a symlink, or a non-canonical path spelling was passed in.","commonSituations":"A dotfile manager or backup tool replaced ~/.caveman with a symlink; the data directory was moved between inspection passes; /tmp or home redirection changed (TMPDIR, container restarts with different mounts); the store path was constructed by hand with ../ or double slashes instead of via PrepareSQLitePathCanonical.","solutions":["Check whether ~/.caveman (or the configured CCR parent) is now a symlink — restore it to a real directory or point the store at the canonical target.","If the parent directory was deleted or moved, recreate it at the original canonical path or reinitialize the store at the new location.","Always construct the database path via PrepareSQLitePathCanonical; the check compares spellings on purpose and rejects non-canonical paths.","Treat this as ErrStorageChanged (not unverifiable): if a terminal quarantine decision triggered, follow the store's quarantine/recovery procedure rather than retrying blindly.","Audit what changed in the environment between open and check (mount changes, container restarts, symlink-swap tools) before recreating the store."],"exampleFix":"// before\n// ~/.caveman replaced by symlink to ~/dotfiles/caveman → parent spelling changes\nstore := ccr.Open(\"/home/me/.caveman/ccr.db\") // error: storage changed: database parent changed\n\n// after\nrm ~/.caveman\nmkdir -p ~/.caveman   # real directory, or exclude it from the dotfile symlink manager\nstore := ccr.Open(\"/home/me/.caveman/ccr.db\")","handlingStrategy":"try-catch","validationCode":"func assertCanonicalCCRParent(path string) error {\n\tparent := filepath.Dir(path)\n\tresolved, err := filepath.EvalSymlinks(parent)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ccr parent missing or unresolvable: %w\", err)\n\t}\n\tif resolved != parent {\n\t\treturn fmt.Errorf(\"%s contains a symlink component (resolves to %s)\", parent, resolved)\n\t}\n\treturn nil\n}\n// run at startup and before every checkGeneration-sensitive operation\n","typeGuard":null,"tryCatchPattern":"files, err := inspectSQLiteGeneration(path)\nif err != nil {\n\tif errors.Is(err, ErrStorageChanged) && !errors.Is(err, errStorageUnverifiable) {\n\t\t// confirmed environment change (parent swapped/removed) — treat the\n\t\t// store identity as changed and follow the quarantine/recovery path\n\t\treturn fmt.Errorf(\"ccr storage changed, manual recovery required: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Exclude the CCR directory from dotfile managers and symlink-farm tools.","Always build the database path with PrepareSQLitePathCanonical — non-canonical spellings are rejected by design.","Recreate the parent at the same canonical path if it was moved; don't repoint via symlink.","Alert on unexpected ErrStorageChanged without errStorageUnverifiable — it means a real environment swap, not an I/O hiccup.","Check for parent-directory deletion between open and check (container restarts, TMPDIR changes, volume remounts)."],"tags":["go","filesystem","sqlite","symlink","security"],"backgroundTag":"file-not-found","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"}