{"record":{"id":"6805802da5a0c93f","repo":"paperclipai/paperclip","slug":"acpx-snapshot-escaped-its-package","errorCode":null,"errorMessage":"ACPX snapshot escaped its package","messagePattern":"ACPX snapshot escaped its package","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":109,"sourceCode":"      : null;\n  };\n  const copy = async (\n    source: string,\n    target: string,\n    root: string,\n  ): Promise<void> => {\n    if (++filesCopied > 30_000)\n      throw new Error(\"ACPX package snapshot exceeds its file bound\");\n    const before = await lstat(source, { bigint: true });\n    if (before.isSymbolicLink()) {\n      const canonical = await realpath(source);\n      const mapped = mapPath(canonical);\n      // Package-manager links to unqualified packages do not grant import authority.\n      if (mapped) await symlink(mapped, target);\n      return;\n    }\n    if (!within(root, await realpath(source)))\n      throw new Error(\"ACPX snapshot escaped its package\");\n    if (before.isDirectory()) {\n      await mkdir(target, { mode: 0o700 });\n      directories.push(target);\n      for (const entry of await readdir(source))\n        await copy(join(source, entry), join(target, entry), root);\n      if (!same(before, await lstat(source, { bigint: true })))\n        throw new Error(\"ACPX package directory changed during snapshot\");\n      return;\n    }\n    if (!before.isFile() || before.size > 16n * 1024n * 1024n)\n      throw new Error(\"ACPX module must be a bounded regular file\");\n    bytesCopied += Number(before.size);\n    if (bytesCopied > MAX_PACKAGE_SNAPSHOT_BYTES)\n      throw new Error(\"ACPX package snapshot exceeds its byte bound\");\n    const handle = await open(\n      source,\n      constants.O_RDONLY | constants.O_NOFOLLOW,\n    );","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L91-L127","documentation":"During snapshot copy, for non-symlink entries the library verifies via realpath that the source resolves within the admitted package root; if not, the snapshot 'escaped its package' and it throws. This blocks path traversal — entries that escape the root via symlinks resolved as real directories or other aliasing — from being inducted into the private snapshot.","triggerScenarios":"copy() encounters a directory/file whose realpath(source) is outside the `root` package root — e.g. a hard-linked tree, a bind-mounted path, or a directory symlink resolved before the isSymbolicLink branch (variants where lstat reports a real dir but the canonical path leaves the root), or a symlink swapped in between lstat and realpath.","commonSituations":"A package containing symlinks that lstat sees as directories due to a race; copy-on-write/hardlink-style installs (pnpm) where entries resolve outside the claimed root; misconfigured package root that doesn't actually contain the entry.","solutions":["Verify the package root is the true canonical parent of all contents (no entries resolving outside it)","Use a real (non-symlinked) package directory, or an install layout where packages own their files (npm's isolated layout rather than symlinked hoisting)","Check for concurrent modification racing the lstat/realpath window and re-run when the tree is stable","Audit package contents for outbound symlinks before admitting the root"],"exampleFix":"// before: root is a symlinked hoist location\nroots: ['/project/node_modules/pkg']\n// after: root is the real package store path\nroots: ['/project/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg']","handlingStrategy":"validation","validationCode":"const real = await fs.realpath(source);\nif (!real.startsWith(await fs.realpath(root) + path.sep)) {\n  throw new Error(`entry escapes package root: ${source} -> ${real}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await createAcpxPrivateSnapshot({ roots: [root] });\n} catch (e) {\n  if (e.message.includes('escaped its package')) {\n    throw new Error(`Root ${root} contains entries resolving outside it; use the real package path`);\n  }\n  throw e;\n}","preventionTips":["Audit packages for outbound symlinks before admitting their roots","Use install layouts where packages own real files (avoid symlink-heavy hoisting when snapshotting)","Resolve the root with realpath before admitting it so it matches canonical entry paths"],"tags":["security","path-traversal","filesystem","snapshot"],"backgroundTag":"path-traversal-blocked","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}