Filesystems without native hard links

37 views
Skip to first unread message

Daniel Distler

unread,
Sep 20, 2025, 3:44:47 AM9/20/25
to bup-...@googlegroups.com
Hi bup-list,

as of bup 0.33.8 fsck uses hard links (see https://github.com/bup/bup/commit/
9c43667f206bd34fde6c57df89c17eb40bf210af). Unfortunately hard links are not
supported on many filesystems, including e.g. FUSE-based filesystems (e.g.,
CryFS) and older formats like FAT and exFAT.

When bup is run on these filesystems, the `os.link` operation fails. This
results in cryptic errors like `OSError: [Errno 34] Numerical result out of
range`, which is not intuitive for the user.

Are there other ways of listing input files for par instead of linking them
into a tmp folder?

Best,
Daniel


Rob Browning

unread,
Sep 20, 2025, 6:52:12 PM9/20/25
to Daniel Distler, bup-...@googlegroups.com
Daniel Distler <daniel.dist...@posteo.de> writes:

> Are there other ways of listing input files for par instead of linking them
> into a tmp folder?

I suppose one possible, simpler option for now would be to fall back to
shutil.copy2() or similar when there's a suitable link failure. If you
like, you could adjust fsck.py to try that instead.

> When bup is run on these filesystems, the `os.link` operation fails. This
> results in cryptic errors like `OSError: [Errno 34] Numerical result out of
> range`, which is not intuitive for the user.

That seems odd -- what system/fs? I ask because linux link(2) says that
should be EPERM.

Thanks
--
Rob Browning
rlb @defaultvalue.org and @debian.org
GPG as of 2011-07-10 E6A9 DA3C C9FD 1FF8 C676 D2C4 C0F0 39E9 ED1B 597A
GPG as of 2002-11-03 14DD 432F AE39 534D B592 F9A0 25C8 D377 8C7E 73A4

Greg Troxel

unread,
Sep 20, 2025, 7:19:36 PM9/20/25
to Rob Browning, Daniel Distler, bup-...@googlegroups.com
Rob Browning <r...@defaultvalue.org> writes:

> Daniel Distler <daniel.dist...@posteo.de> writes:
>
>> Are there other ways of listing input files for par instead of linking them
>> into a tmp folder?
>
> I suppose one possible, simpler option for now would be to fall back to
> shutil.copy2() or similar when there's a suitable link failure. If you
> like, you could adjust fsck.py to try that instead.
>
>> When bup is run on these filesystems, the `os.link` operation fails. This
>> results in cryptic errors like `OSError: [Errno 34] Numerical result out of
>> range`, which is not intuitive for the user.
>
> That seems odd -- what system/fs? I ask because linux link(2) says that
> should be EPERM.

I would have said the right error was EOPNOTSUPP. I looked up POSIX,
and it seems that hard links are simply required.

https://pubs.opengroup.org/onlinepubs/9799919799/functions/link.html

But agreed a code change to do something else if they don't work sounds
good.

Rob Browning

unread,
Sep 20, 2025, 11:31:31 PM9/20/25
to Greg Troxel, Daniel Distler, bup-...@googlegroups.com
Greg Troxel <g...@lexort.com> writes:

> I would have said the right error was EOPNOTSUPP.

Right, looks like that's what FreeBSD does.

> But agreed a code change to do something else if they don't work sounds
> good.

I'll probably adjust the code to catch EOPNOTSUPP and EPERM and try
copy2() or similar in either case.

Aaron M. Ucko

unread,
Sep 21, 2025, 12:04:10 AM9/21/25
to bup-...@googlegroups.com
Rob Browning <rlb-A9c2TQsEE...@public.gmane.org> writes:

> I'll probably adjust the code to catch EOPNOTSUPP and EPERM and try
> copy2() or similar in either case.

Sounds good to me FWIW, but I'd suggest covering additional error codes;
the OP apparently encountered ERANGE (!) and AFS meanwhile disallows
cross-directory links even within a volume (presumably because its ACLs
have directory granularity), albeit with implementation-dependent error
codes: classic OpenAFS reports EXDEV and Linux's kAFS reports EREMOTEIO.
One can also plausibly make a case for EMLINK.

--
Aaron M. Ucko, KB1CJC (amu at alum.mit.edu, ucko at debian.org)
http://www.mit.edu/~amu/ | http://stuff.mit.edu/cgi/finger/?a...@monk.mit.edu

Daniel Distler

unread,
Sep 21, 2025, 7:09:36 AM9/21/25
to Rob Browning, Greg Troxel, bup-...@googlegroups.com
I did see the ERANGE with CryFS on Fedora Linux 41. This might well be an
issue in CryFS. I guess instead of explicitly failing the link request with
EPERM, EOPNOTSUPP or something similar the call just passes on some error of
some internal call.

But why does bup need the link / copy? Would it work to replace
```
rc = par2(b'create', [b'-n1', b'-c200', b'--', base, pack, idx],
verb_floor=2, cwd=tmpdir)
```
with something like
```
rc = par2(b'create', [b'-n1', b'-c200', b'--', base,
join(b'..', pack), join(b'..', idx)],
verb_floor=2, cwd=tmpdir)
```
to avoid the issue altogether?


