{"record":{"id":"9914c2167e077c58","repo":"can1357/oh-my-pi","slug":"archive-hard-link-formatarchivepathforerror-nor","errorCode":null,"errorMessage":"Archive hard link '${formatArchivePathForError(normalizedPath)}' has an invalid target","messagePattern":"Archive hard link '(.+?)' has an invalid target","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/tar.ts","lineNumber":551,"sourceCode":"\t\t\tconst kind = typeFlag === \"1\" ? \"hard link\" : \"symlink\";\n\t\t\tconst portableLinkName = linkName.replace(/\\\\/g, \"/\");\n\t\t\tassertArchivePathString(portableLinkName, \"link target\", limits.maxPathBytes);\n\t\t\tconst targetPath =\n\t\t\t\ttypeFlag === \"1\"\n\t\t\t\t\t? normalizeArchiveEntryPath(portableLinkName)\n\t\t\t\t\t: path.posix.isAbsolute(portableLinkName)\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: normalizeArchiveLookupPath(path.posix.join(path.posix.dirname(normalizedPath), portableLinkName));\n\t\t\tconst entry: ArchiveIndexEntry = {\n\t\t\t\tpath: normalizedPath,\n\t\t\t\tisDirectory: false,\n\t\t\t\tsize: 0,\n\t\t\t\tmtimeMs,\n\t\t\t\tmode,\n\t\t\t};\n\t\t\tif (targetPath === undefined || Buffer.byteLength(targetPath, \"utf-8\") > limits.maxPathBytes) {\n\t\t\t\tif (kind === \"hard link\") {\n\t\t\t\t\tthrow new ArchiveError(\n\t\t\t\t\t\t`Archive hard link '${formatArchivePathForError(normalizedPath)}' has an invalid target`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tentry.storage = { type: \"link\", targetPath: portableLinkName, resolveTarget: false };\n\t\t\t\taddEntry(entry);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\taddEntry(entry, { kind, targetPath });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag !== \"0\" && typeFlag !== \"\\0\" && typeFlag !== \"7\" && typeFlag !== \"S\") continue;\n\t\tassertArchiveMemberSize(displaySize, normalizedPath, limits);\n\t\taddEntry({\n\t\t\tpath: normalizedPath,\n\t\t\tisDirectory: false,\n\t\t\tsize: displaySize,\n\t\t\tmtimeMs,\n\t\t\tmode,","sourceCodeStart":533,"sourceCodeEnd":569,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/tar.ts#L533-L569","documentation":"Thrown during tar indexing when a member with type flag '1' (hard link) has a target that cannot be resolved to a valid in-archive path: either the linkname is empty/absolute/otherwise unnormalizable (normalizeArchiveEntryPath returned undefined) or its byte length exceeds limits.maxPathBytes. Symlinks with the same problem are stored unresolved, but hard links must resolve to another member, so the library fails fast. This indicates a malformed or hostile archive rather than caller error.","triggerScenarios":"Parsing a tar containing a hard-link member whose linkname field is empty, absolute (leading '/'), contains '..' escaping beyond the root, or is longer than the configured maxPathBytes limit.","commonSituations":"Archives produced by non-conforming tar writers that emit absolute hardlink targets; hand-crafted or fuzzed archives (potential malicious input); unusually long paths combined with a strict maxPathBytes limit set by the embedding application.","solutions":["Repack the archive with GNU tar/bsdtar so hard links use valid relative in-archive targets: `tar -cf fixed.tar --format=gnu -C dir .`","Inspect the offending member (`tar -tvf file.tar | grep 'h'`) to see the bogus linkname.","If the archive is untrusted, reject it — the error is the library's path-traversal defense working as intended.","Raise limits.maxPathBytes in FormatReadOptions if the target is legitimately long, but only for trusted archives.","Extract the archive with a tolerant tool and re-archive the extracted tree as regular files (dereference links: `tar -cf out.tar -h ...`)."],"exampleFix":"// before: strict limit rejects long-but-legit link targets in a trusted archive\nconst entries = readTarEntriesFromBuffer(buf, { limits: { maxPathBytes: 256, ... } });\n// after\nconst entries = readTarEntriesFromBuffer(buf, { limits: { maxPathBytes: 4096, ... } });","handlingStrategy":"validation","validationCode":"// pre-screen untrusted archives: hardlink targets must be relative, non-empty, and within the byte limit\nimport * as fs from 'node:fs';\nfunction checkTarHardLinks(bytes, maxPathBytes) {\n  for (let off = 0; off + 512 <= bytes.byteLength; ) {\n    const header = bytes.subarray(off, off + 512);\n    if (header.every((b) => b === 0)) break;\n    const typeFlag = String.fromCharCode(header[156]);\n    if (typeFlag === '1') {\n      const linkName = header.subarray(157, 257).toString('utf-8').replace(/\\0.*$/, '');\n      if (!linkName || linkName.startsWith('/') || Buffer.byteLength(linkName) > maxPathBytes) {\n        throw new Error(`rejecting archive: hard link target '${linkName}' invalid`);\n      }\n    }\n    const size = parseInt(header.subarray(124, 136).toString().replace(/\\0/g, '').trim() || '0', 8) || 0;\n    off += 512 + Math.ceil(size / 512) * 512;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const entries = readTarEntriesFromBuffer(bytes, { limits });\n} catch (err) {\n  if (err instanceof ArchiveError && /hard link .* invalid target/.test(err.message)) {\n    throw new Error('Refusing archive: malformed hard link (possible malicious input)');\n  }\n  throw err;\n}","preventionTips":["Treat this error as a trust signal — do not loosen limits for untrusted archives.","Repack third-party archives with `tar -h -cf` (dereference) when hard links are not needed.","Set maxPathBytes generously (e.g. 4096) for trusted archives with deep paths.","Fuzz/validate producer tooling that writes linkname fields; absolute targets are non-conforming."],"tags":["tar","hardlink","path-validation","security"],"backgroundTag":"invalid-hardlink-target","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}