diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -1320,9 +1320,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; @@ -1375,9 +1381,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'; @@ -1396,11 +1401,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 require + * 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; diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc --- a/tests/sys/fs/fusefs/lookup.cc +++ b/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"; @@ -398,4 +494,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); +}