can1357/oh-my-pi · error · ArchiveError
Archive path '${archivePath}' crosses a cyclic symlink
Error message
Archive path '${archivePath}' crosses a cyclic symlink What it means
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.
Source
Thrown at packages/utils/src/ar/entries.ts:96
let replacement: string | undefined;
for (let end = resolvedPath.length; end > 0; end = resolvedPath.lastIndexOf("/", end - 1)) {
const entry = entries.get(resolvedPath.slice(0, end));
if (entry?.storage?.type !== "link" || (!entry.isDirectory && !entry.storage.resolveTarget)) continue;
const suffix = resolvedPath.slice(end + 1);
replacement = suffix
? entry.storage.targetPath
? `${entry.storage.targetPath}/${suffix}`
: suffix
: entry.storage.targetPath;
break;
}
if (replacement === undefined) return resolvedPath;
// The bound counts performed rewrites, so a chain of exactly
// maxLinkDepth aliases still resolves; only needing one more trips it.
if (++rewrites > maxLinkDepth) break;
resolvedPath = replacement;
}
throw new ArchiveError(`Archive path '${archivePath}' crosses a cyclic symlink`);
}
View on GitHub (pinned to 9690622007)
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
Example fix
// before (default 40-link bound trips)
const entries = await readArchive(cyclicDeb);
// after
const entries = await readArchive(cyclicDeb, { limits: { ...DEFAULT_ARCHIVE_LIMITS, maxLinkDepth: 80 } }); Defensive patterns
Strategy: validation
Validate before calling
// pre-scan symlinks for cycles before resolving paths
function detectCycle(links) {
const seen = new Set();
const visit = (p) => {
if (seen.has(p)) return true;
seen.add(p);
const target = links.get(p);
return target ? visit(target) : false;
};
return [...links.keys()].some(visit);
} Type guard
function isCyclicSymlinkError(err) {
return err instanceof ArchiveError && err.message.includes('crosses a cyclic symlink');
} Try / catch
try {
resolved = resolvedPath(entry);
} catch (err) {
if (err instanceof ArchiveError && err.message.includes('cyclic symlink')) {
return null; // skip unresolvable entry
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Archive contains cyclic or unsupported links
- Invalid CPIO archive: symlink '${recordPath}' has an invalid
- Invalid LZH symbolic link '${header.path}'
- Archive symlink escapes extraction dir: ${link.path} -> ${li
- Unsupported compressed RAR4 symlink '${path}'
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/1aaa41f322d4885b.
Report an issue: GitHub.