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
- Re-run the publish when no other process is writing the file
- Verify the file size is under the limit (default MAX_ENVELOPE_BYTES)
- Check `stat` output: regular file, nlink 1, stable inode
- 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
- Do not edit key/envelope files while a publish is running
- Read from a private, single-writer directory (e.g. /run/user/$UID)
- Verify file sizes are under the limit before invoking the script
- If this error appears repeatedly with no local writer, investigate for tampering
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
- external credential path was redirected while opening
- Automation lock must not be a reparse point
- Automation lock must not have hard links
- built-in plugin path may not be a symbolic link or reparse…
- Codewhale-owned credential file must be singly linked
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)