{"record":{"id":"69a6c617b4f6353c","repo":"abhigyanpatwari/GitNexus","slug":"refusing-to-delete-dbpath-resolved-path-real","errorCode":null,"errorMessage":"Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory","messagePattern":"Refusing to delete (.+?): resolved path (.+?) is outside storage directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/lbug/lbug-adapter.ts","lineNumber":896,"sourceCode":"    conn = usable.conn;\n    currentDbReadOnly = true;\n  } else {\n    // LadybugDB stores the database as a single file (not a directory).\n    // If the path already exists, it must be a valid LadybugDB database file.\n    // Remove stale empty directories or files from older versions.\n    try {\n      const stat = await fs.lstat(dbPath);\n      if (stat.isSymbolicLink()) {\n        // Never follow symlinks — just remove the link itself\n        await fs.unlink(dbPath);\n      } else if (stat.isDirectory()) {\n        // Verify path is within expected storage directory before deleting\n        const realPath = await fs.realpath(dbPath);\n        const parentDir = path.dirname(dbPath);\n        const realParent = await fs.realpath(parentDir);\n        const safePrefix = realParent.endsWith(path.sep) ? realParent : realParent + path.sep;\n        if (!realPath.startsWith(safePrefix) && realPath !== realParent) {\n          throw new Error(\n            `Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory`,\n          );\n        }\n        // Old-style directory database or empty leftover - remove it\n        await fs.rm(dbPath, { recursive: true, force: true });\n      }\n      // If it's a file, assume it's an existing LadybugDB database - LadybugDB will open it\n    } catch (err) {\n      if (!isMissingFileError(err)) {\n        throw err;\n      }\n      // Path doesn't exist, which is what LadybugDB wants for a new database\n    }\n\n    // -------------------------------------------------------------------------\n    // Cross-process critical section: acquire init lock, clean orphan sidecars,\n    // and open the database. The lock prevents a TOCTOU race where another\n    // process could create a fresh DB between our access() check and the","sourceCodeStart":878,"sourceCodeEnd":914,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/core/lbug/lbug-adapter.ts#L878-L914","documentation":"A safety guard in lbug-adapter.ts (line 832): when the path to clear is a directory (old-style directory database or leftover), its realpath must resolve inside the realpath'd parent storage directory before `fs.rm(..., {recursive, force})` runs. Symlinked dbPaths are never followed — only the link is unlinked. If realpath resolves outside the parent (symlink chains, bind mounts), deletion is refused; this prevents a crafted or accidentally-symlinked layout from deleting arbitrary directories.","triggerScenarios":"dbPath is a symlink whose target directory lives outside the storage dir (e.g. user symlinked ~/.gitnexus/…/db to another disk, but here it points at a real directory); or mount/realpath weirdness (bind mounts, container volumes) makes the target resolve to a path not prefixed by the parent's realpath.","commonSituations":"Users relocating GitNexus storage to another drive via symlinks; migrating ~/.gitnexus between machines with partial symlinking; exotic container volume layouts where parent and child resolve differently.","solutions":["Inspect the path from the message: run `readlink -f <dbPath>` and compare with `readlink -f <parentDir>`","If the target is genuinely the old directory database, remove it manually (`rm -rf <realPath>` after verifying), then re-run so a fresh database is created","Avoid symlinking the database path across filesystem boundaries; symlink the whole storage directory instead, so parent and child resolve consistently"],"exampleFix":"# before: db path is a symlink pointing elsewhere\n$ ln -s /mnt/other/db ~/.gitnexus/repo/db\n# after: remove manually, let GitNexus recreate in-place\n$ rm -rf /mnt/other/db\ngitnexus analyze --force /path/to/repo","handlingStrategy":"validation","validationCode":"import fs from 'node:path';\nimport path from 'node:path';\n\n// Before instructing any cleanup, ensure dbPath resolves inside its parent\nconst realPath = await fs.realpath(dbPath);\nconst realParent = await fs.realpath(path.dirname(dbPath));\nif (!realPath.startsWith(realParent + path.sep) && realPath !== realParent) {\n  throw new Error(`Refusing cleanup: ${dbPath} resolves to ${realPath}, outside ${realParent} — remove manually if intended`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await prepareDbPath(dbPath);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Refusing to delete')) {\n    // inspect with `readlink -f`; if the target is genuinely the old db, remove it manually and re-run\n  }\n  throw err;\n}","preventionTips":["Do not symlink individual database files out of the storage dir; relocate the whole storage directory instead","When moving ~/.gitnexus, move it atomically (rename) rather than leaving symlinks behind","Treat this refusal as a safety tripwire — always resolve it by inspection, never by forcing deletion flags"],"tags":["filesystem","safety","symlinks","path-traversal","cleanup","deletion-guard"],"backgroundTag":"path-traversal-guard","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}