From 2a6591d1cdbd437dfc62a6ad01248e4d0f33b2cb Mon Sep 17 00:00:00 2001 From: Sandy Luppino Date: Fri, 26 Jun 2026 12:05:28 -0400 Subject: [PATCH] cmd/mount2: fix NFS directory listings by supporting non-zero Seekdir offsets Seekdir handled only a rewind to offset 0 and returned ENOTSUP otherwise. The stateless kernel NFS server opens a fresh directory handle and seeks to the last returned cookie on every readdir continuation, so any listing spanning more than one readdir batch failed over NFS. dirStream is a snapshot taken at Readdir time and go-fuse assigns each entry a sequential offset, so seeking to off positions the stream at index off; off == 0 still resets to the start, preserving the rewind/re-read behaviour. Before: ls of a directory that spans more than one readdir batch failed over NFS with "Unknown error 524". After: it lists correctly. Adds TestDirStreamSeekdir covering rewind, mid-stream resume and the EOF clamp. The full NFS path was validated against a real Linux nfs-kernel-server export over NFSv3, NFSv4.0 and NFSv4.2. Fixes #9547 (cherry picked from commit 22aba810748fe0200becb72ab817f93953601e37) --- cmd/mount2/node.go | 24 +++++++++++++--------- cmd/mount2/node_test.go | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 cmd/mount2/node_test.go diff --git a/cmd/mount2/node.go b/cmd/mount2/node.go index 464ed8aaa..705a14f38 100644 --- a/cmd/mount2/node.go +++ b/cmd/mount2/node.go @@ -271,16 +271,22 @@ func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) { func (ds *dirStream) Close() { } -// Seekdir implements fusefs.FileSeekdirer so go-fuse can rewind the directory -// stream when the kernel calls lseek(fd, 0, SEEK_SET) before a second getdents. -// Without this, go-fuse returns ENOTSUP and ls returns empty on every call after -// the first. See: https://github.com/hanwen/go-fuse/issues/549 +// Seekdir implements fusefs.FileSeekdirer so go-fuse can reposition the +// directory stream. The kernel calls this both to rewind (lseek(fd, 0, +// SEEK_SET) before a second getdents) and, for a kernel-NFS-exported mount, +// to resume from a previously returned directory cookie: nfsd is stateless, +// so it opens a fresh handle and seeks to the last offset on every readdir +// continuation. Handling only offset 0 made go-fuse return ENOTSUP for those +// resumes, so any listing spanning more than one readdir batch failed over NFS. +// +// dirStream is a snapshot taken at Readdir time and go-fuse assigns each entry +// a sequential offset in Next() order (the entry yielded at index i carries +// offset i+1), so repositioning to off means the next entry is the one at +// index off. Values past the end clamp to EOF via HasNext. +// See: https://github.com/hanwen/go-fuse/issues/549 func (ds *dirStream) Seekdir(_ context.Context, off uint64) syscall.Errno { - if off == 0 { - ds.i = 0 - return 0 - } - return syscall.ENOTSUP + ds.i = int(off) + return 0 } var _ fusefs.DirStream = (*dirStream)(nil) diff --git a/cmd/mount2/node_test.go b/cmd/mount2/node_test.go new file mode 100644 index 000000000..a02c806de --- /dev/null +++ b/cmd/mount2/node_test.go @@ -0,0 +1,45 @@ +//go:build linux || (darwin && amd64) + +package mount2 + +import ( + "context" + "os" + "testing" +) + +// TestDirStreamSeekdir checks that Seekdir repositions the snapshot directory +// stream to an arbitrary offset. A kernel-NFS-exported mount needs this for +// readdir continuation: the stateless server opens a fresh handle and seeks to +// the last returned cookie on every batch. Offset 0 must still reset to the +// start (the rewind / re-read case) and offsets at or past the end must clamp +// to EOF. +func TestDirStreamSeekdir(t *testing.T) { + ctx := context.Background() + // 3 real entries plus the synthesized "." and ".." => 5 entries total, + // occupying internal indices 0..4 (go-fuse offsets 1..5). HasNext is true + // while i < len(nodes)+2, i.e. i < 5. + ds := &dirStream{nodes: make([]os.FileInfo, 3)} + + for _, tc := range []struct { + off uint64 + wantI int + wantNext bool + }{ + {0, 0, true}, // rewind to start (the re-read case) + {2, 2, true}, // resume at the first real entry + {4, 4, true}, // last entry + {5, 5, false}, // exactly at end => EOF + {99, 99, false}, // past end => clamped to EOF via HasNext + } { + if errno := ds.Seekdir(ctx, tc.off); errno != 0 { + t.Fatalf("Seekdir(%d) returned errno %v, want 0", tc.off, errno) + } + if ds.i != tc.wantI { + t.Errorf("Seekdir(%d): i = %d, want %d", tc.off, ds.i, tc.wantI) + } + if got := ds.HasNext(); got != tc.wantNext { + t.Errorf("Seekdir(%d): HasNext() = %v, want %v", tc.off, got, tc.wantNext) + } + } +}