From 40b3d32fe57555d4e425ad34f122bd7677f0e161 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 1 Sep 2026 14:13:16 -0700 Subject: [PATCH] test: metadata operations on unlinked open files and removed open directories (#11075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The POSIX suites skirt this: pjdfstest's unlink/14.t covers only fstat and pread on an unlinked descriptor — its driver has no fchmod at all and never opens a directory. Pin the full rule in the FUSE integration suite: ftruncate, fchmod, futimes, fstat, and the f*xattr calls keep working between the removal of the last name and the final close, for a file after unlink and a directory after rmdir, with nlink 0 and the changes visible to a following fstat. Claude-Session: https://claude.ai/code/session_01GYqLENjZzbV5hgt4L8cSAK --- test/fuse_integration/go.mod | 5 +- test/fuse_integration/go.sum | 2 + test/fuse_integration/unlinked_open_test.go | 94 +++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 test/fuse_integration/unlinked_open_test.go diff --git a/test/fuse_integration/go.mod b/test/fuse_integration/go.mod index 47246cdd8..bb1f5aa09 100644 --- a/test/fuse_integration/go.mod +++ b/test/fuse_integration/go.mod @@ -2,7 +2,10 @@ module seaweedfs-fuse-tests go 1.21 -require github.com/stretchr/testify v1.8.4 +require ( + github.com/stretchr/testify v1.8.4 + golang.org/x/sys v0.28.0 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/test/fuse_integration/go.sum b/test/fuse_integration/go.sum index fa4b6e682..21c915f83 100644 --- a/test/fuse_integration/go.sum +++ b/test/fuse_integration/go.sum @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/test/fuse_integration/unlinked_open_test.go b/test/fuse_integration/unlinked_open_test.go new file mode 100644 index 000000000..72efa0306 --- /dev/null +++ b/test/fuse_integration/unlinked_open_test.go @@ -0,0 +1,94 @@ +//go:build linux + +package fuse_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +// TestMetadataOnUnlinkedOpen covers the POSIX rule that an inode outlives its +// last name while a descriptor holds it open: metadata operations through the +// descriptor keep working between the removal and the final close, for a file +// after unlink and for a directory after rmdir. +func TestMetadataOnUnlinkedOpen(t *testing.T) { + framework := NewFuseTestFramework(t, DefaultTestConfig()) + defer framework.Cleanup() + + require.NoError(t, framework.Setup(DefaultTestConfig())) + + t.Run("UnlinkedOpenFile", func(t *testing.T) { + testUnlinkedOpenFile(t, framework) + }) + + t.Run("RemovedOpenDirectory", func(t *testing.T) { + testRemovedOpenDirectory(t, framework) + }) +} + +func testUnlinkedOpenFile(t *testing.T, framework *FuseTestFramework) { + path := filepath.Join(framework.GetMountPoint(), "unlinked_open_file") + fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDWR, 0644) + require.NoError(t, err) + defer unix.Close(fd) + _, err = unix.Write(fd, []byte("hello")) + require.NoError(t, err) + + require.NoError(t, os.Remove(path)) + + require.NoError(t, unix.Ftruncate(fd, 2), "ftruncate on an unlinked open file") + require.NoError(t, unix.Fchmod(fd, 0600), "fchmod on an unlinked open file") + require.NoError(t, unix.Futimes(fd, []unix.Timeval{{Sec: 1234567890}, {Sec: 987654321}}), + "futimes on an unlinked open file") + + var st unix.Stat_t + require.NoError(t, unix.Fstat(fd, &st), "fstat on an unlinked open file") + assert.EqualValues(t, 0, st.Nlink, "nlink after unlink") + assert.EqualValues(t, 0600, st.Mode&0777, "mode after fchmod") + assert.EqualValues(t, 2, st.Size, "size after ftruncate") + assert.EqualValues(t, 1234567890, st.Atim.Sec, "atime after futimes") + assert.EqualValues(t, 987654321, st.Mtim.Sec, "mtime after futimes") + + testFdXattrRoundTrip(t, fd) +} + +func testRemovedOpenDirectory(t *testing.T, framework *FuseTestFramework) { + path := filepath.Join(framework.GetMountPoint(), "removed_open_dir") + require.NoError(t, os.Mkdir(path, 0755)) + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY, 0) + require.NoError(t, err) + defer unix.Close(fd) + + require.NoError(t, os.Remove(path)) + + require.NoError(t, unix.Fchmod(fd, 0770), "fchmod on a removed open directory") + require.NoError(t, unix.Futimes(fd, []unix.Timeval{{Sec: 1234567890}, {Sec: 987654321}}), + "futimes on a removed open directory") + + var st unix.Stat_t + require.NoError(t, unix.Fstat(fd, &st), "fstat on a removed open directory") + assert.EqualValues(t, unix.S_IFDIR, st.Mode&unix.S_IFMT, "type after rmdir") + assert.EqualValues(t, 0770, st.Mode&0777, "mode after fchmod") + assert.EqualValues(t, 0, st.Nlink, "nlink after rmdir") + assert.EqualValues(t, 987654321, st.Mtim.Sec, "mtime after futimes") + + testFdXattrRoundTrip(t, fd) +} + +func testFdXattrRoundTrip(t *testing.T, fd int) { + require.NoError(t, unix.Fsetxattr(fd, "user.k", []byte("v"), 0), "fsetxattr on a removed open inode") + + buf := make([]byte, 8) + n, err := unix.Fgetxattr(fd, "user.k", buf) + require.NoError(t, err, "fgetxattr on a removed open inode") + assert.Equal(t, "v", string(buf[:n])) + + require.NoError(t, unix.Fremovexattr(fd, "user.k"), "fremovexattr on a removed open inode") + _, err = unix.Fgetxattr(fd, "user.k", buf) + assert.ErrorIs(t, err, unix.ENODATA, "fgetxattr after fremovexattr") +}