{"record":{"id":"5e32e46b37708e3b","repo":"affaan-m/ECC","slug":"label-must-remain-a-regular-non-symlink-file-w","errorCode":null,"errorMessage":"${label} must remain a regular, non-symlink file while it is opened.","messagePattern":"(.+?) must remain a regular, non-symlink file while it is opened\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/memory-vault.js","lineNumber":164,"sourceCode":"    assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);\n  }\n\n  const flags = fs.constants.O_RDONLY\n    | (fs.constants.O_NOFOLLOW || 0)\n    | (fs.constants.O_NONBLOCK || 0);\n  const descriptor = fs.openSync(filePath, flags);\n  try {\n    const opened = fs.fstatSync(descriptor, { bigint: true });\n    if (!opened.isFile()) {\n      throw new Error(`${label} must be a regular, non-symlink file.`);\n    }\n    const after = fs.lstatSync(filePath, { bigint: true });\n    if (\n      after.isSymbolicLink()\n      || !after.isFile()\n      || !sameFileIdentity(after, opened)\n    ) {\n      throw new Error(`${label} must remain a regular, non-symlink file while it is opened.`);\n    }\n    if (options.trustedRoot) {\n      assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);\n    }\n    if (opened.size > BigInt(maxBytes)) {\n      throw new Error(`${label} is too large (${opened.size} bytes).`);\n    }\n\n    const chunks = [];\n    let total = 0;\n    while (total <= maxBytes) {\n      const buffer = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));\n      const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null);\n      if (bytesRead === 0) break;\n      chunks.push(buffer.subarray(0, bytesRead));\n      total += bytesRead;\n    }\n    if (total > maxBytes) {","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/memory-vault.js#L146-L182","documentation":"After successfully opening a memory file, readRegularTextFile re-runs lstat on the path and compares identity (inode via sameFileIdentity) against the descriptor's fstat. If the path now resolves to a symlink, a non-regular file, or a different inode, the open and the on-disk state have diverged — a classic TOCTOU swap. The vault treats this as an attack signal and aborts rather than read possibly-attacker-controlled content.","triggerScenarios":"Between fs.openSync and the subsequent fs.lstatSync, another process replaces the file (rename/link/unlink) so the path no longer points at the originally-opened inode. Realistically only seen under concurrent modification, a hostile local process, a misbehaving sync tool that rewrote the file mid-read, or on Windows with affected libuv versions (1.49.0–1.50.x) where stat/lstat and fstat disagree on volume serial.","commonSituations":"Two agents or two CLI invocations writing the same memory file at once; an editor or sync engine rewriting the file during a vault scan; downgrading/upgrading Node across the 22.12–22.16 or 24.0–24.1 window on Windows where the libuv GetFileInformationByName bug left volume serial unset; a security tool quarantining and replacing the file mid-read.","solutions":["Retry the operation when no other process is touching the vault (avoid concurrent saves to the same id).","On Windows, upgrade Node to 22.17+ or 24.2+ (libuv 1.51.0+ contains the fix referenced in the sameFileIdentity comment).","Pause any sync tool (Syncthing, Dropbox, Maestral) that targets the vault directory and re-run the read.","If the file is genuinely being swapped by a legitimate pipeline, serialize vault access behind a lock so reads and writes do not overlap."],"exampleFix":"// before: concurrent processes both writing mem_x.md, reads intermittently fail\n// serialize with a lockfile\nconst properLockfile = require('proper-lockfile');\nawait properLockfile.lock(vaultRoot);\ntry { await readMemoryById('mem_x'); } finally { await properLockfile.unlock(vaultRoot); }\n// after: no in-flight swaps, identity check passes","handlingStrategy":"retry","validationCode":"const fs = require('fs');\nfunction stableIdentity(p) {\n  const a = fs.statSync(p, { bigint: true });\n  const b = fs.statSync(p, { bigint: true });\n  return a.ino === b.ino && (a.dev === b.dev);\n}\n// only a hint; the real guard is serializing access so the file does not change mid-read","typeGuard":null,"tryCatchPattern":"async function readStable(id, retries = 2) {\n  for (let i = 0; i <= retries; i++) {\n    try { return readMemoryById(id); }\n    catch (error) {\n      if (/remain a regular, non-symlink file/i.test(error.message) && i < retries) continue;\n      throw error;\n    }\n  }\n}","preventionTips":["Serialize concurrent vault access with a lock (proper-lockfile, lockfile, or a queue).","On Windows, use Node 22.17+ / 24.2+ (libuv 1.51.0+) to avoid the volume-serial stat mismatch.","Do not run sync tools that rewrite vault files mid-scan.","Treat intermittent 'remain a regular file' errors as a signal of concurrency, not a bug in the vault."],"tags":["security","toctou","memory-vault","filesystem","concurrency"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}