{"record":{"id":"79d3c01e282c3174","repo":"affaan-m/ECC","slug":"invalid-ecc-repo-root-unreadable-package-json-at","errorCode":null,"errorMessage":"Invalid ECC repo root: unreadable package.json at ${packageJsonPath}","messagePattern":"Invalid ECC repo root: unreadable package\\.json at (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/auto-update.js","lineNumber":155,"sourceCode":"\nfunction validateRepoRoot(repoRoot) {\n  const normalized = path.resolve(repoRoot);\n  const packageJsonPath = path.join(normalized, 'package.json');\n  const installApplyPath = path.join(normalized, 'scripts', 'install-apply.js');\n\n  if (!fs.existsSync(packageJsonPath)) {\n    throw new Error(`Invalid ECC repo root: missing package.json at ${packageJsonPath}`);\n  }\n\n  if (!fs.existsSync(installApplyPath)) {\n    throw new Error(`Invalid ECC repo root: missing install script at ${installApplyPath}`);\n  }\n\n  let pkgName = null;\n  try {\n    pkgName = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name;\n  } catch {\n    throw new Error(`Invalid ECC repo root: unreadable package.json at ${packageJsonPath}`);\n  }\n  if (!ECC_PACKAGE_NAMES.has(pkgName)) {\n    throw new Error(`Refusing to run install from untrusted repo root ${normalized}: package.json name '${pkgName}' is not an official ECC package.`);\n  }\n\n  return normalized;\n}\n\nfunction runExternalCommand(command, args, options = {}) {\n  const result = spawnSync(command, args, {\n    cwd: options.cwd,\n    env: options.env || process.env,\n    encoding: 'utf8',\n    maxBuffer: 10 * 1024 * 1024\n  });\n\n  if (result.error) {\n    throw result.error;","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/auto-update.js#L137-L173","documentation":"After successfully opening the file, readRegularTextFile re-stats the path with lstat and compares identity (via sameFileIdentity — inode must match) against the opened descriptor. If the path has been swapped for a symlink, stopped being a regular file, or now refers to a different inode, it throws this error. This is a TOCTOU defense: it guarantees the file was not replaced between open and read.","triggerScenarios":"A concurrent process replaces or unlinks-and-recreates the file (especially with a symlink) after readRegularTextFile opens it but before/while reading; an editor's atomic-save (write temp + rename) races with the read; an attacker swaps the path mid-read.","commonSituations":"Editors or build tools performing atomic saves while a memory read is in flight; sync clients (Dropbox, iCloud) churning files; parallel test runs mutating the same vault file; active tampering attempting to redirect the read.","solutions":["Retry the read — the error usually indicates a transient concurrent replacement, and the next attempt will see a stable regular file.","Stop processes that mutate the file during reads (editors with atomic save, sync clients, concurrent writers).","Serialize vault access: use locking or ensure only one process reads/writes a memory file at a time.","Investigate for tampering if the swap was not caused by a known local process — inode change to a symlink is an attack signature."],"exampleFix":"// before (racy)\nconst data = readRegularTextFile(vaultFile, { label: 'memory' }); // throws if editor renames over it\n\n// after (retry transient swap)\nfunction readStable(file, opts) {\n  try { return readRegularTextFile(file, opts); }\n  catch (e) {\n    if (/must remain a regular/.test(e.message)) return readRegularTextFile(file, opts); // one retry\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"// Cannot be pre-validated reliably (TOCTOU) — mitigate by stabilizing the file:\nconst before = fs.lstatSync(path);\n// ... ensure no writers are active (flock or app-level lock) before reading\nif (fs.lstatSync(path).ino !== before.ino) throw new Error('File changed during read setup; retry');","typeGuard":"const isStableRegularFile = (p) => {\n  const a = fs.lstatSync(p), b = fs.statSync(p);\n  return a.isFile() && !a.isSymbolicLink() && a.ino === b.ino;\n};","tryCatchPattern":"const MAX_TRIES = 3;\nfor (let i = 0; ; i++) {\n  try { return readRegularTextFile(file, { label: 'memory document', maxBytes }); }\n  catch (err) {\n    if (err.message.includes('must remain a regular') && i < MAX_TRIES - 1) {\n      await new Promise(r => setTimeout(r, 50 * (i + 1))); // backoff, file was swapped mid-read\n      continue;\n    }\n    throw err;\n  }\n}","preventionTips":["Serialize vault file access with file locks or a single-writer discipline.","Disable/avoid atomic-save editors and sync clients while batch-reading the vault.","Retry reads with short backoff — swaps are usually transient.","Escalate to a security review if inode swaps recur without a known cause."],"tags":["filesystem","symlink","race-condition","toctou","memory-vault"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}