Index: sys/fs/fuse/fuse_vnops.c =================================================================== --- sys/fs/fuse/fuse_vnops.c +++ sys/fs/fuse/fuse_vnops.c @@ -1252,9 +1252,15 @@ else if ((err = fuse_internal_access(dvp, VEXEC, td, cred))) return err; - if (flags & ISDOTDOT) { - KASSERT(VTOFUD(dvp)->flag & FN_PARENT_NID, - ("Looking up .. is TODO")); + if ((flags & ISDOTDOT) && !(data->dataflags & FSESS_EXPORT_SUPPORT)) + { + if (!(VTOFUD(dvp)->flag & FN_PARENT_NID)) { + /* + * Since the file system doesn't support ".." lookups, + * we have no way to find this entry. + */ + return ESTALE; + } nid = VTOFUD(dvp)->parent_nid; if (nid == 0) return ENOENT; @@ -1307,9 +1313,8 @@ return err; } - nid = VTOI(dvp); fdisp_init(&fdi, cnp->cn_namelen + 1); - fdisp_make(&fdi, FUSE_LOOKUP, mp, nid, td, cred); + fdisp_make(&fdi, FUSE_LOOKUP, mp, VTOI(dvp), td, cred); memcpy(fdi.indata, cnp->cn_nameptr, cnp->cn_namelen); ((char *)fdi.indata)[cnp->cn_namelen] = '\0'; @@ -1328,11 +1333,16 @@ lookup_err = ENOENT; if (cnp->cn_flags & MAKEENTRY) { fuse_validity_2_timespec(feo, &timeout); + /* Use the same entry_time for .. as for + * the file itself. That doesn't honor + * exactly what the fuse server tells + * us, but to do otherwise would requir + * another cache lookup at this point. + */ + struct timespec *dtsp = NULL; cache_enter_time(dvp, *vpp, cnp, - &timeout, NULL); + &timeout, dtsp); } - } else if (nid == FUSE_ROOT_ID) { - lookup_err = EINVAL; } vtyp = IFTOVT(feo->attr.mode); filesize = feo->attr.size; Index: tests/sys/fs/fusefs/forget.cc =================================================================== --- tests/sys/fs/fusefs/forget.cc +++ tests/sys/fs/fusefs/forget.cc @@ -44,18 +44,12 @@ using namespace testing; -const char reclaim_mib[] = "debug.try_reclaim_vnode"; - class Forget: public FuseTest { public: void SetUp() { if (geteuid() != 0) GTEST_SKIP() << "Only root may use " << reclaim_mib; - if (-1 == sysctlbyname(reclaim_mib, NULL, 0, NULL, 0) && - errno == ENOENT) - GTEST_SKIP() << reclaim_mib << " is not available"; - FuseTest::SetUp(); } @@ -71,7 +65,6 @@ uint64_t ino = 42; mode_t mode = S_IFREG | 0755; sem_t sem; - int err; ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); @@ -94,8 +87,7 @@ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); - err = sysctlbyname(reclaim_mib, NULL, 0, FULLPATH, sizeof(FULLPATH)); - ASSERT_EQ(0, err) << strerror(errno); + reclaim_vnode(FULLPATH); sem_wait(&sem); sem_destroy(&sem); @@ -113,7 +105,6 @@ const char FNAME[] = "some_file.txt"; uint64_t dir_ino = 42; uint64_t file_ino = 43; - int err; EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME) .Times(2) @@ -149,8 +140,7 @@ ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno); /* Reclaim the directory, invalidating its children from namecache */ - err = sysctlbyname(reclaim_mib, NULL, 0, FULLDPATH, sizeof(FULLDPATH)); - ASSERT_EQ(0, err) << strerror(errno); + reclaim_vnode(FULLDPATH); /* Access the file again, causing another lookup */ ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno); Index: tests/sys/fs/fusefs/lookup.cc =================================================================== --- tests/sys/fs/fusefs/lookup.cc +++ tests/sys/fs/fusefs/lookup.cc @@ -31,6 +31,10 @@ */ extern "C" { +#include +#include + +#include #include } @@ -40,6 +44,7 @@ using namespace testing; class Lookup: public FuseTest {}; + class Lookup_7_8: public Lookup { public: virtual void SetUp() { @@ -48,6 +53,14 @@ } }; +class LookupExportable: public Lookup { +public: +virtual void SetUp() { + m_init_flags = FUSE_EXPORT_SUPPORT; + Lookup::SetUp(); +} +}; + /* * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs * should use the cached attributes, rather than query the daemon @@ -181,6 +194,89 @@ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); } +/* + * Lookup ".." when that vnode's entry cache has timed out, but its child's + * hasn't. Since this file system doesn't set FUSE_EXPORT_SUPPORT, we have no + * choice but to use the cached entry, even though it expired. + */ +TEST_F(Lookup, dotdot_entry_cache_timeout) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; // immediate timeout + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_opendir(bar_ino); + + int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} + +/* + * Lookup ".." for a vnode with no valid parent nid + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 + * Since the file system is not exportable, we have no choice but to return an + * error. + */ +TEST_F(Lookup, dotdot_no_parent_nid) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + }))); + expect_forget(FUSE_ROOT_ID, 1, NULL); + expect_forget(foo_ino, 1, NULL); + + fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + // Try (and fail) to unmount the file system, to reclaim the mountpoint + // and foo vnodes. + ASSERT_NE(0, unmount("mountpoint", 0)); + EXPECT_EQ(EBUSY, errno); + nap(); // Because vnode reclamation is asynchronous + EXPECT_NE(0, faccessat(fd, "../..", F_OK, 0)); + EXPECT_EQ(ESTALE, errno); +} + TEST_F(Lookup, enoent) { const char FULLPATH[] = "mountpoint/does_not_exist"; @@ -380,4 +476,111 @@ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); } +/* + * Lookup ".." when that vnode's entry cache has timed out, but its child's + * hasn't. + */ +TEST_F(LookupExportable, dotdot_entry_cache_timeout) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; // immediate timeout + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_opendir(bar_ino); + EXPECT_LOOKUP(foo_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + /* FreeBSD's fusefs driver always uses the same cache expiration time + * for ".." as for the directory itself. So we need to look up two + * levels to find an expired ".." cache entry. + */ + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} + +/* + * Lookup ".." for a vnode with no valid parent nid + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 + * Since the file system is exportable, we should resolve the problem by + * sending a FUSE_LOOKUP for "..". + */ +TEST_F(LookupExportable, dotdot_no_parent_nid) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + }))); + expect_forget(FUSE_ROOT_ID, 1, NULL); + expect_forget(foo_ino, 1, NULL); + EXPECT_LOOKUP(bar_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + // Try (and fail) to unmount the file system, to reclaim the mountpoint + // and foo vnodes. + ASSERT_NE(0, unmount("mountpoint", 0)); + EXPECT_EQ(EBUSY, errno); + nap(); // Because vnode reclamation is asynchronous + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} Index: tests/sys/fs/fusefs/utils.hh =================================================================== --- tests/sys/fs/fusefs/utils.hh +++ tests/sys/fs/fusefs/utils.hh @@ -73,6 +73,7 @@ unsigned m_time_gran; MockFS *m_mock = NULL; const static uint64_t FH = 0xdeadbeef1a7ebabe; + const char *reclaim_mib = "debug.try_reclaim_vnode"; public: int m_maxbcachebuf; @@ -255,4 +256,7 @@ * See comments for FuseTest::leak */ static void leakdir(DIR* dirp __unused) {} + + /* Manually reclaim a vnode. Requires root privileges. */ + void reclaim_vnode(const char *fullpath); }; Index: tests/sys/fs/fusefs/utils.cc =================================================================== --- tests/sys/fs/fusefs/utils.cc +++ tests/sys/fs/fusefs/utils.cc @@ -623,6 +623,15 @@ return; } +void +FuseTest::reclaim_vnode(const char *path) +{ + int err; + + err = sysctlbyname(reclaim_mib, NULL, 0, path, strlen(path) + 1); + ASSERT_EQ(0, err) << strerror(errno); +} + static void usage(char* progname) { fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname); exit(2);