can1357/oh-my-pi · error · ArchiveError

Archive hard link '${formatArchivePathForError(normalizedPat

Error message

Archive hard link '${formatArchivePathForError(normalizedPath)}' has an invalid target

What it means

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.

Source

Thrown at packages/utils/src/ar/tar.ts:551

			const kind = typeFlag === "1" ? "hard link" : "symlink";
			const portableLinkName = linkName.replace(/\\/g, "/");
			assertArchivePathString(portableLinkName, "link target", limits.maxPathBytes);
			const targetPath =
				typeFlag === "1"
					? normalizeArchiveEntryPath(portableLinkName)
					: path.posix.isAbsolute(portableLinkName)
						? undefined
						: normalizeArchiveLookupPath(path.posix.join(path.posix.dirname(normalizedPath), portableLinkName));
			const entry: ArchiveIndexEntry = {
				path: normalizedPath,
				isDirectory: false,
				size: 0,
				mtimeMs,
				mode,
			};
			if (targetPath === undefined || Buffer.byteLength(targetPath, "utf-8") > limits.maxPathBytes) {
				if (kind === "hard link") {
					throw new ArchiveError(
						`Archive hard link '${formatArchivePathForError(normalizedPath)}' has an invalid target`,
					);
				}
				entry.storage = { type: "link", targetPath: portableLinkName, resolveTarget: false };
				addEntry(entry);
				continue;
			}
			addEntry(entry, { kind, targetPath });
			continue;
		}
		if (typeFlag !== "0" && typeFlag !== "\0" && typeFlag !== "7" && typeFlag !== "S") continue;
		assertArchiveMemberSize(displaySize, normalizedPath, limits);
		addEntry({
			path: normalizedPath,
			isDirectory: false,
			size: displaySize,
			mtimeMs,
			mode,

View on GitHub (pinned to 9690622007)

Solutions

  1. Repack the archive with GNU tar/bsdtar so hard links use valid relative in-archive targets: `tar -cf fixed.tar --format=gnu -C dir .`
  2. Inspect the offending member (`tar -tvf file.tar | grep 'h'`) to see the bogus linkname.
  3. If the archive is untrusted, reject it — the error is the library's path-traversal defense working as intended.
  4. Raise limits.maxPathBytes in FormatReadOptions if the target is legitimately long, but only for trusted archives.
  5. Extract the archive with a tolerant tool and re-archive the extracted tree as regular files (dereference links: `tar -cf out.tar -h ...`).

Example fix

// before: strict limit rejects long-but-legit link targets in a trusted archive
const entries = readTarEntriesFromBuffer(buf, { limits: { maxPathBytes: 256, ... } });
// after
const entries = readTarEntriesFromBuffer(buf, { limits: { maxPathBytes: 4096, ... } });
Defensive patterns

Strategy: validation

Validate before calling

// pre-screen untrusted archives: hardlink targets must be relative, non-empty, and within the byte limit
import * as fs from 'node:fs';
function checkTarHardLinks(bytes, maxPathBytes) {
  for (let off = 0; off + 512 <= bytes.byteLength; ) {
    const header = bytes.subarray(off, off + 512);
    if (header.every((b) => b === 0)) break;
    const typeFlag = String.fromCharCode(header[156]);
    if (typeFlag === '1') {
      const linkName = header.subarray(157, 257).toString('utf-8').replace(/\0.*$/, '');
      if (!linkName || linkName.startsWith('/') || Buffer.byteLength(linkName) > maxPathBytes) {
        throw new Error(`rejecting archive: hard link target '${linkName}' invalid`);
      }
    }
    const size = parseInt(header.subarray(124, 136).toString().replace(/\0/g, '').trim() || '0', 8) || 0;
    off += 512 + Math.ceil(size / 512) * 512;
  }
}

Try / catch

try {
  const entries = readTarEntriesFromBuffer(bytes, { limits });
} catch (err) {
  if (err instanceof ArchiveError && /hard link .* invalid target/.test(err.message)) {
    throw new Error('Refusing archive: malformed hard link (possible malicious input)');
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/9914c2167e077c58. Report an issue: GitHub.