Skip to content

extract: skip a hard link item that refers to itself, refs #10393 (1.4-maint) - #10599

Merged
ThomasWaldmann merged 1 commit into
borgbackup:1.4-maintfrom
ThomasWaldmann:extract-hardlink-self-source-1.4
Nov 5, 2026
Merged

ThomasWaldmann merged 1 commit into
borgbackup:1.4-maintfrom
ThomasWaldmann:extract-hardlink-self-source-1.4

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

Description

An archive can contain the same hard link item twice. Two ways to get one:

  • borg create ::a input input/file3, where input/file3 is a hard link and the first member of its hard link group. The root input/file3 lies inside the earlier root input.
  • printf 'input/file1\ninput/file1\n' | borg create --paths-from-stdin ::a with a hard-linked input/file1. This is deterministic.

The duplicate is an h item whose source is its own path:

-rw-r--r-- input/file1 ->
hrw-r--r-- input/file1 -> input/file1
hrw-r--r-- input/file2 -> input/file1

borg extract first extracts input/file1. For the duplicate, the "remove existing files" step then unlinks that file, and hard linking the path to itself fails with ENOENT. Extract returns rc 1, and input/file1 is gone, along with its other hard links (input/file2 fails to link the same way), although the archive data is intact:

input/file1: link: [Errno 2] No such file or directory: '.../input/file1' -> '.../input/file1'
input/file2: link: [Errno 2] No such file or directory: '.../input/file1' -> '.../input/file2'

Fix: extract now skips a hard link item whose link target is its own path. That path was extracted already. The check runs before anything touches the filesystem. It uses the same link target that extract_helper would use (hardlink_masters and --strip-components taken into account). This is the extract-side part of #10394 (master), ported to 1.4's source / hardlink_masters hard link model. Because it works on the items as they are stored, it also makes existing archives that contain such duplicates restore correctly.

export-tar and mount get the same small guard:

  • export-tar wrote a LNKTYPE entry that links to itself. GNU tar and Python's tarfile ignore it, but bsdtar (libarchive) says "Skipping hardlink pointing to itself" and exits with rc 1.
  • mount counted the duplicate as another link, so st_nlink was one too high (3 instead of 2). Contents were fine.

The create side (not archiving such duplicates in the first place) is not part of this PR.

refs #10393

Checklist

  • PR is against 1.4-maint
  • New tests: test_extract_duplicate_hardlink_item (also with --strip-components), test_export_tar_duplicate_hardlink_item, test_fuse_mount_duplicate_hardlink_item. The extract and export-tar tests fail without the fix.
  • archiver.py and archive.py test modules pass locally (macOS). The FUSE tests, including the new one, pass on Linux with pyfuse3.

🤖 Generated with Claude Code

…#10393

If an archive has the same hard link item twice (e.g. a recursion root
inside an earlier root, or the same path given twice via --paths-from-stdin)
and the item's path is the link target of its hard link group, extract
removed the already extracted file and then failed to hard link the path
to itself. The file (and its other hard links) was missing afterwards.

Such a duplicate item is skipped now, its path was extracted already.
This also makes existing archives with such duplicates restore correctly.

export-tar and mount skip it likewise: export-tar added a hard link entry
pointing to itself (bsdtar refuses it and exits with rc 1), mount counted
it as an additional link (st_nlink one too high).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ThomasWaldmann
ThomasWaldmann merged commit 9947002 into borgbackup:1.4-maint Nov 5, 2026
13 checks passed
@ThomasWaldmann
ThomasWaldmann deleted the extract-hardlink-self-source-1.4 branch November 5, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant