diff options
| author | Alan Somers <asomers@FreeBSD.org> | 2026-01-23 21:23:51 +0000 |
|---|---|---|
| committer | Alan Somers <asomers@FreeBSD.org> | 2026-03-12 16:11:25 +0000 |
| commit | 7e68af7ce2c1b892954df415774fe59fd2f1b62f (patch) | |
| tree | 50ff32cd74613ce3fd69ba4048fab6681d313be3 /tests | |
| parent | ce9aff829e02c9a21c04eae77a45f2193d1ed5a1 (diff) | |
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/sys/fs/fusefs/bmap.cc | 34 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/notify.cc | 38 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/read.cc | 192 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/rename.cc | 90 |
4 files changed, 332 insertions, 22 deletions
diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc index e61dadb6d79e..622a3c3debcc 100644 --- a/tests/sys/fs/fusefs/bmap.cc +++ b/tests/sys/fs/fusefs/bmap.cc @@ -44,10 +44,13 @@ using namespace testing; const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; -class Bmap: public FuseTest { +class Bmap: public FuseTest, + public WithParamInterface<tuple<int, int>> +{ public: virtual void SetUp() { m_maxreadahead = UINT32_MAX; + m_init_flags |= get<0>(GetParam()); FuseTest::SetUp(); } void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn) @@ -73,12 +76,12 @@ void expect_lookup(const char *relpath, uint64_t ino, off_t size) } }; -class BmapEof: public Bmap, public WithParamInterface<int> {}; +class BmapEof: public Bmap {}; /* * Test FUSE_BMAP */ -TEST_F(Bmap, bmap) +TEST_P(Bmap, bmap) { struct fiobmap2_arg arg; /* @@ -124,7 +127,7 @@ TEST_F(Bmap, bmap) * If the daemon does not implement VOP_BMAP, fusefs should return sensible * defaults. */ -TEST_F(Bmap, default_) +TEST_P(Bmap, default_) { struct fiobmap2_arg arg; const off_t filesize = 1 << 30; @@ -181,7 +184,7 @@ TEST_F(Bmap, default_) * The server returns an error for some reason for FUSE_BMAP. fusefs should * faithfully report that error up to the caller. */ -TEST_F(Bmap, einval) +TEST_P(Bmap, einval) { struct fiobmap2_arg arg; const off_t filesize = 1 << 30; @@ -217,7 +220,7 @@ TEST_F(Bmap, einval) * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=264196 . The bug did not * lie in fusefs, but this is a convenient place for a regression test. */ -TEST_F(Bmap, spurious_einval) +TEST_P(Bmap, spurious_einval) { const off_t filesize = 4ull << 30; const ino_t ino = 42; @@ -288,7 +291,7 @@ TEST_P(BmapEof, eof) int fd; int ngetattrs; - ngetattrs = GetParam(); + ngetattrs = get<1>(GetParam()); FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0); expect_open(ino, 0, 1); // Depending on ngetattrs, FUSE_READ could be called with either @@ -348,6 +351,17 @@ TEST_P(BmapEof, eof) leak(fd); } -INSTANTIATE_TEST_SUITE_P(BE, BmapEof, - Values(1, 2, 3) -); +/* + * Try with and without async reads, because it affects the type of vnode lock + * on entry to fuse_vnop_bmap. + */ +INSTANTIATE_TEST_SUITE_P(B, Bmap, Values( + tuple(0, 0), + tuple(FUSE_ASYNC_READ, 0) +)); + +INSTANTIATE_TEST_SUITE_P(BE, BmapEof, Values( + tuple(0, 1), + tuple(0, 2), + tuple(0, 3) +)); diff --git a/tests/sys/fs/fusefs/notify.cc b/tests/sys/fs/fusefs/notify.cc index d370a1e6e706..69742fb2a54b 100644 --- a/tests/sys/fs/fusefs/notify.cc +++ b/tests/sys/fs/fusefs/notify.cc @@ -47,8 +47,15 @@ using namespace testing; * invalidation. This file tests our client's handling of those messages. */ -class Notify: public FuseTest { +class Notify: public FuseTest, + public WithParamInterface<int> +{ public: +virtual void SetUp() { + m_init_flags |= GetParam(); + FuseTest::SetUp(); +} + /* Ignore an optional FUSE_FSYNC */ void maybe_expect_fsync(uint64_t ino) { @@ -154,7 +161,7 @@ static void* store(void* arg) { } /* Invalidate a nonexistent entry */ -TEST_F(Notify, inval_entry_nonexistent) +TEST_P(Notify, inval_entry_nonexistent) { const static char *name = "foo"; struct inval_entry_args iea; @@ -173,7 +180,7 @@ TEST_F(Notify, inval_entry_nonexistent) } /* Invalidate a cached entry */ -TEST_F(Notify, inval_entry) +TEST_P(Notify, inval_entry) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -211,7 +218,7 @@ TEST_F(Notify, inval_entry) * Invalidate a cached entry beneath the root, which uses a slightly different * code path. */ -TEST_F(Notify, inval_entry_below_root) +TEST_P(Notify, inval_entry_below_root) { const static char FULLPATH[] = "mountpoint/some_dir/foo"; const static char DNAME[] = "some_dir"; @@ -258,7 +265,7 @@ TEST_F(Notify, inval_entry_below_root) } /* Invalidating an entry invalidates the parent directory's attributes */ -TEST_F(Notify, inval_entry_invalidates_parent_attrs) +TEST_P(Notify, inval_entry_invalidates_parent_attrs) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -302,7 +309,7 @@ TEST_F(Notify, inval_entry_invalidates_parent_attrs) } -TEST_F(Notify, inval_inode_nonexistent) +TEST_P(Notify, inval_inode_nonexistent) { struct inval_inode_args iia; ino_t ino = 42; @@ -320,7 +327,7 @@ TEST_F(Notify, inval_inode_nonexistent) EXPECT_EQ(0, (intptr_t)thr0_value); } -TEST_F(Notify, inval_inode_with_clean_cache) +TEST_P(Notify, inval_inode_with_clean_cache) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -390,7 +397,7 @@ TEST_F(Notify, inval_inode_with_clean_cache) * nothing bad should happen. * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519 */ -TEST_F(Notify, notify_after_unmount) +TEST_P(Notify, notify_after_unmount) { const static char *name = "foo"; struct inval_entry_args iea; @@ -408,7 +415,7 @@ TEST_F(Notify, notify_after_unmount) /* FUSE_NOTIFY_STORE with a file that's not in the entry cache */ /* disabled because FUSE_NOTIFY_STORE is not yet implemented */ -TEST_F(Notify, DISABLED_store_nonexistent) +TEST_P(Notify, DISABLED_store_nonexistent) { struct store_args sa; ino_t ino = 42; @@ -427,7 +434,7 @@ TEST_F(Notify, DISABLED_store_nonexistent) /* Store data into for a file that does not yet have anything cached */ /* disabled because FUSE_NOTIFY_STORE is not yet implemented */ -TEST_F(Notify, DISABLED_store_with_blank_cache) +TEST_P(Notify, DISABLED_store_with_blank_cache) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -465,7 +472,7 @@ TEST_F(Notify, DISABLED_store_with_blank_cache) leak(fd); } -TEST_F(NotifyWriteback, inval_inode_with_dirty_cache) +TEST_P(NotifyWriteback, inval_inode_with_dirty_cache) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -506,7 +513,7 @@ TEST_F(NotifyWriteback, inval_inode_with_dirty_cache) leak(fd); } -TEST_F(NotifyWriteback, inval_inode_attrs_only) +TEST_P(NotifyWriteback, inval_inode_attrs_only) { const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; @@ -600,3 +607,10 @@ TEST(PreMount, inval_entry_before_mount) EXPECT_EQ(ENODEV, errno); delete out; } + +/* + * Try with and without async reads, because it affects the type of vnode lock + * acquired in fuse_internal_invalidate_entry. + */ +INSTANTIATE_TEST_SUITE_P(N, Notify, Values(0, FUSE_ASYNC_READ)); +INSTANTIATE_TEST_SUITE_P(N, NotifyWriteback, Values(0, FUSE_ASYNC_READ)); diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc index e9c79ba2ffda..52dad3bc829e 100644 --- a/tests/sys/fs/fusefs/read.cc +++ b/tests/sys/fs/fusefs/read.cc @@ -95,6 +95,19 @@ class AsyncRead: public AioRead { } }; +class AsyncReadNoAttrCache: public Read { +virtual void SetUp() { + m_init_flags = FUSE_ASYNC_READ; + Read::SetUp(); +} +public: +void expect_lookup(const char *relpath, uint64_t ino) +{ + // Don't return size, and set attr_valid=0 + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1, 0); +} +}; + class ReadAhead: public Read, public WithParamInterface<tuple<bool, int>> { @@ -351,6 +364,185 @@ TEST_F(AsyncRead, async_read) leak(fd); } +/* + * Regression test for a VFS locking bug: as of + * 22bb70a6b3bb7799276ab480e40665b7d6e4ce25 (17-December-2024), fusefs did not + * obtain an exclusive vnode lock before attempting to clear the attr cache + * after an unexpected eof. The vnode lock would already be exclusive except + * when FUSE_ASYNC_READ is set. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283391 + */ + TEST_F(AsyncRead, eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + uint64_t offset = 100; + ssize_t bufsize = strlen(CONTENTS); + ssize_t partbufsize = 3 * bufsize / 4; + ssize_t r; + uint8_t buf[bufsize]; + struct stat sb; + + expect_lookup(RELPATH, ino, offset + bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, offset + bufsize, offset + partbufsize, CONTENTS); + expect_getattr(ino, offset + partbufsize); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + r = pread(fd, buf, bufsize, offset); + ASSERT_LE(0, r) << strerror(errno); + EXPECT_EQ(partbufsize, r) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)); + EXPECT_EQ((off_t)(offset + partbufsize), sb.st_size); + leak(fd); +} + +/* + * If the daemon disables the attribute cache (or if it has expired), then the + * kernel must fetch attributes during VOP_READ. If async reads are enabled, + * then fuse_internal_cache_attrs will be called without the vnode exclusively + * locked. Regression test for + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=291064 + */ +TEST_F(AsyncReadNoAttrCache, read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = bufsize; + out.body.attr.attr_valid = 0; + }))); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +/* + * If the vnode's attr cache has expired before VOP_READ begins, the kernel + * will have to fetch the file's size from the server. If it has changed since + * our last query, we'll need to update the vnode pager. But we'll only have a + * shared vnode lock. + */ +TEST_F(AsyncReadNoAttrCache, read_sizechange) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + ssize_t size1 = bufsize - 1; + ssize_t size2 = bufsize; + Sequence seq; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = size1; + out.body.attr.attr_valid = 0; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == 0 && + in.body.read.size == size1); + }, Eq(true)), + _) + ).Times(1) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(struct fuse_out_header) + size1; + memmove(out.body.bytes, CONTENTS, size1); + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = size2; + out.body.attr.attr_valid = 0; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == 0 && + in.body.read.size == size2); + }, Eq(true)), + _) + ).Times(1) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(struct fuse_out_header) + size2; + memmove(out.body.bytes, CONTENTS, size2); + }))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(size1, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, size1)); + + /* Read again, but this time the server has changed the file's size */ + bzero(buf, size2); + ASSERT_EQ(size2, pread(fd, buf, bufsize, 0)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, size2)); + + leak(fd); +} + /* The kernel should update the cached atime attribute during a read */ TEST_F(Read, atime) { diff --git a/tests/sys/fs/fusefs/rename.cc b/tests/sys/fs/fusefs/rename.cc index 6b5687e209c6..385f26953aae 100644 --- a/tests/sys/fs/fusefs/rename.cc +++ b/tests/sys/fs/fusefs/rename.cc @@ -163,6 +163,96 @@ TEST_F(Rename, entry_cache_negative_purge) ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno); } +static volatile int stopit = 0; + +static void* setattr_th(void* arg) { + char *path = (char*)arg; + + while (stopit == 0) + chmod(path, 0777); + return 0; +} + +/* + * Rename restarts the syscall to avoid a LOR + * + * This test triggers a race: the chmod() calls VOP_SETATTR, which locks its + * vnode, but fuse_vnop_rename also tries to lock the same vnode. The result + * is that, in order to avoid a LOR, fuse_vnop_rename returns ERELOOKUP to + * restart the syscall. + * + * To verify that the race is hit, watch the fusefs:fusefs:vnops:erelookup + * dtrace probe while the test is running. On my system, that probe fires more + * than 100 times per second during this test. + */ +TEST_F(Rename, erelookup) +{ + const char FULLDST[] = "mountpoint/dstdir/dst"; + const char RELDSTDIR[] = "dstdir"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + pthread_t th0; + uint64_t ino = 42; + uint64_t dst_dir_ino = 43; + uint32_t mode = S_IFDIR | 0644; + struct timespec now, timeout; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.uid = 0; + out.body.entry.attr.gid = 0; + out.body.entry.entry_valid = UINT64_MAX; + })) + ); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = dst_dir_ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.attr.ino = dst_dir_ino; + out.body.entry.attr.uid = geteuid(); + out.body.entry.attr.gid = getegid(); + }))); + EXPECT_LOOKUP(dst_dir_ino, RELDST) + .WillRepeatedly(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(EIO))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RENAME); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(EIO))); + + ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, (void*)FULLSRC)) + << strerror(errno); + + ASSERT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &timeout)); + timeout.tv_nsec += NAP_NS; + do { + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + EXPECT_EQ(EIO, errno); + ASSERT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &now)); + } while (timespeccmp(&now, &timeout, <)); + stopit = 1; + pthread_join(th0, NULL); +} + TEST_F(Rename, exdev) { const char FULLB[] = "mountpoint/src"; |