Rob Browning

unread,
Sep 21, 2025, 1:52:17 PM9/21/25
to Aaron M. Ucko, bup-...@googlegroups.com
"'Aaron M. Ucko' via bup-list" <bup-...@googlegroups.com> writes:

> Sounds good to me FWIW, but I'd suggest covering additional error codes;
> the OP apparently encountered ERANGE (!)

Oh, right. Forgot about that.

> and AFS meanwhile disallows cross-directory links even within a volume
> (presumably because its ACLs have directory granularity), albeit with
> implementation-dependent error codes: classic OpenAFS reports EXDEV
> and Linux's kAFS reports EREMOTEIO. One can also plausibly make a
> case for EMLINK.

Interesting. I suppose it's likely fine to be fairly permissive here.

Rob Browning

unread,
Sep 21, 2025, 1:59:22 PM9/21/25
to Daniel Distler, Greg Troxel, bup-...@googlegroups.com
Daniel Distler <daniel.dist...@posteo.de> writes:

> But why does bup need the link / copy? Would it work to replace
> ```
> rc = par2(b'create', [b'-n1', b'-c200', b'--', base, pack, idx],
> verb_floor=2, cwd=tmpdir)
> ```
> with something like
> ```
> rc = par2(b'create', [b'-n1', b'-c200', b'--', base,
> join(b'..', pack), join(b'..', idx)],
> verb_floor=2, cwd=tmpdir)
> ```
> to avoid the issue altogether?

Unfortunately par2 isn't exactly designed for our purposes in a number
of ways, and here:

$ par2 create -n1 -c200 -- PAR ../README.md ../DESIGN.md
Ignoring out of basepath source file: /home/rlb/src/bup/main/README.md
Ignoring out of basepath source file: /home/rlb/src/bup/main/HACKING.md
You must specify a list of files when creating.

Tomas Hnyk

unread,
Jan 17, 2026, 7:14:21 PMJan 17
to bup-list
Hello all,

On Saturday, September 20, 2025 at 9:44:47 AM UTC+2 Daniel Distler wrote:
Hi bup-list,

as of bup 0.33.8 fsck uses hard links (see https://github.com/bup/bup/commit/
9c43667f206bd34fde6c57df89c17eb40bf210af). Unfortunately hard links are not
supported on many filesystems, including e.g. FUSE-based filesystems (e.g.,
CryFS) and older formats like FAT and exFAT.

When bup is run on these filesystems, the `os.link` operation fails. This
results in cryptic errors like `OSError: [Errno 34] Numerical result out of
range`, which is not intuitive for the user.

Am I running into this bug when I try to back up to an exFat external harddrive? When I did the backup to my second internal btrfs drive, it completed succesfully. I am using KDE's kup frontend, but reading the logs it seems the problem is with kup. Trying to use the exfat drive, I get this error message:

bup "-d" "/run/media/drew/T5EVO/Zaloha/KDE_Backup-Kup" "fsck" "-g" "-j" "4"
exception: PermissionError(1, 'Operation not permitted')
exception: PermissionError(1, 'Operation not permitted')
exception: PermissionError(1, 'Operation not permitted')
[repeats 300 times]
exception: PermissionError(1, 'Operation not permitted')

Exit code: 99
Kup did not successfully complete the bup backup job: failed to generate recovery info.
 
I use exfat on that harddrive so that it is interoperable with other people's computers, so using a different filesystem is not really feasible. I will patiently wait for a fix if this is indeed this problem, in the meantime I guess I can backup to the other internal btrfs harddrive and just copy a tar archive of bup's backup directory to my exfat file, but obviously, that is a bit annoying :-).
Kind regards
Tomas
 

Rob Browning

unread,
Jan 18, 2026, 5:02:24 PM (14 days ago) Jan 18
to Tomas Hnyk, bup-list
Tomas Hnyk <toma...@gmail.com> writes:

> as of bup 0.33.8 fsck uses hard links (see
> https://github.com/bup/bup/commit/
> 9c43667f206bd34fde6c57df89c17eb40bf210af).

Right, because par2 1.0 broke the use of symlinks. I suppose we could
try to copy the files if hardlinking fails. That would mean copying up
to about a gigabyte (by default) of data into the temporary directory.

It looked like you mentioned both EPERM and ERANGE. I don't see ERANGE
mentioned in at least Linux's link(2). Was that during fsck, or
elsewhere?

Thanks

Rob Browning

unread,
Jan 18, 2026, 5:36:54 PM (14 days ago) Jan 18
to Tomas Hnyk, bup-list
Rob Browning <r...@defaultvalue.org> writes:

> Right, because par2 1.0 broke the use of symlinks. I suppose we could
> try to copy the files if hardlinking fails. That would mean copying up
> to about a gigabyte (by default) of data into the temporary directory.

Oh, right, this is already the case in main, just not in 0.33.x. I
might take a look at backporting the fallback, but it should be "fixed"
by 0.34, regardless.

