Skip to content

create: archive recursion roots that are hard links of an earlier root, fixes #10388 - #10389

Open
ThomasWaldmann wants to merge 1 commit into
borgbackup:masterfrom
ThomasWaldmann:create-hardlinked-roots
Open

ThomasWaldmann wants to merge 1 commit into
borgbackup:masterfrom
ThomasWaldmann:create-hardlinked-roots

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

Fixes #10388.

Problem

borg create silently did not archive recursion roots that are hard links of an earlier root (rc 0, no warning):

  • borg create arch input/file1 input/file2 input/file3 (hard links of each other): only input/file1 was archived.
  • borg create arch input/file1 input: input/file2 and input/file3 were dropped while recursing into input.
  • borg create arch input/link input/target (link being a followed symlink to the file target, borg create should follow symlinks pointed to by roots #4737): input/target was dropped.

Cause

The duplicate root protection of #5603 put the (st_ino, st_dev) of each finished root into skip_inodes, and _rec_walk() skips everything with a matching inode. That is right for directories, but hard links are different paths with the same inode.

Fix

skip_key(path, st) builds the key used for skip_inodes:

  • directories: (st_ino, st_dev), as before. Same directory given twice, a symlink to a directory plus that directory, child directory first and parent after: all still archived once (Specifying the same path twice breaks hardlink during extraction #5603 protection unchanged, test_create_duplicate_root and test_extract_hardlinks_twice pass unchanged).
  • other fs objects: (st_ino, st_dev, path). Only the very same (normalized) path is skipped, so input/file1 input/file1 ./input/../input/file1 is still archived once, and input/file1 input archives input/file1 once.

The borg create help text about roots given twice is adapted (usage docs / man page not regenerated).

Tests

New in create_cmd_test.py, all four fail without the fix:

  • test_create_hardlinked_roots: three hard-linked roots, all in the archive with one hlid, extracting to one inode with st_nlink == 3.
  • test_create_hardlinked_root_and_parent_dir: input/file1 input, every path exactly once.
  • test_create_duplicate_file_root: same file given three times (two spellings) plus a hard link of it.
  • test_create_symlink_root_and_target_file: symlink to a file and the file, both archived.

Not changed by this PR

A directory root followed by a root inside it (borg create arch input input/file1) archives input/file1 twice. That is the same before and after this change: fs objects found while recursing never were in skip_inodes.

🤖 Generated with Claude Code

fixes borgbackup#10388

The duplicate root protection (borgbackup#5603) identified a finished root by
(st_ino, st_dev) only. Hard links are different paths with the same inode, so
they were taken for duplicates of the first root and silently not archived:

- borg create arch input/file1 input/file2 (file2 being a hard link of file1)
- borg create arch input/file1 input (hard links of file1 inside input)
- borg create arch input/link input/target (followed symlink to a file)

skip_key() now identifies directories by inode (as before) and all other fs
objects by inode and path, so only the very same path is skipped.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.15%. Comparing base (7a16a03) to head (6393c1e).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10389      +/-   ##
==========================================
- Coverage   88.19%   88.15%   -0.04%     
==========================================
  Files         103      103              
  Lines       18822    18826       +4     
  Branches     2919     2920       +1     
==========================================
- Hits        16600    16597       -3     
- Misses       1548     1552       +4     
- Partials      674      677       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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.

create: recursion roots that are hard links of an earlier root are silently not archived

1 participant