Hmbown/CodeWhale · error · Error

file is not a bounded regular single-link file

Error message

file is not a bounded regular single-link file

What it means

After opening the file with O_NOFOLLOW, readBoundedFile re-statists via the fd and requires the file to still be a regular single-link file, within maxBytes, and to be the same inode/device as the pre-open lstat. This error means one of those post-open checks failed — i.e. the file changed between lstat and open (TOCTOU) or grew beyond the bound.

Solutions

  1. Re-run the publish when no other process is writing the file
  2. Verify the file size is under the limit (default MAX_ENVELOPE_BYTES)
  3. Check `stat` output: regular file, nlink 1, stable inode
  4. Move the file to a private directory only your process writes to

Example fix

// before
CODEWHALE_FACTS_SIGNING_KEY_FILE=/tmp/shared/key.pem  # rewritten by a watcher
// after
install -m 600 key.pem /run/user/$UID/codewhale/key.pem
CODEWHALE_FACTS_SIGNING_KEY_FILE=/run/user/$UID/codewhale/key.pem
Defensive patterns

Strategy: validation

Validate before calling

const a = lstatSync(path);
if (a.size > MAX_ENVELOPE_BYTES) throw new Error(`${path} exceeds ${MAX_ENVELOPE_BYTES} bytes`);
// Ensure no concurrent writers before the read.

Type guard

null

Try / catch

try { bytes = readBoundedFile(path); } catch (e) { if (e.message === 'file is not a bounded regular single-link file') { console.error(`${path} changed or grew during read — stop writers and retry once`); process.exit(2); } throw e; }

Prevention

When it happens

Trigger: readBoundedFile(path, maxBytes) where fstat on the open fd shows: not a regular file, nlink != 1, size > maxBytes, or ino/dev differing from the initial lstat — indicating the path was swapped, truncated, or oversized.

Common situations: Key file regenerated concurrently by another process; file replaced by a symlink mid-read (attack or race); accidentally pointing at an oversized bundle file; editing the key file while the publish script runs.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/848137b70891b3ae. Report an issue: GitHub.

Appendix: source

Thrown at web/scripts/facts-publish.mjs:348

    const arg = argv[i];
    if (arg.startsWith("--")) {
      const key = arg.slice(2);
      const next = argv[i + 1];
      if (next === undefined || next.startsWith("--")) flags[key] = true;
      else { flags[key] = next; i += 1; }
    } else positional.push(arg);
  }
  return { positional, flags };
}

/** Bounded, regular, single-link file reads; no symlink or FIFO following. */
export function readBoundedFile(path, maxBytes = MAX_ENVELOPE_BYTES) {
  const before = lstatSync(path);
  if (!before.isFile() || before.nlink !== 1) throw new Error("file is not a regular single-link file");
  const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
  try {
    const stat = fstatSync(fd);
    if (!stat.isFile() || stat.nlink !== 1 || stat.size > maxBytes || stat.ino !== before.ino || stat.dev !== before.dev) throw new Error("file is not a bounded regular single-link file");
    const bytes = Buffer.alloc(maxBytes + 1);
    let size = 0;
    while (size <= maxBytes) {
      const count = readSync(fd, bytes, size, maxBytes + 1 - size, null);
      if (!count) break;
      size += count;
    }
    if (size > maxBytes) throw new Error("file exceeds size limit");
    return bytes.subarray(0, size);
  } finally { closeSync(fd); }
}

function loadPrivateKeyFromEnv() {
  refuseUnderCi();
  let pem = process.env.CODEWHALE_FACTS_SIGNING_KEY;
  const file = process.env.CODEWHALE_FACTS_SIGNING_KEY_FILE;
  if (!pem && file) pem = readBoundedFile(file, 16 * 1024).toString("utf8");
  if (!pem) throw new Error("set CODEWHALE_FACTS_SIGNING_KEY (PEM) or CODEWHALE_FACTS_SIGNING_KEY_FILE");

View on GitHub (pinned to 433685b202)