{"record":{"id":"848137b70891b3ae","repo":"Hmbown/CodeWhale","slug":"file-is-not-a-bounded-regular-single-link-file","errorCode":null,"errorMessage":"file is not a bounded regular single-link file","messagePattern":"file is not a bounded regular single-link file","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":348,"sourceCode":"    const arg = argv[i];\n    if (arg.startsWith(\"--\")) {\n      const key = arg.slice(2);\n      const next = argv[i + 1];\n      if (next === undefined || next.startsWith(\"--\")) flags[key] = true;\n      else { flags[key] = next; i += 1; }\n    } else positional.push(arg);\n  }\n  return { positional, flags };\n}\n\n/** Bounded, regular, single-link file reads; no symlink or FIFO following. */\nexport function readBoundedFile(path, maxBytes = MAX_ENVELOPE_BYTES) {\n  const before = lstatSync(path);\n  if (!before.isFile() || before.nlink !== 1) throw new Error(\"file is not a regular single-link file\");\n  const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));\n  try {\n    const stat = fstatSync(fd);\n    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\");\n    const bytes = Buffer.alloc(maxBytes + 1);\n    let size = 0;\n    while (size <= maxBytes) {\n      const count = readSync(fd, bytes, size, maxBytes + 1 - size, null);\n      if (!count) break;\n      size += count;\n    }\n    if (size > maxBytes) throw new Error(\"file exceeds size limit\");\n    return bytes.subarray(0, size);\n  } finally { closeSync(fd); }\n}\n\nfunction loadPrivateKeyFromEnv() {\n  refuseUnderCi();\n  let pem = process.env.CODEWHALE_FACTS_SIGNING_KEY;\n  const file = process.env.CODEWHALE_FACTS_SIGNING_KEY_FILE;\n  if (!pem && file) pem = readBoundedFile(file, 16 * 1024).toString(\"utf8\");\n  if (!pem) throw new Error(\"set CODEWHALE_FACTS_SIGNING_KEY (PEM) or CODEWHALE_FACTS_SIGNING_KEY_FILE\");","sourceCodeStart":330,"sourceCodeEnd":366,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L330-L366","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nCODEWHALE_FACTS_SIGNING_KEY_FILE=/tmp/shared/key.pem  # rewritten by a watcher\n// after\ninstall -m 600 key.pem /run/user/$UID/codewhale/key.pem\nCODEWHALE_FACTS_SIGNING_KEY_FILE=/run/user/$UID/codewhale/key.pem","handlingStrategy":"validation","validationCode":"const a = lstatSync(path);\nif (a.size > MAX_ENVELOPE_BYTES) throw new Error(`${path} exceeds ${MAX_ENVELOPE_BYTES} bytes`);\n// Ensure no concurrent writers before the read.\n","typeGuard":"null","tryCatchPattern":"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; }","preventionTips":["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"],"tags":["filesystem","security","race-condition"],"backgroundTag":"file-read-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}