{"record":{"id":"0e6e9a04d2aa3fb3","repo":"JuliusBrussee/caveman","slug":"w-w-inspect-database-parent-v","errorCode":null,"errorMessage":"%w: %w: inspect database parent: %v","messagePattern":"%w: %w: inspect database parent: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/store_generation.go","lineNumber":43,"sourceCode":"// a parent directory whose mode was loosened. It still wraps ErrStorageChanged,\n// so a caller that only asks \"is this store usable right now\" is unaffected —\n// only the terminal quarantine decision looks for it.\nvar 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","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/store_generation.go#L25-L61","documentation":"inspectSQLiteGeneration (engine/ccr/store_generation.go:43) verifies the identity of the SQLite database and its parent directory before trusting the recovery store. When resolving symlinks of the database's parent directory fails for any reason other than 'does not exist' (e.g. EACCES, ELOOP, I/O errors), it wraps the failure with both ErrStorageChanged and errStorageUnverifiable. The errStorageUnverifiable marker signals the store could NOT confirm the database was replaced — only that it cannot be verified right now — so terminal quarantine decisions that match on errStorageUnverifiable can distinguish this from a confirmed swap (line 46).","triggerScenarios":"checkGeneration (and its callers openWithBudgetHooks / secureSQLiteFiles / the generation-capture test) invoke inspectSQLiteGeneration and filepath.EvalSymlinks(parent) fails with an error other than os.ErrNotExist — e.g. permission denied on a component of the parent path, too many symlink levels, or a transient filesystem I/O error.","commonSituations":"Running the engine under a service account that lost execute permission on ~/.caveman or an intermediate directory; a symlink chain in the CCR path (common with dotfile managers or mounted home dirs) creating a loop; containerized runs where the data volume is mounted with restrictive modes; NFS/overlayfs transient failures.","solutions":["Check and restore execute/search permission on the database's parent directory and all path components (chmod, or run as the owning user).","Inspect the wrapped %v error: EACCES means permissions, ELOOP means a symlink cycle — fix the specific filesystem condition.","Verify ~/.caveman (or the configured CCR dir) is a real directory on a local filesystem, not a symlink loop or flaky network mount.","Ensure the path passed to the store came from PrepareSQLitePathCanonical; non-canonical spellings are intentionally rejected.","If this occurs at startup only, confirm the parent directory exists before opening the store (a missing parent takes the different 'database parent changed' path at line 46)."],"exampleFix":"// before\n// ~/.caveman owned by root; engine runs as service user → EvalSymlinks fails with EACCES\n// error: storage changed: storage identity could not be verified: inspect database parent: permission denied\n\n// after\nsudo chown -R serviceuser:servicegroup ~/.caveman\nchmod 700 ~/.caveman","handlingStrategy":"try-catch","validationCode":"func ccrParentAccessible(path string) error {\n\tparent := filepath.Dir(path)\n\tinfo, err := os.Stat(parent)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ccr parent %s: %w\", parent, err)\n\t}\n\tif !info.IsDir() {\n\t\treturn fmt.Errorf(\"%s is not a directory\", parent)\n\t}\n\tif _, err := os.Stat(filepath.Join(parent, \".probe\")); os.IsPermission(err) {\n\t\treturn fmt.Errorf(\"no search permission on %s\", parent)\n\t}\n\treturn nil\n}\n// call before opening the store\n","typeGuard":null,"tryCatchPattern":"files, err := inspectSQLiteGeneration(path)\nif err != nil {\n\tif errors.Is(err, ErrStorageChanged) && errors.Is(err, errStorageUnverifiable) {\n\t\t// cannot verify right now (permissions/IO) — do NOT quarantine the store;\n\t\t// surface a retryable environment error\n\t\treturn fmt.Errorf(\"ccr store temporarily unverifiable, fix permissions/mount and retry: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Ensure the service user has execute permission on every component of the CCR path.","Avoid symlink loops in home directories managed by dotfile tools.","Run the store on local filesystems, not flaky NFS/network mounts.","Distinguish errStorageUnverifiable (transient) from a confirmed ErrStorageChanged before making terminal decisions.","Pre-create and chown the CCR directory during deployment, before first engine run."],"tags":["go","filesystem","sqlite","permissions","symlink"],"backgroundTag":"permission-denied","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"}