{"record":{"id":"503924edb097cc4c","repo":"affaan-m/ECC","slug":"gate-variant-invalid","errorCode":"gate.variant_invalid","errorMessage":"variant trees must contain only regular files and directories","messagePattern":"variant trees must contain only regular files and directories","errorType":"exception","errorClass":"GateError","httpStatus":null,"severity":"error","filePath":"scripts/lib/eval-harness/gate.js","lineNumber":62,"sourceCode":"]);\n\nclass GateError extends Error {\n  constructor(code, message, details = {}) {\n    super(message);\n    this.name = 'GateError';\n    this.code = code;\n    Object.assign(this, details);\n  }\n}\n\nfunction listFiles(dir, base = dir, acc = []) {\n  for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {\n    if (entry.name === 'node_modules' || entry.name === '.git') {\n      continue;\n    }\n    const full = path.join(dir, entry.name);\n    if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {\n      throw new GateError('gate.variant_invalid', 'variant trees must contain only regular files and directories');\n    }\n    if (entry.isDirectory()) {\n      listFiles(full, base, acc);\n    } else if (entry.isFile()) {\n      acc.push(path.relative(base, full).split(path.sep).join('/'));\n    }\n  }\n  return acc;\n}\n\n/** Read the opened regular file, never reopen a previously checked pathname.\n * No-follow/nonblocking flags reduce symlink and special-file hazards where\n * supported. Descriptor/path identity also rejects symlinks on other hosts.\n * This is static inspection of a caller-controlled tree, not OS containment.\n */\nfunction readRegularFile(filePath, encoding) {\n  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0);\n  let fd;","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/eval-harness/gate.js#L44-L80","documentation":"listFiles() walks a variant tree and rejects anything that is not a regular file or directory — most importantly symbolic links, but also sockets, FIFOs, and device nodes — with 'gate.variant_invalid'. Symlinks are forbidden because variants are content-addressed by digest; a symlink could escape the tree (path traversal) or make the digest non-reproducible. The same guard backs digestDir, loadVariant, and scanTripwires.","triggerScenarios":"Calling loadVariant/gate on a variant directory containing a symlink (e.g. node_modules-style links, a symlinked config, or .git worktree links not skipped), a Unix socket left by a dev server, or a named pipe inside the tree.","commonSituations":"Checking out fixtures via a tool that creates symlinks; copying variant trees with cp -r that preserved links; running a dev server in the variant dir that left a .sock file; pnpm-style symlinked node_modules layouts (only node_modules and .git are skipped, not other link farms).","solutions":["Find the offending entry with: find <variant-dir> -type l -o ! -type f -o ! -type d (excluding node_modules/.git).","Replace symlinks with real copies of their targets (cp -L) or remove them if unnecessary.","Delete stray sockets/FIFOs (e.g. leftover *.sock files) from the tree.","Re-copy the variant tree with dereferencing (rsync -L or cp -rL) so only regular files/dirs remain."],"exampleFix":"// before: variant dir contains a symlink to shared fixtures\nconst variant = loadVariant('./variants/base'); // gate.variant_invalid\n\n// after: materialize links as real files first\n// cp -rL ./variants/base ./variants/base-resolved && rm -rf ./variants/base-resolved/node_modules\nconst variant = loadVariant('./variants/base-resolved');","handlingStrategy":"validation","validationCode":"const { execSync } = require('child_process');\nconst links = execSync(`find ${dir} -type l -not -path '*/node_modules/*' -not -path '*/.git/*'`).toString().trim();\nif (links) throw new Error(`symlinks in variant tree must be resolved first:\\n${links}`);","typeGuard":"function isRegularTree(dir) {\n  for (const e of fs.readdirSync(dir, { withFileTypes: true, recursive: true })) {\n    if (e.name === 'node_modules' || e.name === '.git') continue;\n    if (e.isSymbolicLink() || (!e.isFile() && !e.isDirectory())) return false;\n  }\n  return true;\n}","tryCatchPattern":"try {\n  const files = listFiles(variantDir, variantDir, []);\n} catch (e) {\n  if (e instanceof GateError && e.code === 'gate.variant_invalid') {\n    throw new Error(`${e.message}; fix with: find ${variantDir} -type l -exec cp -L {} {}.real \\\\; -exec mv {}.real {} \\\\;`);\n  }\n  throw e;\n}","preventionTips":["Materialize variant trees with cp -rL / rsync -L so no symlinks survive.","Clean stray sockets/FIFOs (*.sock) from directories used as variants.","Avoid running dev servers inside variant directories.","Keep only node_modules/.git exclusions in mind — every other non-regular entry fails the gate."],"tags":["filesystem","symlink","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}