From 461bddb0e8f467cd2d59e4b108947cdfc1ac12db Mon Sep 17 00:00:00 2001 From: Johannes Truschnigg Date: Sun, 19 Apr 2026 13:44:18 +0200 Subject: [PATCH 1/4] mount: Ensure a hard link count > 0 for all files When using `restic mount` to serve a repository via a POSIX host's file system, all files backed up from Windows systems will show up on the mounting host with a (hard)link count of 0. While this is not a problem in general and most programs do not even register this strange value, some others (such as Samba's smbd(8)) will go the extra mile and check a file's stat() results for various properties (such as a positive non-zero link count), and refuse to operate if any of the reported values appear off. Since other inode properties absent from non-POSIX backup sources are also "faked up" (e.g., the inode number) during mount and work fine in general, the chances of this change to be in any way harmful are probably rather slim. --- internal/fuse/file.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/fuse/file.go b/internal/fuse/file.go index 42a83d652..986d8428f 100644 --- a/internal/fuse/file.go +++ b/internal/fuse/file.go @@ -55,7 +55,8 @@ func (f *file) Attr(_ context.Context, a *fuse.Attr) error { a.Size = f.node.Size a.Blocks = (f.node.Size + blockSize - 1) / blockSize a.BlockSize = blockSize - a.Nlink = uint32(f.node.Links) + // present a link count > 0 to keep over-eager stat() .st_nlink checks (e.g., from Samba's smbd) happy + a.Nlink = max(uint32(1), uint32(f.node.Links)) if !f.root.cfg.OwnerIsRoot { a.Uid = f.node.UID From 4f781b69f9afb9c8b2b482294e6e0244633a3734 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 13 May 2026 22:29:34 +0200 Subject: [PATCH 2/4] add hardlink test --- internal/fuse/fuse_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/fuse/fuse_test.go b/internal/fuse/fuse_test.go index c82252458..05dc5f91e 100644 --- a/internal/fuse/fuse_test.go +++ b/internal/fuse/fuse_test.go @@ -5,6 +5,7 @@ package fuse import ( "bytes" "context" + "fmt" "math/rand" "os" "strings" @@ -277,6 +278,28 @@ func TestBlocks(t *testing.T) { } } +// Windows (and other non-POSIX) backups may store a link count of 0; FUSE +// must still report a positive nlink so tools that validate stat() (e.g. +// Samba) accept the file. +func TestFileAttrNlink(t *testing.T) { + root := &Root{} + for _, tc := range []struct { + links uint64 + want uint32 + }{ + {0, 1}, + {1, 1}, + {42, 42}, + } { + t.Run(fmt.Sprintf("links_%d", tc.links), func(t *testing.T) { + f := &file{root: root, node: &data.Node{Links: tc.links}} + var a fuse.Attr + rtest.OK(t, f.Attr(context.TODO(), &a)) + rtest.Equals(t, tc.want, a.Nlink) + }) + } +} + func TestInodeFromNode(t *testing.T) { node := &data.Node{Name: "foo.txt", Type: data.NodeTypeCharDev, Links: 2} ino1 := inodeFromNode(1, node) From 65d90641bb626212aa0b05dcab6765d0e2bb42b1 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 13 May 2026 22:44:22 +0200 Subject: [PATCH 3/4] add changelog for windows hardlink count fix --- changelog/unreleased/pull-21784 | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/unreleased/pull-21784 diff --git a/changelog/unreleased/pull-21784 b/changelog/unreleased/pull-21784 new file mode 100644 index 000000000..012505234 --- /dev/null +++ b/changelog/unreleased/pull-21784 @@ -0,0 +1,9 @@ +Bugfix: Support exporting a `restic mount` of a Windows system via Samba + +A repository mounted using `restic mount` on a POSIX system, could not use +Samba to export files from restic backups of Windows systems. Backups of +other systems were not affected. This has been fixed. + +https://github.com/restic/restic/pull/21784 +https://github.com/restic/restic/issues/2034 +https://github.com/restic/restic/issues/4382 From 4547fd7b18a8cbf474c1a48fc8b334411ed42b7e Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 14 May 2026 10:53:42 +0200 Subject: [PATCH 4/4] fuse: tweak comment --- internal/fuse/file.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/fuse/file.go b/internal/fuse/file.go index 986d8428f..6fd042f81 100644 --- a/internal/fuse/file.go +++ b/internal/fuse/file.go @@ -55,7 +55,9 @@ func (f *file) Attr(_ context.Context, a *fuse.Attr) error { a.Size = f.node.Size a.Blocks = (f.node.Size + blockSize - 1) / blockSize a.BlockSize = blockSize - // present a link count > 0 to keep over-eager stat() .st_nlink checks (e.g., from Samba's smbd) happy + // Windows (and other non-POSIX) backups may store a link count of 0. + // FUSE must still report a positive nlink so tools that validate stat() + // (e.g. Samba) accept the file. a.Nlink = max(uint32(1), uint32(f.node.Links)) if !f.root.cfg.OwnerIsRoot {