> It looked like you mentioned both EPERM and ERANGE. I don't see ERANGE
> mentioned in at least Linux's link(2). Was that during fsck, or
> elsewhere?

Nevermind --- already addressed this in the changes in main.

Rob Browning

unread,
Jan 18, 2026, 6:08:32 PM (14 days ago) Jan 18
to bup-...@googlegroups.com
If the platform/filesystem doesn't appear to support hard links, fall
back to copy2(). See also:

commit daa6bf3eeabf1f50630b17b0cbbbdcbbef59c90e
fsck: switch from symlinks to hard links for par2 sandbox

Thanks to Daniel Distler for reporting the issue and Greg Troxel,
and Aaron M. Ucko for help with the errno values.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
(adapted from commit 337173a9f20fd9aa94c0da348e43d671fecc3d79)
---

Proposed for 0.33.x.

lib/bup/cmd/fsck.py | 27 ++++++++++++++++++++++++---
note/0.33.x.md | 15 +++++++++++++++
2 files changed, 39 insertions(+), 3 deletions(-)
create mode 100644 note/0.33.x.md

diff --git a/lib/bup/cmd/fsck.py b/lib/bup/cmd/fsck.py
index abdcbe2d..9d5cd937 100644
--- a/lib/bup/cmd/fsck.py
+++ b/lib/bup/cmd/fsck.py
@@ -4,7 +4,8 @@ from subprocess import PIPE
from tempfile import mkdtemp
from binascii import hexlify
from os.path import join
-import glob, os, subprocess, sys
+from shutil import copy2
+import errno, glob, os, subprocess, sys

from bup import options, git
from bup.compat import argv_bytes
@@ -86,6 +87,16 @@ def par2(action, args, verb_floor=0, cwd=None):
cmd.extend(args)
return run(cmd, cwd=cwd)

+_unable_to_link = set()
+_unable_to_link.add(getattr(errno, 'EMLINK', None))
+_unable_to_link.add(getattr(errno, 'EOPNOTSUPP', None)) # freebsd
+_unable_to_link.add(getattr(errno, 'EPERM', None)) # linux
+_unable_to_link.add(getattr(errno, 'ERANGE', None)) # cryfs
+_unable_to_link.add(getattr(errno, 'EREMOTEIO', None)) # kafs (cross-directory)
+_unable_to_link.add(getattr(errno, 'EXDEV', None)) # openafs (cross-directory)
+_unable_to_link.discard(None)
+_unable_to_link = frozenset(_unable_to_link)
+
def par2_generate(stem):
parent, base = os.path.split(stem)
# Work in a temp_dir because par2 was observed creating empty
@@ -94,8 +105,18 @@ def par2_generate(stem):
with temp_dir(dir=parent, prefix=(base + b'-bup-tmp-')) as tmpdir:
idx = base + b'.idx'
pack = base + b'.pack'
- os.link(join(tmpdir, b'..', idx), join(tmpdir, idx))
- os.link(join(tmpdir, b'..', pack), join(tmpdir, pack))
+ for entry in idx, pack:
+ entry_src = join(tmpdir, b'..', entry)
+ entry_dst = join(tmpdir, entry)
+ copy_instead = False
+ try:
+ os.link(entry_src, entry_dst)
+ except OSError as ex:
+ if not ex.errno in _unable_to_link:
+ raise
+ copy_instead = True
+ if copy_instead:
+ copy2(entry_src, entry_dst)
rc = par2(b'create', [b'-n1', b'-c200', b'--', base, pack, idx],
verb_floor=2, cwd=tmpdir)
if rc == 0:
diff --git a/note/0.33.x.md b/note/0.33.x.md
new file mode 100644
index 00000000..022b1a67
--- /dev/null
+++ b/note/0.33.x.md
@@ -0,0 +1,15 @@
+Notable changes in 0.33.x (incomplete)
+======================================
+
+Bugs
+----
+
+* As noted in 0.33.8, `bup fsck` switched from symlinks to hardlinks
+ to accommodate an incompatible change in `par2` 1.0's behavior. To
+ allow the use of filesystems without hardlinks, `bup` now copies the
+ input files if hardlinking fails.
+
+Thanks to (at least)
+====================
+
+...
--
2.47.3

Rob Browning

unread,
Jan 18, 2026, 8:12:13 PM (14 days ago) Jan 18
to bup-...@googlegroups.com
Rob Browning <r...@defaultvalue.org> writes:

> Proposed for 0.33.x.

Decided to go ahead; pushed to 0.33.x.

Tomas Hnyk

unread,
Jan 20, 2026, 12:29:34 PM (12 days ago) Jan 20
to bup-list
On Monday, January 19, 2026 at 2:12:13 AM UTC+1 Rob Browning wrote:
Rob Browning <r...@defaultvalue.org> writes:

> Proposed for 0.33.x.

Decided to go ahead; pushed to 0.33.x.
Thank you a lot, cannot wait for a release :-). I am very glad that it is fixed!
Tomas

 
Reply all
Reply to author
Forward
0 new messages