From e5b50fe736adf65fa6b5c60c1384b3d3e2785db1 Mon Sep 17 00:00:00 2001 From: Alan Somers Date: Thu, 23 May 2019 00:44:01 +0000 Subject: [PATCH] fusefs: Make fuse file systems NFS-exportable This commit adds the VOPs needed by userspace NFS servers (tested with net/unfs3). More work is needed to make the in-kernel nfsd work, because of its stateless nature. It doesn't open files prior to doing I/O. Also, the NFS-related VOPs currently ignore the entry cache. Sponsored by: The FreeBSD Foundation --- sys/fs/fuse/fuse_internal.c | 4 +- sys/fs/fuse/fuse_ipc.h | 1 + sys/fs/fuse/fuse_node.c | 14 +- sys/fs/fuse/fuse_node.h | 24 +++ sys/fs/fuse/fuse_vfsops.c | 108 +++++++++++- sys/fs/fuse/fuse_vnops.c | 52 +++++- tests/sys/fs/fusefs/Makefile | 2 + tests/sys/fs/fusefs/nfs.cc | 307 +++++++++++++++++++++++++++++++++ tests/sys/fs/fusefs/readdir.cc | 50 ------ tests/sys/fs/fusefs/utils.cc | 51 ++++++ tests/sys/fs/fusefs/utils.hh | 8 + 11 files changed, 557 insertions(+), 64 deletions(-) create mode 100644 tests/sys/fs/fusefs/nfs.cc diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c index d86c7c9128a8..1064bd4b35f7 100644 --- a/sys/fs/fuse/fuse_internal.c +++ b/sys/fs/fuse/fuse_internal.c @@ -727,6 +727,8 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio) data->max_write = fiio->max_write; if (fiio->flags & FUSE_POSIX_LOCKS) data->dataflags |= FSESS_POSIX_LOCKS; + if (fiio->flags & FUSE_EXPORT_SUPPORT) + data->dataflags |= FSESS_EXPORT_SUPPORT; } else { err = EINVAL; } @@ -764,7 +766,7 @@ fuse_internal_send_init(struct fuse_data *data, struct thread *td) * the size of a buffer cache block. */ fiii->max_readahead = maxbcachebuf; - fiii->flags = FUSE_POSIX_LOCKS; + fiii->flags = FUSE_EXPORT_SUPPORT | FUSE_POSIX_LOCKS; fuse_insert_callback(fdi.tick, fuse_internal_init_callback); fuse_insert_message(fdi.tick, false); diff --git a/sys/fs/fuse/fuse_ipc.h b/sys/fs/fuse/fuse_ipc.h index 8a55210cfdff..b040b671f59f 100644 --- a/sys/fs/fuse/fuse_ipc.h +++ b/sys/fs/fuse/fuse_ipc.h @@ -222,6 +222,7 @@ struct fuse_data { #define FSESS_NO_NAMECACHE 0x0400 /* disable name cache */ #define FSESS_NO_MMAP 0x0800 /* disable mmap */ #define FSESS_POSIX_LOCKS 0x2000 /* daemon supports POSIX locks */ +#define FSESS_EXPORT_SUPPORT 0x10000 /* daemon supports NFS-style lookups */ #define FSESS_MNTOPTS_MASK ( \ FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \ FSESS_DEFAULT_PERMISSIONS | FSESS_NO_ATTRCACHE | \ diff --git a/sys/fs/fuse/fuse_node.c b/sys/fs/fuse/fuse_node.c index 7c1920df3395..511dec3c2b68 100644 --- a/sys/fs/fuse/fuse_node.c +++ b/sys/fs/fuse/fuse_node.c @@ -77,7 +77,6 @@ __FBSDID("$FreeBSD$"); #include #include #include -#include #include #include #include @@ -165,18 +164,12 @@ fuse_vnode_destroy(struct vnode *vp) atomic_subtract_acq_int(&fuse_node_count, 1); } -static int +int fuse_vnode_cmp(struct vnode *vp, void *nidp) { return (VTOI(vp) != *((uint64_t *)nidp)); } -static uint32_t inline -fuse_vnode_hash(uint64_t id) -{ - return (fnv_32_buf(&id, sizeof(id), FNV1_32_INIT)); -} - SDT_PROBE_DEFINE3(fusefs, , node, stale_vnode, "struct vnode*", "enum vtype", "uint64_t"); static int @@ -215,6 +208,7 @@ fuse_vnode_alloc(struct mount *mp, return (EAGAIN); } MPASS((*vpp)->v_data != NULL); + MPASS(VTOFUD(*vpp)->nid == nodeid); SDT_PROBE2(fusefs, , node, trace, 1, "vnode taken from hash"); return (0); } @@ -269,6 +263,7 @@ fuse_vnode_get(struct mount *mp, enum vtype vtyp) { struct thread *td = (cnp != NULL ? cnp->cn_thread : curthread); + uint64_t generation = feo ? feo->generation : 1; int err = 0; err = fuse_vnode_alloc(mp, td, nodeid, vtyp, vpp); @@ -293,10 +288,11 @@ fuse_vnode_get(struct mount *mp, cache_enter_time(dvp, *vpp, cnp, &timeout, NULL); } + VTOFUD(*vpp)->generation = generation; /* * In userland, libfuse uses cached lookups for dot and dotdot entries, * thus it does not really bump the nlookup counter for forget. - * Follow the same semantic and avoid tu bump it in order to keep + * Follow the same semantic and avoid the bump in order to keep * nlookup counters consistent. */ if (cnp == NULL || ((cnp->cn_flags & ISDOTDOT) == 0 && diff --git a/sys/fs/fuse/fuse_node.h b/sys/fs/fuse/fuse_node.h index fcbb9946efd0..25551c520ab6 100644 --- a/sys/fs/fuse/fuse_node.h +++ b/sys/fs/fuse/fuse_node.h @@ -60,6 +60,7 @@ #ifndef _FUSE_NODE_H_ #define _FUSE_NODE_H_ +#include #include #include @@ -75,10 +76,13 @@ */ #define FN_SIZECHANGE 0x00000100 #define FN_DIRECTIO 0x00000200 +/* Indicates that parent_nid is valid */ +#define FN_PARENT_NID 0x00000400 struct fuse_vnode_data { /** self **/ uint64_t nid; + uint64_t generation; /** parent **/ uint64_t parent_nid; @@ -98,6 +102,17 @@ struct fuse_vnode_data { enum vtype vtype; }; +/* + * This overlays the fid structure (see mount.h). Mostly the same as the types + * used by UFS and ext2. + */ +struct fuse_fid { + uint16_t len; /* Length of structure. */ + uint16_t pad; /* Force 32-bit alignment. */ + uint32_t gen; /* Generation number. */ + uint64_t nid; /* FUSE node id. */ +}; + #define VTOFUD(vp) \ ((struct fuse_vnode_data *)((vp)->v_data)) #define VTOI(vp) (VTOFUD(vp)->nid) @@ -119,6 +134,12 @@ fuse_vnode_clear_attr_cache(struct vnode *vp) bintime_clear(&VTOFUD(vp)->attr_cache_timeout); } +static uint32_t inline +fuse_vnode_hash(uint64_t id) +{ + return (fnv_32_buf(&id, sizeof(id), FNV1_32_INIT)); +} + #define VTOILLU(vp) ((uint64_t)(VTOFUD(vp) ? VTOI(vp) : 0)) #define FUSE_NULL_ID 0 @@ -126,12 +147,15 @@ fuse_vnode_clear_attr_cache(struct vnode *vp) extern struct vop_vector fuse_fifoops; extern struct vop_vector fuse_vnops; +int fuse_vnode_cmp(struct vnode *vp, void *nidp); + static inline void fuse_vnode_setparent(struct vnode *vp, struct vnode *dvp) { if (dvp != NULL && vp->v_type == VDIR) { MPASS(dvp->v_type == VDIR); VTOFUD(vp)->parent_nid = VTOI(dvp); + VTOFUD(vp)->flag |= FN_PARENT_NID; } } diff --git a/sys/fs/fuse/fuse_vfsops.c b/sys/fs/fuse/fuse_vfsops.c index 7a3753c8a6c2..4bd736c1fca6 100644 --- a/sys/fs/fuse/fuse_vfsops.c +++ b/sys/fs/fuse/fuse_vfsops.c @@ -107,16 +107,20 @@ SDT_PROBE_DEFINE2(fusefs, , vfsops, trace, "int", "char*"); #define PRIV_VFS_FUSE_SYNC_UNMOUNT PRIV_VFS_MOUNT_NONUSER #endif +static vfs_fhtovp_t fuse_vfsop_fhtovp; static vfs_mount_t fuse_vfsop_mount; static vfs_unmount_t fuse_vfsop_unmount; static vfs_root_t fuse_vfsop_root; static vfs_statfs_t fuse_vfsop_statfs; +static vfs_vget_t fuse_vfsop_vget; struct vfsops fuse_vfsops = { + .vfs_fhtovp = fuse_vfsop_fhtovp, .vfs_mount = fuse_vfsop_mount, .vfs_unmount = fuse_vfsop_unmount, .vfs_root = fuse_vfsop_root, .vfs_statfs = fuse_vfsop_statfs, + .vfs_vget = fuse_vfsop_vget, }; static int fuse_enforce_dev_perms = 0; @@ -256,6 +260,34 @@ fuse_vfs_remount(struct mount *mp, struct thread *td, uint64_t mntopts, return err; } +static int +fuse_vfsop_fhtovp(struct mount *mp, struct fid *fhp, int flags, + struct vnode **vpp) +{ + struct fuse_fid *ffhp = (struct fuse_fid *)fhp; + struct fuse_vnode_data *fvdat; + struct vnode *nvp; + int error; + + if (!(fuse_get_mpdata(mp)->dataflags & FSESS_EXPORT_SUPPORT)) + return EOPNOTSUPP; + + error = VFS_VGET(mp, ffhp->nid, LK_EXCLUSIVE, &nvp); + if (error) { + *vpp = NULLVP; + return (error); + } + fvdat = VTOFUD(nvp); + if (fvdat->generation != ffhp->gen ) { + vput(nvp); + *vpp = NULLVP; + return (ESTALE); + } + *vpp = nvp; + vnode_create_vobject(*vpp, 0, curthread); + return (0); +} + static int fuse_vfsop_mount(struct mount *mp) { @@ -322,7 +354,6 @@ fuse_vfsop_mount(struct mount *mp) SDT_PROBE1(fusefs, , vfsops, mntopts, mntopts); if (mp->mnt_flag & MNT_UPDATE) { - /*dev_rel(fdev);*/ return fuse_vfs_remount(mp, td, mntopts, max_read, daemon_timeout); } @@ -352,7 +383,8 @@ fuse_vfsop_mount(struct mount *mp) td->td_fpop = fptmp; fdrop(fp, td); FUSE_LOCK(); - if (err != 0 || data == NULL || data->mp != NULL) { + + if (err != 0 || data == NULL) { err = ENXIO; SDT_PROBE4(fusefs, , vfsops, mount_err, "invalid or not opened device", data, mp, err); @@ -480,7 +512,6 @@ fuse_vfsop_unmount(struct mount *mp, int mntflags) MNT_ILOCK(mp); mp->mnt_data = NULL; - mp->mnt_flag &= ~MNT_LOCAL; MNT_IUNLOCK(mp); dev_rel(fdev); @@ -488,6 +519,77 @@ fuse_vfsop_unmount(struct mount *mp, int mntflags) return 0; } +static int +fuse_vfsop_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp) +{ + uint64_t nodeid = ino; + struct thread *td = curthread; + struct fuse_dispatcher fdi; + struct fuse_entry_out *feo; + struct fuse_vnode_data *fvdat; + const char dot[] = "."; + off_t filesize; + enum vtype vtyp; + int error; + + /* + * TODO Check the vnode cache, verifying entry cache timeout. Normally + * done during VOP_LOOKUP + */ + /*error = vfs_hash_get(mp, fuse_vnode_hash(nodeid), LK_EXCLUSIVE, td, vpp,*/ + /*fuse_vnode_cmp, &nodeid);*/ + /*if (error || *vpp != NULL)*/ + /*return error;*/ + + /* Do a LOOKUP, using nodeid as the parent and "." as filename */ + fdisp_init(&fdi, sizeof(dot)); + fdisp_make(&fdi, FUSE_LOOKUP, mp, nodeid, td, td->td_ucred); + memcpy(fdi.indata, dot, sizeof(dot)); + error = fdisp_wait_answ(&fdi); + + if (error) + return error; + + feo = (struct fuse_entry_out *)fdi.answ; + if (feo->nodeid == 0) { + /* zero nodeid means ENOENT and cache it */ + error = ENOENT; + goto out; + } + + vtyp = IFTOVT(feo->attr.mode); + error = fuse_vnode_get(mp, feo, nodeid, NULL, vpp, NULL, vtyp); + if (error) + goto out; + filesize = feo->attr.size; + + /* + * In the case where we are looking up a FUSE node represented by an + * existing cached vnode, and the true size reported by FUSE_LOOKUP + * doesn't match the vnode's cached size, then any cached writes beyond + * the file's current size are lost. + * + * We can get here: + * * following attribute cache expiration, or + * * due a bug in the daemon, or + */ + fvdat = VTOFUD(*vpp); + if (vnode_isreg(*vpp) && + filesize != fvdat->cached_attrs.va_size && + fvdat->flag & FN_SIZECHANGE) { + printf("%s: WB cache incoherent on %s!\n", __func__, + vnode_mount(*vpp)->mnt_stat.f_mntonname); + + fvdat->flag &= ~FN_SIZECHANGE; + } + + fuse_internal_cache_attrs(*vpp, td->td_ucred, &feo->attr, + feo->attr_valid, feo->attr_valid_nsec, NULL); +out: + fdisp_destroy(&fdi); + return error; +} + static int fuse_vfsop_root(struct mount *mp, int lkflags, struct vnode **vpp) { diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c index 2f3635edd5aa..c39b8bd69d72 100644 --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -151,6 +151,7 @@ static vop_write_t fuse_vnop_write; static vop_getpages_t fuse_vnop_getpages; static vop_putpages_t fuse_vnop_putpages; static vop_print_t fuse_vnop_print; +static vop_vptofh_t fuse_vnop_vptofh; struct vop_vector fuse_fifoops = { .vop_default = &fifo_specops, @@ -165,6 +166,7 @@ struct vop_vector fuse_fifoops = { .vop_reclaim = fuse_vnop_reclaim, .vop_setattr = fuse_vnop_setattr, .vop_write = VOP_PANIC, + .vop_vptofh = fuse_vnop_vptofh, }; struct vop_vector fuse_vnops = { @@ -202,6 +204,7 @@ struct vop_vector fuse_vnops = { .vop_getpages = fuse_vnop_getpages, .vop_putpages = fuse_vnop_putpages, .vop_print = fuse_vnop_print, + .vop_vptofh = fuse_vnop_vptofh, }; static u_long fuse_lookup_cache_hits = 0; @@ -908,6 +911,8 @@ fuse_vnop_lookup(struct vop_lookup_args *ap) return err; if (flags & ISDOTDOT) { + KASSERT(VTOFUD(dvp)->flag & FN_PARENT_NID, + ("Looking up .. is TODO")); nid = VTOFUD(dvp)->parent_nid; if (nid == 0) return ENOENT; @@ -970,8 +975,8 @@ fuse_vnop_lookup(struct vop_lookup_args *ap) if (!lookup_err) { /* lookup call succeeded */ - nid = ((struct fuse_entry_out *)fdi.answ)->nodeid; feo = (struct fuse_entry_out *)fdi.answ; + nid = feo->nodeid; if (nid == 0) { /* zero nodeid means ENOENT and cache it */ struct timespec timeout; @@ -2446,3 +2451,48 @@ fuse_vnop_print(struct vop_print_args *ap) return 0; } + +/* + * Get an NFS filehandle for a FUSE file. + * + * This will only work for FUSE file systems that guarantee the uniqueness of + * nodeid:generation, which most don't + */ +/* +vop_vptofh { + IN struct vnode *a_vp; + IN struct fid *a_fhp; +}; +*/ +static int +fuse_vnop_vptofh(struct vop_vptofh_args *ap) +{ + struct vnode *vp = ap->a_vp; + struct fuse_vnode_data *fvdat = VTOFUD(vp); + struct fuse_fid *fhp = (struct fuse_fid *)(ap->a_fhp); + _Static_assert(sizeof(struct fuse_fid) <= sizeof(struct fid), + "FUSE fid type is too big"); + struct mount *mp = vnode_mount(vp); + struct fuse_data *data = fuse_get_mpdata(mp); + struct vattr va; + int err; + + if (!(data->dataflags & FSESS_EXPORT_SUPPORT)) + return EOPNOTSUPP; + + err = fuse_internal_getattr(vp, &va, curthread->td_ucred, curthread); + if (err) + return err; + + /*ip = VTOI(ap->a_vp);*/ + /*ufhp = (struct ufid *)ap->a_fhp;*/ + fhp->len = sizeof(struct fuse_fid); + fhp->nid = fvdat->nid; + if (fvdat->generation <= UINT32_MAX) + fhp->gen = fvdat->generation; + else + return EOVERFLOW; + return (0); +} + + diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index 584db707747a..8c2b5ffa6316 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -26,6 +26,7 @@ GTESTS+= lookup GTESTS+= mkdir GTESTS+= mknod GTESTS+= mount +GTESTS+= nfs GTESTS+= open GTESTS+= opendir GTESTS+= read @@ -52,6 +53,7 @@ SRCS.$p+= utils.cc TEST_METADATA.default_permissions+= required_user="unprivileged" TEST_METADATA.default_permissions_privileged+= required_user="root" TEST_METADATA.mknod+= required_user="root" +TEST_METADATA.nfs+= required_user="root" # TODO: drastically increase timeout after test development is mostly complete TEST_METADATA+= timeout=10 diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc new file mode 100644 index 000000000000..d1edb33c455c --- /dev/null +++ b/tests/sys/fs/fusefs/nfs.cc @@ -0,0 +1,307 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* This file tests functionality needed by NFS servers */ +extern "C" { +#include +#include + +#include +#include +#include +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace std; +using namespace testing; + + +class Nfs: public FuseTest { +public: +virtual void SetUp() { + if (geteuid() != 0) + GTEST_SKIP() << "This test requires a privileged user"; + FuseTest::SetUp(); +} +}; + +class Exportable: public Nfs { +public: +virtual void SetUp() { + m_init_flags = FUSE_EXPORT_SUPPORT; + Nfs::SetUp(); +} +}; + +class Fhstat: public Exportable {}; +class FhstatNotExportable: public Nfs {}; +class Getfh: public Exportable {}; +class Readdir: public Exportable {}; + +/* If the server returns a different generation number, then file is stale */ +TEST_F(Fhstat, estale) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + Sequence seq; + + EXPECT_LOOKUP(1, RELDIRPATH) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 2; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(-1, fhstat(&fhp, &sb)); + EXPECT_EQ(ESTALE, errno); +} + +/* If we must lookup an entry from the server, send a LOOKUP request for "." */ +TEST_F(Fhstat, lookup_dot) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(1, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr.uid = uid; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr.uid = uid; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(uid, sb.st_uid); + EXPECT_EQ(mode, sb.st_mode); +} + +/* Use a file handle whose entry is still cached */ +/* + * Disabled because fuse_vfsop_vget doesn't yet check the entry cache. No PR + * because that's a feature request, not a bug + */ +TEST_F(Fhstat, DISABLED_cached) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(1, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr.uid = uid; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(uid, sb.st_uid); + EXPECT_EQ(mode, sb.st_mode); +} + +/* + * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style + * lookups + */ +TEST_F(FhstatNotExportable, lookup_dot) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + + EXPECT_LOOKUP(1, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); + ASSERT_EQ(EOPNOTSUPP, errno); +} + +/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ +TEST_F(Getfh, eoverflow) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + uint64_t ino = 42; + + EXPECT_LOOKUP(1, RELDIRPATH) + .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 = ino; + out->body.entry.generation = (uint64_t)UINT32_MAX + 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_NE(0, getfh(FULLPATH, &fhp)); + EXPECT_EQ(EOVERFLOW, errno); +} + +/* Get an NFS file handle */ +TEST_F(Getfh, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + uint64_t ino = 42; + + EXPECT_LOOKUP(1, RELDIRPATH) + .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 = ino; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); +} + +/* + * Call readdir via a file handle. + * + * This is how a userspace nfs server like nfs-ganesha or unfs3 would call + * readdir. The in-kernel NFS server never does any equivalent of open. I + * haven't discovered a way to mimic nfsd's behavior short of actually running + * nfsd. + */ +TEST_F(Readdir, getdirentries) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + mode_t mode = S_IFDIR | 0755; + fhandle_t fhp; + int fd; + char buf[8192]; + ssize_t r; + + EXPECT_LOOKUP(1, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = mode; + out->body.entry.nodeid = ino; + out->body.entry.generation = 1; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.entry_valid = 0; + }))); + + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_READDIR && + in->header.nodeid == ino && + in->body.readdir.size == sizeof(buf)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { + out->header.error = 0; + out->header.len = sizeof(out->header); + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + fd = fhopen(&fhp, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + r = getdirentries(fd, buf, sizeof(buf), 0); + ASSERT_EQ(0, r) << strerror(errno); + + /* Deliberately leak fd. RELEASEDIR will be tested separately */ +} diff --git a/tests/sys/fs/fusefs/readdir.cc b/tests/sys/fs/fusefs/readdir.cc index 35aff0eefd6c..6be4ce774c5a 100644 --- a/tests/sys/fs/fusefs/readdir.cc +++ b/tests/sys/fs/fusefs/readdir.cc @@ -45,56 +45,6 @@ void expect_lookup(const char *relpath, uint64_t ino) { FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); } - -void expect_readdir(uint64_t ino, uint64_t off, vector &ents) -{ - EXPECT_CALL(*m_mock, process( - ResultOf([=](auto in) { - return (in->header.opcode == FUSE_READDIR && - in->header.nodeid == ino && - in->body.readdir.fh == FH && - in->body.readdir.offset == off); - }, Eq(true)), - _) - ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto out) { - struct fuse_dirent *fde = (struct fuse_dirent*)out->body.bytes; - int i = 0; - - out->header.error = 0; - out->header.len = 0; - - for (const auto& it: ents) { - size_t entlen, entsize; - - fde->ino = it.d_fileno; - fde->off = it.d_off; - fde->type = it.d_type; - fde->namelen = it.d_namlen; - strncpy(fde->name, it.d_name, it.d_namlen); - entlen = FUSE_NAME_OFFSET + fde->namelen; - entsize = FUSE_DIRENT_SIZE(fde); - /* - * The FUSE protocol does not require zeroing out the - * unused portion of the name. But it's a good - * practice to prevent information disclosure to the - * FUSE client, even though the client is usually the - * kernel - */ - memset(fde->name + fde->namelen, 0, entsize - entlen); - if (out->header.len + entsize > in->body.read.size) { - printf("Overflow in readdir expectation: i=%d\n" - , i); - break; - } - out->header.len += entsize; - fde = (struct fuse_dirent*) - ((long*)fde + entsize / sizeof(long)); - i++; - } - out->header.len += sizeof(out->header); - }))); - -} }; class Readdir_7_8: public Readdir { diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 890578990bca..db9e27efdb71 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -35,6 +35,7 @@ extern "C" { #include #include +#include #include #include #include @@ -285,6 +286,56 @@ void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize, }))).RetiresOnSaturation(); } +void FuseTest::expect_readdir(uint64_t ino, uint64_t off, + std::vector &ents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_READDIR && + in->header.nodeid == ino && + in->body.readdir.fh == FH && + in->body.readdir.offset == off); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto out) { + struct fuse_dirent *fde = (struct fuse_dirent*)out->body.bytes; + int i = 0; + + out->header.error = 0; + out->header.len = 0; + + for (const auto& it: ents) { + size_t entlen, entsize; + + fde->ino = it.d_fileno; + fde->off = it.d_off; + fde->type = it.d_type; + fde->namelen = it.d_namlen; + strncpy(fde->name, it.d_name, it.d_namlen); + entlen = FUSE_NAME_OFFSET + fde->namelen; + entsize = FUSE_DIRENT_SIZE(fde); + /* + * The FUSE protocol does not require zeroing out the + * unused portion of the name. But it's a good + * practice to prevent information disclosure to the + * FUSE client, even though the client is usually the + * kernel + */ + memset(fde->name + fde->namelen, 0, entsize - entlen); + if (out->header.len + entsize > in->body.read.size) { + printf("Overflow in readdir expectation: i=%d\n" + , i); + break; + } + out->header.len += entsize; + fde = (struct fuse_dirent*) + ((long*)fde + entsize / sizeof(long)); + i++; + } + out->header.len += sizeof(out->header); + }))); + +} void FuseTest::expect_release(uint64_t ino, uint64_t fh) { EXPECT_CALL(*m_mock, process( diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 7c4a61dbb218..6149a6701a50 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -150,6 +150,14 @@ class FuseTest : public ::testing::Test { void expect_read(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents); + /* + * Create an expectation that FUSE_READIR will be called any number of + * times on the given ino with the given offset, returning (by copy) + * the provided entries + */ + void expect_readdir(uint64_t ino, uint64_t off, + std::vector &ents); + /* * Create an expectation that FUSE_RELEASE will be called exactly once * for the given inode and filehandle, returning success