{"record":{"id":"74cfc6242d57a029","repo":"Hmbown/CodeWhale","slug":"file-is-not-a-regular-single-link-file","errorCode":null,"errorMessage":"file is not a regular single-link file","messagePattern":"file is not a regular single-link file","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":344,"sourceCode":"function parseArgs(argv) {\n  const positional = [];\n  const flags = {};\n  for (let i = 0; i < argv.length; i += 1) {\n    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();","sourceCodeStart":326,"sourceCodeEnd":362,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L326-L362","documentation":"readBoundedFile performs defensive reads of key/envelope files: it first lstat()s the path and requires a regular file with exactly one hard link, throwing this error otherwise. This prevents following symlinks, FIFOs, device nodes, or hardlinked files that could be swapped or point elsewhere.","triggerScenarios":"Calling readBoundedFile(path) where the path is a symlink (lstat isFile() false), a FIFO/socket/device, or a regular file with nlink > 1 (hard-linked).","commonSituations":"Pointing CODEWHALE_FACTS_SIGNING_KEY_FILE at a symlink into a secrets manager or dotfiles symlink farm; a temp file hard-linked by a backup tool; the key file being created by a pipe.","solutions":["Replace the symlink with a real copy of the file (cp -L) and point at the copy","Check with `ls -l` and `stat` for link count and type; remove extra hard links","Ensure the file is created as a regular file, not a FIFO or socket","If symlinking is intentional for your workflow, read the target yourself and pass the bytes/env var instead"],"exampleFix":"// before\nCODEWHALE_FACTS_SIGNING_KEY_FILE=~/.secrets/key.pem   # symlink\n// after\ncp -L ~/.secrets/key.pem ./key.pem && stat -c '%h %F' ./key.pem\nCODEWHALE_FACTS_SIGNING_KEY_FILE=./key.pem","handlingStrategy":"validation","validationCode":"import { lstatSync, constants as fsC } from 'node:fs';\nconst st = lstatSync(path);\nif (!st.isFile() || st.nlink !== 1) throw new Error(`${path} must be a regular single-link file`);","typeGuard":"const isRegularSingleLink = (path) => { try { const s = lstatSync(path); return s.isFile() && s.nlink === 1; } catch { return false; } };","tryCatchPattern":"try { pem = readBoundedFile(keyFile, 16 * 1024); } catch (e) { if (e.message === 'file is not a regular single-link file') { console.error(`Replace symlink/FIFO/hardlink at ${keyFile} with a real file`); process.exit(2); } throw e; }","preventionTips":["Use `cp -L` to materialize symlinked secrets into real files","Check `stat -c '%h %F'` on key files before pointing the script at them","Keep secrets in a dedicated directory without dotfiles symlink farms","Never point the script at pipes or process substitution targets"],"tags":["filesystem","security","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}