affaan-m/ECC · error · Error

${label} must remain a regular, non-symlink file while it is

Error message

${label} must remain a regular, non-symlink file while it is opened.

What it means

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.

Source

Thrown at scripts/lib/memory-vault.js:164

    assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);
  }

  const flags = fs.constants.O_RDONLY
    | (fs.constants.O_NOFOLLOW || 0)
    | (fs.constants.O_NONBLOCK || 0);
  const descriptor = fs.openSync(filePath, flags);
  try {
    const opened = fs.fstatSync(descriptor, { bigint: true });
    if (!opened.isFile()) {
      throw new Error(`${label} must be a regular, non-symlink file.`);
    }
    const after = fs.lstatSync(filePath, { bigint: true });
    if (
      after.isSymbolicLink()
      || !after.isFile()
      || !sameFileIdentity(after, opened)
    ) {
      throw new Error(`${label} must remain a regular, non-symlink file while it is opened.`);
    }
    if (options.trustedRoot) {
      assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);
    }
    if (opened.size > BigInt(maxBytes)) {
      throw new Error(`${label} is too large (${opened.size} bytes).`);
    }

    const chunks = [];
    let total = 0;
    while (total <= maxBytes) {
      const buffer = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));
      const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null);
      if (bytesRead === 0) break;
      chunks.push(buffer.subarray(0, bytesRead));
      total += bytesRead;
    }
    if (total > maxBytes) {

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Retry the operation when no other process is touching the vault (avoid concurrent saves to the same id).
  2. On Windows, upgrade Node to 22.17+ or 24.2+ (libuv 1.51.0+ contains the fix referenced in the sameFileIdentity comment).
  3. Pause any sync tool (Syncthing, Dropbox, Maestral) that targets the vault directory and re-run the read.
  4. If the file is genuinely being swapped by a legitimate pipeline, serialize vault access behind a lock so reads and writes do not overlap.

Example fix

// before: concurrent processes both writing mem_x.md, reads intermittently fail
// serialize with a lockfile
const properLockfile = require('proper-lockfile');
await properLockfile.lock(vaultRoot);
try { await readMemoryById('mem_x'); } finally { await properLockfile.unlock(vaultRoot); }
// after: no in-flight swaps, identity check passes
Defensive patterns

Strategy: retry

Validate before calling

const fs = require('fs');
function stableIdentity(p) {
  const a = fs.statSync(p, { bigint: true });
  const b = fs.statSync(p, { bigint: true });
  return a.ino === b.ino && (a.dev === b.dev);
}
// only a hint; the real guard is serializing access so the file does not change mid-read

Try / catch

async function readStable(id, retries = 2) {
  for (let i = 0; i <= retries; i++) {
    try { return readMemoryById(id); }
    catch (error) {
      if (/remain a regular, non-symlink file/i.test(error.message) && i < retries) continue;
      throw error;
    }
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/5e32e46b37708e3b. Report an issue: GitHub.