mount2: fix empty directory listings on re-read

With cmd/mount2, reading a directory more than once returned the correct
entries on the first read but nothing on subsequent reads. Plain `ls`
triggers this: it does lseek(fd, 0, SEEK_SET) to rewind the directory
before a second getdents.

go-fuse v2.9.0 rewinds a directory stream by calling Seekdir on the
FileSeekdirer interface. dirStream did not implement it, so go-fuse
returned ENOTSUP and produced an empty listing on every read after the
first.

This implements Seekdir on dirStream: a rewind to offset 0 resets the
stream to the start, restoring correct listings on re-read. Non-zero
offsets are uncommon for in-memory listings and still return ENOTSUP,
matching go-fuse's own default. A compile-time interface assertion is
added so signature drift on future go-fuse updates is caught at build
time.

Before: second and subsequent reads of a directory returned no entries.
After: directories list correctly on every read.

See: https://github.com/hanwen/go-fuse/issues/549
Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
This commit is contained in:
Janne Beate Bakeng
2026-06-01 13:06:03 +02:00
committed by GitHub
parent 2dbad62a11
commit 00bd00d83d
4 changed files with 102 additions and 0 deletions

View File

@@ -271,7 +271,20 @@ 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
func (ds *dirStream) Seekdir(_ context.Context, off uint64) syscall.Errno {
if off == 0 {
ds.i = 0
return 0
}
return syscall.ENOTSUP
}
var _ fusefs.DirStream = (*dirStream)(nil)
var _ fusefs.FileSeekdirer = (*dirStream)(nil)
// Readdir opens a stream of directory entries.
//

View File

@@ -0,0 +1,13 @@
//go:build !linux && !darwin && !freebsd
package vfstest
import (
"runtime"
"testing"
)
// TestDirRewind checks that re-reading a rewound directory works
func TestDirRewind(t *testing.T) {
t.Skip("not supported on " + runtime.GOOS)
}

75
vfs/vfstest/dir_unix.go Normal file
View File

@@ -0,0 +1,75 @@
//go:build linux || darwin || freebsd
package vfstest
import (
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// countDirFd reads the directory referred to by fd to end-of-stream using
// raw getdents syscalls and returns the number of entries (excluding "." and
// ".." which ParseDirent skips).
//
// This deliberately uses syscall.ReadDirent rather than (*os.File).Readdir so
// that it exercises the kernel readdir path directly on a single file
// descriptor, the same way an in-process rewinddir does.
func countDirFd(t *testing.T, fd int) int {
buf := make([]byte, 8192)
var names []string
for {
n, err := syscall.ReadDirent(fd, buf)
require.NoError(t, err)
if n <= 0 {
break
}
_, _, names = syscall.ParseDirent(buf[:n], -1, names)
}
return len(names)
}
// TestDirRewind checks that re-reading a directory after rewinding the
// directory stream (lseek(fd, 0, SEEK_SET), as rewinddir(3) does) returns the
// same entries as the first read.
//
// This reproduces the bug where the mount2 backend returned an empty listing
// on every read after the first because go-fuse v2.9.0 rewinds a directory by
// calling Seekdir, which dirStream did not implement. See PR #9469 and
// https://github.com/hanwen/go-fuse/issues/549
func TestDirRewind(t *testing.T) {
run.skipIfVFS(t)
run.skipIfNoFUSE(t)
run.mkdir(t, "dir")
run.createFile(t, "dir/f1", "1")
run.createFile(t, "dir/f2", "2")
run.createFile(t, "dir/f3", "3")
run.checkDir(t, "dir/|dir/f1 1|dir/f2 1|dir/f3 1")
fh, err := run.os.Open(run.path("dir"))
require.NoError(t, err)
defer func() {
_ = fh.Close()
}()
fd := int(fh.Fd())
first := countDirFd(t, fd)
assert.Equal(t, 3, first, "first read should see all entries")
// rewinddir == lseek(fd, 0, SEEK_SET)
_, err = syscall.Seek(fd, 0, 0)
require.NoError(t, err)
second := countDirFd(t, fd)
assert.Equal(t, first, second, "re-read after rewind should match first read")
require.NoError(t, fh.Close())
run.rm(t, "dir/f1")
run.rm(t, "dir/f2")
run.rm(t, "dir/f3")
run.rmdir(t, "dir")
run.checkDir(t, "")
}

View File

@@ -86,6 +86,7 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir)
t.Run("TestDirRenameFullDir", TestDirRenameFullDir)
t.Run("TestDirModTime", TestDirModTime)
t.Run("TestDirRewind", TestDirRewind)
if enableCacheTests {
t.Run("TestDirCacheFlush", TestDirCacheFlush)
}