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
This commit is contained in:
parent
2013b723d3
commit
e5b50fe736
Notes:
svn2git
2020-12-20 02:59:44 +00:00
svn path=/projects/fuse2/; revision=348135
@ -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);
|
||||
|
@ -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 | \
|
||||
|
@ -77,7 +77,6 @@ __FBSDID("$FreeBSD$");
|
||||
#include <sys/mount.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <sys/fnv_hash.h>
|
||||
#include <sys/priv.h>
|
||||
#include <sys/buf.h>
|
||||
#include <security/mac/mac_framework.h>
|
||||
@ -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 &&
|
||||
|
@ -60,6 +60,7 @@
|
||||
#ifndef _FUSE_NODE_H_
|
||||
#define _FUSE_NODE_H_
|
||||
|
||||
#include <sys/fnv_hash.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/mutex.h>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
307
tests/sys/fs/fusefs/nfs.cc
Normal file
307
tests/sys/fs/fusefs/nfs.cc
Normal file
@ -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 <sys/param.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
}
|
||||
|
||||
#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 */
|
||||
}
|
@ -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<struct dirent> &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 {
|
||||
|
@ -35,6 +35,7 @@ extern "C" {
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <semaphore.h>
|
||||
@ -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<struct dirent> &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(
|
||||
|
@ -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<struct dirent> &ents);
|
||||
|
||||
/*
|
||||
* Create an expectation that FUSE_RELEASE will be called exactly once
|
||||
* for the given inode and filehandle, returning success
|
||||
|
Loading…
Reference in New Issue
Block a user