{"record":{"id":"1aaa41f322d4885b","repo":"can1357/oh-my-pi","slug":"archive-path-archivepath-crosses-a-cyclic-sym","errorCode":null,"errorMessage":"Archive path '${archivePath}' crosses a cyclic symlink","messagePattern":"Archive path '(.+?)' crosses a cyclic symlink","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/entries.ts","lineNumber":96,"sourceCode":"\t\tlet replacement: string | undefined;\n\t\tfor (let end = resolvedPath.length; end > 0; end = resolvedPath.lastIndexOf(\"/\", end - 1)) {\n\t\t\tconst entry = entries.get(resolvedPath.slice(0, end));\n\t\t\tif (entry?.storage?.type !== \"link\" || (!entry.isDirectory && !entry.storage.resolveTarget)) continue;\n\t\t\tconst suffix = resolvedPath.slice(end + 1);\n\t\t\treplacement = suffix\n\t\t\t\t? entry.storage.targetPath\n\t\t\t\t\t? `${entry.storage.targetPath}/${suffix}`\n\t\t\t\t\t: suffix\n\t\t\t\t: entry.storage.targetPath;\n\t\t\tbreak;\n\t\t}\n\t\tif (replacement === undefined) return resolvedPath;\n\t\t// The bound counts performed rewrites, so a chain of exactly\n\t\t// maxLinkDepth aliases still resolves; only needing one more trips it.\n\t\tif (++rewrites > maxLinkDepth) break;\n\t\tresolvedPath = replacement;\n\t}\n\tthrow new ArchiveError(`Archive path '${archivePath}' crosses a cyclic symlink`);\n}\n","sourceCodeStart":78,"sourceCodeEnd":98,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/entries.ts#L78-L98","documentation":"When resolving archive entry paths, the library rewrites symlink aliases up to maxLinkDepth times (default 40). If a path still needs another rewrite after that many steps, the link chain is considered cyclic (an a->b->a loop or an over-long chain) and the library refuses to resolve it rather than looping forever. This protects callers from infinite aliasing inside untrusted archives.","triggerScenarios":"Reading or resolving an entry whose path traverses symlinks that form a cycle — e.g. members 'a' -> 'b', 'b' -> 'a', then resolving path 'a/file.txt' — or a legitimate symlink chain deeper than maxLinkDepth (40) links, via resolvedPath, resolvedChildPath, or resolvePendingLinks during archive indexing/extraction.","commonSituations":"Malicious archives crafted with symlink loops (zip-symlink/deb data.tar attacks), and rare legitimate archives with extremely deep symlink chains exceeding the 40-link bound.","solutions":["Inspect the archive's symlink members and break the cycle (delete or rename the looping link) before re-reading","If the chain is legitimate but deep, raise the limits option's maxLinkDepth when calling the reader","Extract with symlink resolution disabled or into a location where you resolve links manually","Treat the archive as untrusted if the cycle is intentional — reject or sanitize it"],"exampleFix":"// before (default 40-link bound trips)\nconst entries = await readArchive(cyclicDeb);\n// after\nconst entries = await readArchive(cyclicDeb, { limits: { ...DEFAULT_ARCHIVE_LIMITS, maxLinkDepth: 80 } });","handlingStrategy":"validation","validationCode":"// pre-scan symlinks for cycles before resolving paths\nfunction detectCycle(links) {\n  const seen = new Set();\n  const visit = (p) => {\n    if (seen.has(p)) return true;\n    seen.add(p);\n    const target = links.get(p);\n    return target ? visit(target) : false;\n  };\n  return [...links.keys()].some(visit);\n}","typeGuard":"function isCyclicSymlinkError(err) {\n  return err instanceof ArchiveError && err.message.includes('crosses a cyclic symlink');\n}","tryCatchPattern":"try {\n  resolved = resolvedPath(entry);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes('cyclic symlink')) {\n    return null; // skip unresolvable entry\n  }\n  throw err;\n}","preventionTips":["Treat archives with symlink loops as untrusted and sanitize before reading","Raise limits.maxLinkDepth only if you legitimately have deep chains (default 40)","When extracting, prefer policies that never follow in-archive symlinks outside the target dir"],"tags":["symlink","archive","cycle","path-resolution"],"backgroundTag":"symlink-cycle","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}