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:
Alan Somers 2019-05-23 00:44:01 +00:00
parent 2013b723d3
commit e5b50fe736
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/projects/fuse2/; revision=348135
11 changed files with 557 additions and 64 deletions

View File

@ -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);

View File

@ -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 | \

View File

@ -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 &&

View File

@ -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;
}
}

View File

@ -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)
{

View File

@ -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);
}

View File

@ -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
View 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 */
}

View File

@ -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 {

View File

@ -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(

View File

@ -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