fusefs: merge from projects/fuse2

This commit imports the new fusefs driver. It raises the protocol level
from 7.8 to 7.23, fixes many bugs, adds a test suite for the driver, and
adds many new features. New features include:

* Optional kernel-side permissions checks (-o default_permissions)
* Implement VOP_MKNOD, VOP_BMAP, and VOP_ADVLOCK
* Allow interrupting FUSE operations
* Support named pipes and unix-domain sockets in fusefs file systems
* Forward UTIME_NOW during utimensat(2) to the daemon
* kqueue support for /dev/fuse
* Allow updating mounts with "mount -u"
* Allow exporting fusefs file systems over NFS
* Server-initiated invalidation of the name cache or data cache
* Respect RLIMIT_FSIZE
* Try to support servers as old as protocol 7.4

Performance enhancements include:

* Implement FUSE's FOPEN_KEEP_CACHE and FUSE_ASYNC_READ flags
* Cache file attributes
* Cache lookup entries, both positive and negative
* Server-selectable cache modes: writethrough, writeback, or uncached
* Write clustering
* Readahead
* Use counter(9) for statistical reporting

PR:		199934 216391 233783 234581 235773 235774 235775
PR:		236226 236231 236236 236291 236329 236381 236405
PR:		236327 236466 236472 236473 236474 236530 236557
PR:		236560 236844 237052 237181 237588 238565
Reviewed by:	bcr (man pages)
Reviewed by:	cem, ngie, rpokala, glebius, kib, bde, emaste (post-commit
		review on project branch)
MFC after:	3 weeks
Relnotes:	yes
Sponsored by:	The FreeBSD Foundation
Pull Request:	https://reviews.freebsd.org/D21110
This commit is contained in:
Alan Somers 2019-08-07 00:38:26 +00:00
commit 0b4275accb
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/head/; revision=350665
69 changed files with 20567 additions and 2286 deletions

View File

@ -53,6 +53,7 @@ contrib/pjdfstest asomers,ngie,pjd,#test Pre-commit review requested.
etc/mail gshapiro Pre-commit review requested. Keep in sync with -STABLE.
etc/sendmail gshapiro Pre-commit review requested. Keep in sync with -STABLE.
fetch des Pre-commit review requested, email only.
fusefs(5) asomers Pre-commit review requested.
geli pjd Pre-commit review requested (both sys/geom/eli/ and sbin/geom/class/eli/).
isci(4) jimharris Pre-commit review requested.
iwm(4) adrian Pre-commit review requested, send to freebsd-wireless@freebsd.org

View File

@ -26,6 +26,18 @@ NOTE TO PEOPLE WHO THINK THAT FreeBSD 13.x IS SLOW:
disable the most expensive debugging functionality run
"ln -s 'abort:false,junk:false' /etc/malloc.conf".)
20190727:
The vfs.fusefs.sync_unmount and vfs.fusefs.init_backgrounded sysctls
and the "-o sync_unmount" and "-o init_backgrounded" mount options have
been removed from mount_fusefs(8). You can safely remove them from
your scripts, because they had no effect.
The vfs.fusefs.fix_broken_io, vfs.fusefs.sync_resize,
vfs.fusefs.refresh_size, vfs.fusefs.mmap_enable,
vfs.fusefs.reclaim_revoked, and vfs.fusefs.data_cache_invalidate
sysctls have been removed. If you felt the need to set any of them to
a non-default value, please tell asomers@FreeBSD.org why.
20190713:
Default permissions on the /var/account/acct file (and copies of it
rotated by periodic daily scripts) are changed from 0644 to 0640

View File

@ -731,6 +731,8 @@
file
..
fs
fusefs
..
tmpfs
..
..

View File

@ -3,6 +3,11 @@
.\" Copyright (c) 2005, 2006 Csaba Henk
.\" All rights reserved.
.\"
.\" Copyright (c) 2019 The FreeBSD Foundation
.\"
.\" Portions of this documentation were written by BFF Storage Systems 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:
@ -29,7 +34,7 @@
.\"
.\" $FreeBSD$
.\"
.Dd November 17, 2018
.Dd July 31, 2019
.Dt MOUNT_FUSEFS 8
.Os
.Sh NAME
@ -108,27 +113,27 @@ Intended for use in scripts and the
.Xr sudoers 5
file.
.It Fl S , Ic --safe
Run in safe mode (i.e. reject invoking a filesystem daemon)
Run in safe mode (i.e., reject invoking a filesystem daemon).
.It Fl v
Be verbose
.It Fl D, Ic --daemon Ar daemon
Be verbose.
.It Fl D , Ic --daemon Ar daemon
Call the specified
.Ar daemon
.It Fl O, Ic --daemon_opts Ar opts
.Ar daemon .
.It Fl O , Ic --daemon_opts Ar opts
Add
.Ar opts
to the daemon's command line
.It Fl s, Ic --special Ar special
to the daemon's command line.
.It Fl s , Ic --special Ar special
Use
.Ar special
as special
.It Fl m, Ic --mountpath Ar node
as special.
.It Fl m , Ic --mountpath Ar node
Mount on
.Ar node
.It Fl h, Ic --help
Show help
.It Fl V, Ic --version
Show version information
.Ar node .
.It Fl h , Ic --help
Show help.
.It Fl V , Ic --version
Show version information.
.It Fl o
Mount options are specified via
.Fl o .
@ -136,23 +141,38 @@ The following options are available (and also their negated versions,
by prefixing them with
.Dq no ) :
.Bl -tag -width indent
.It Cm default_permissions
Enable traditional (file mode based) permission checking in kernel
.It Cm allow_other
Do not apply
.Sx STRICT ACCESS POLICY .
Only root can use this option
Only root can use this option.
.It Cm async
I/O to the file system may be done asynchronously.
Writes may be delayed and/or reordered.
.It Cm default_permissions
Enable traditional (file mode based) permission checking in kernel.
.It Cm intr
Allow signals to interrupt operations that are blocked waiting for a reply from the server.
When this option is in use, system calls may fail with
.Er EINTR
whenever a signal is received.
.It Cm max_read Ns = Ns Ar n
Limit size of read requests to
.Ar n
.Ar n .
.It Cm neglect_shares
Do not refuse unmounting if there are secondary mounts.
.It Cm private
Refuse shared mounting of the daemon.
This is the default behaviour, to allow sharing, expicitly use
.Fl o Cm noprivate
.It Cm neglect_shares
Do not refuse unmounting if there are secondary mounts
.Fl o Cm noprivate .
.It Cm push_symlinks_in
Prefix absolute symlinks with the mountpoint
Prefix absolute symlinks with the mountpoint.
.It Cm subtype Ns = Ns Ar fsname
Suffix
.Ar fsname
to the file system name as reported by
.Xr statfs 2 .
This option can be used to identify the file system implemented by
.Ar fuse_daemon .
.El
.El
.Pp
@ -167,11 +187,11 @@ However, there are some which do require in-kernel support.
Currently the options supported by the kernel are:
.Bl -tag -width indent
.It Cm direct_io
Bypass the buffer cache system
Bypass the buffer cache system.
.It Cm kernel_cache
By default cached buffers of a given file are flushed at each
.Xr open 2 .
This option disables this behaviour
This option disables this behaviour.
.El
.Sh DAEMON MOUNTS
Usually users do not need to use
@ -194,7 +214,7 @@ only if the filesystem daemon has the same credentials (uid, real uid, gid,
real gid) as the user.
.Pp
This is applied for Fuse mounts by default and only root can mount without
the strict access policy (i.e. the
the strict access policy (i.e., the
.Cm allow_other
mount option).
.Pp
@ -206,7 +226,7 @@ Users might opt to willingly relax strict access policy (as far they
are concerned) by doing their own secondary mount (See
.Sx SHARED MOUNTS ) .
.Sh SHARED MOUNTS
A Fuse daemon can be shared (i.e. mounted multiple times).
A Fuse daemon can be shared (i.e., mounted multiple times).
When doing the first (primary) mount, the spawner and the mounter of the daemon
must have the same uid, or the mounter should be the superuser.
.Pp
@ -225,7 +245,7 @@ is used or not.
.Pp
The device name of a secondary mount is the device name of the corresponding
primary mount, followed by a '#' character and the index of the secondary
mount; e.g.
mount; e.g.,
.Pa /dev/fuse0#3 .
.Sh SECURITY
System administrators might want to use a custom mount policy (ie., one going
@ -239,7 +259,7 @@ However, given that
is capable of invoking an arbitrary program, one must be careful when doing this.
.Nm
is designed in a way such that it makes that easy.
For this purpose, there are options which disable certain risky features (i.e.
For this purpose, there are options which disable certain risky features (
.Fl S
and
.Fl A ) ,
@ -342,7 +362,7 @@ does not call any external utility and also provides a hacky
was written as the part of the
.Fx
implementation of the Fuse userspace filesystem framework (see
.Xr https://github.com/libfuse/libfuse )
.Lk https://github.com/libfuse/libfuse )
and first appeared in the
.Pa sysutils/fusefs-kmod
port, supporting

View File

@ -5,6 +5,11 @@
* Copyright (c) 2005 Csaba Henk
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were developed by BFF Storage Systems 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:
@ -60,7 +65,6 @@ void __usage_short(void);
void usage(void);
void helpmsg(void);
void showversion(void);
int init_backgrounded(void);
static struct mntopt mopts[] = {
#define ALTF_PRIVATE 0x01
@ -73,8 +77,6 @@ static struct mntopt mopts[] = {
{ "max_read=", 0, ALTF_MAXREAD, 1 },
#define ALTF_SUBTYPE 0x40
{ "subtype=", 0, ALTF_SUBTYPE, 1 },
#define ALTF_SYNC_UNMOUNT 0x80
{ "sync_unmount", 0, ALTF_SYNC_UNMOUNT, 1 },
/*
* MOPT_AUTOMOUNTED, included by MOPT_STDOPTS, does not fit into
* the 'flags' argument to nmount(2). We have to abuse altflags
@ -82,6 +84,8 @@ static struct mntopt mopts[] = {
*/
#define ALTF_AUTOMOUNTED 0x100
{ "automounted", 0, ALTF_AUTOMOUNTED, 1 },
#define ALTF_INTR 0x200
{ "intr", 0, ALTF_INTR, 1 },
/* Linux specific options, we silently ignore them */
{ "fsname=", 0, 0x00, 1 },
{ "fd=", 0, 0x00, 1 },
@ -91,6 +95,8 @@ static struct mntopt mopts[] = {
{ "large_read", 0, 0x00, 1 },
/* "nonempty", just the first two chars are stripped off during parsing */
{ "nempty", 0, 0x00, 1 },
{ "async", 0, MNT_ASYNC, 0},
{ "noasync", 1, MNT_ASYNC, 0},
MOPT_STDOPTS,
MOPT_END
};
@ -107,7 +113,7 @@ static struct mntval mvals[] = {
{ 0, NULL, 0 }
};
#define DEFAULT_MOUNT_FLAGS ALTF_PRIVATE | ALTF_SYNC_UNMOUNT
#define DEFAULT_MOUNT_FLAGS ALTF_PRIVATE
int
main(int argc, char *argv[])
@ -409,12 +415,6 @@ main(int argc, char *argv[])
}
}
if (fd >= 0 && ! init_backgrounded() && close(fd) < 0) {
if (pid)
kill(pid, SIGKILL);
err(1, "failed to close fuse device");
}
/* Prepare the options vector for nmount(). build_iovec() is declared
* in mntopts.h. */
sprintf(fdstr, "%d", fd);
@ -471,6 +471,7 @@ helpmsg(void)
" -o allow_other allow access to other users\n"
/* " -o nonempty allow mounts over non-empty file/dir\n" */
" -o default_permissions enable permission checking by kernel\n"
" -o intr interruptible mount\n"
/*
" -o fsname=NAME set filesystem name\n"
" -o large_read issue large read requests (2.4 only)\n"
@ -481,7 +482,6 @@ helpmsg(void)
" -o neglect_shares don't report EBUSY when unmount attempted\n"
" in presence of secondary mounts\n"
" -o push_symlinks_in prefix absolute symlinks with mountpoint\n"
" -o sync_unmount do unmount synchronously\n"
);
exit(EX_USAGE);
}
@ -492,17 +492,3 @@ showversion(void)
puts("mount_fusefs [fuse4bsd] version: " FUSE4BSD_VERSION);
exit(EX_USAGE);
}
int
init_backgrounded(void)
{
int ibg;
size_t len;
len = sizeof(ibg);
if (sysctlbyname("vfs.fusefs.init_backgrounded", &ibg, &len, NULL, 0))
return (0);
return (ibg);
}

View File

@ -3,8 +3,8 @@
.\"
.\" Copyright (c) 2019 The FreeBSD Foundation
.\"
.\" This software was developed by BFF Storage Systems, LLC under sponsorship
.\" from the FreeBSD Foundation.
.\" This documentation was written 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
@ -28,7 +28,7 @@
.\" SUCH DAMAGE.
.\"
.\" $FreeBSD$
.Dd April 13, 2019
.Dd July 31, 2019
.Dt FUSEFS 5
.Os
.Sh NAME
@ -60,11 +60,9 @@ Finally, the
API is portable.
Many daemons can run on multiple operating systems with minimal modifications.
.Sh SYSCTL VARIABLES
The following variables are available as both
The following
.Xr sysctl 8
variables and
.Xr loader 8
tunables:
variables are available:
.Bl -tag -width indent
.It Va vfs.fusefs.kernelabi_major
Major version of the FUSE kernel ABI supported by this driver.
@ -73,7 +71,7 @@ Minor version of the FUSE kernel ABI supported by this driver.
.It Va vfs.fusefs.data_cache_mode
Controls how
.Nm
will cache file data.
will cache file data for pre-7.23 file systems.
A value of 0 will disable caching entirely.
Every data access will be forwarded to the daemon.
A value of 1 will select write-through caching.
@ -84,33 +82,26 @@ Reads and writes will both be cached, and writes will occasionally be flushed
to the daemon by the page daemon.
Write-back caching is usually unsafe, especially for FUSE file systems that
require network access.
.It Va vfs.fusefs.lookup_cache_enable
Controls whether
.Nm
will cache lookup responses from the file system.
FUSE file systems indicate whether lookup responses should be cacheable, but
it may be useful to globally disable caching them if a file system is
misbehaving.
.Pp
FUSE file systems using protocol 7.23 or later specify their cache behavior
on a per-mountpoint basis, ignoring this sysctl.
.It Va vfs.fusefs.stats.filehandle_count
Current number of open FUSE file handles.
.It Va vfs.fusefs.stats.lookup_cache_hits
Total number of lookup cache hits.
.It Va vfs.fusefs.stats.lookup_cache_misses
Total number of lookup cache misses.
.It Va vfs.fusefs.stats.node_count
Current number of allocated FUSE vnodes.
.It Va vfs.fusefs.stats.ticket_count
Current number of allocated FUSE tickets, which is roughly equal to the number
of FUSE operations currently being processed by daemons.
.\" Undocumented sysctls
.\" ====================
.\" Counters: I intend to rename to vfs.fusefs.stats.* for clarity
.\" vfs.fusefs.lookup_cache_{hits, misses}
.\" vfs.fusefs.filehandle_count
.\" vfs.fusefs.ticker_count
.\" vfs.fusefs.node_count
.\"
.\" vfs.fusefs.version - useless since the driver moved in-tree
.\" vfs.fusefs.reclaim_revoked: I don't understand it well-enough
.\" vfs.fusefs.sync_unmount: dead code
.\" vfs.fusefs.enforce_dev_perms: I don't understand it well enough.
.\" vfs.fusefs.init_backgrounded: dead code
.\" vfs.fusefs.iov_credit: I don't understand it well enough
.\" vfs.fusefs.iov_permanent_bufsize: I don't understand it well enough
.\" vfs.fusefs.fix_broken_io: I don't understand it well enough
.\" vfs.fusefs.sync_resize: useless and should be removed
.\" vfs.fusefs.refresh_size: probably useless?
.\" vfs.fusefs.mmap_enable: why is this optional?
.\" vfs.fusefs.data_cache_invalidate: what is this needed for?
.El
.Sh SEE ALSO
.Xr mount_fusefs 8
.Sh HISTORY
@ -119,7 +110,7 @@ The
driver was written as the part of the
.Fx
implementation of the FUSE userspace file system framework (see
.Xr https://github.com/libfuse/libfuse )
.Lk https://github.com/libfuse/libfuse )
and first appeared in the
.Pa sysutils/fusefs-kmod
port, supporting

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -63,87 +68,10 @@
#define FUSE_MIN_DAEMON_TIMEOUT 0 /* s */
#define FUSE_MAX_DAEMON_TIMEOUT 600 /* s */
#ifndef FUSE_FREEBSD_VERSION
#define FUSE_FREEBSD_VERSION "0.4.4"
#endif
/* Mapping versions to features */
#define FUSE_KERNELABI_GEQ(maj, min) \
(FUSE_KERNEL_VERSION > (maj) || (FUSE_KERNEL_VERSION == (maj) && FUSE_KERNEL_MINOR_VERSION >= (min)))
/*
* Appearance of new FUSE operations is not always in par with version
* numbering... At least, 7.3 is a sufficient condition for having
* FUSE_{ACCESS,CREATE}.
*/
#if FUSE_KERNELABI_GEQ(7, 3)
#ifndef FUSE_HAS_ACCESS
#define FUSE_HAS_ACCESS 1
#endif
#ifndef FUSE_HAS_CREATE
#define FUSE_HAS_CREATE 1
#endif
#else /* FUSE_KERNELABI_GEQ(7, 3) */
#ifndef FUSE_HAS_ACCESS
#define FUSE_HAS_ACCESS 0
#endif
#ifndef FUSE_HAS_CREATE
#define FUSE_HAS_CREATE 0
#endif
#endif
#if FUSE_KERNELABI_GEQ(7, 7)
#ifndef FUSE_HAS_GETLK
#define FUSE_HAS_GETLK 1
#endif
#ifndef FUSE_HAS_SETLK
#define FUSE_HAS_SETLK 1
#endif
#ifndef FUSE_HAS_SETLKW
#define FUSE_HAS_SETLKW 1
#endif
#ifndef FUSE_HAS_INTERRUPT
#define FUSE_HAS_INTERRUPT 1
#endif
#else /* FUSE_KERNELABI_GEQ(7, 7) */
#ifndef FUSE_HAS_GETLK
#define FUSE_HAS_GETLK 0
#endif
#ifndef FUSE_HAS_SETLK
#define FUSE_HAS_SETLK 0
#endif
#ifndef FUSE_HAS_SETLKW
#define FUSE_HAS_SETLKW 0
#endif
#ifndef FUSE_HAS_INTERRUPT
#define FUSE_HAS_INTERRUPT 0
#endif
#endif
#if FUSE_KERNELABI_GEQ(7, 8)
#ifndef FUSE_HAS_FLUSH_RELEASE
#define FUSE_HAS_FLUSH_RELEASE 1
/*
* "DESTROY" came in the middle of the 7.8 era,
* so this is not completely exact...
*/
#ifndef FUSE_HAS_DESTROY
#define FUSE_HAS_DESTROY 1
#endif
#endif
#else /* FUSE_KERNELABI_GEQ(7, 8) */
#ifndef FUSE_HAS_FLUSH_RELEASE
#define FUSE_HAS_FLUSH_RELEASE 0
#ifndef FUSE_HAS_DESTROY
#define FUSE_HAS_DESTROY 0
#endif
#endif
#endif
/* misc */
SYSCTL_DECL(_vfs_fusefs);
SYSCTL_DECL(_vfs_fusefs_stats);
/* Fuse locking */

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -81,27 +86,28 @@ __FBSDID("$FreeBSD$");
#include <sys/selinfo.h>
#include "fuse.h"
#include "fuse_internal.h"
#include "fuse_ipc.h"
SDT_PROVIDER_DECLARE(fuse);
SDT_PROVIDER_DECLARE(fusefs);
/*
* Fuse trace probe:
* arg0: verbosity. Higher numbers give more verbose messages
* arg1: Textual message
*/
SDT_PROBE_DEFINE2(fuse, , device, trace, "int", "char*");
SDT_PROBE_DEFINE2(fusefs, , device, trace, "int", "char*");
static struct cdev *fuse_dev;
static d_kqfilter_t fuse_device_filter;
static d_open_t fuse_device_open;
static d_close_t fuse_device_close;
static d_poll_t fuse_device_poll;
static d_read_t fuse_device_read;
static d_write_t fuse_device_write;
static struct cdevsw fuse_device_cdevsw = {
.d_kqfilter = fuse_device_filter,
.d_open = fuse_device_open,
.d_close = fuse_device_close,
.d_name = "fuse",
.d_poll = fuse_device_poll,
.d_read = fuse_device_read,
@ -109,6 +115,15 @@ static struct cdevsw fuse_device_cdevsw = {
.d_version = D_VERSION,
};
static int fuse_device_filt_read(struct knote *kn, long hint);
static void fuse_device_filt_detach(struct knote *kn);
struct filterops fuse_device_rfiltops = {
.f_isfd = 1,
.f_detach = fuse_device_filt_detach,
.f_event = fuse_device_filt_read,
};
/****************************
*
* >>> Fuse device op defs
@ -119,11 +134,100 @@ static void
fdata_dtor(void *arg)
{
struct fuse_data *fdata;
struct fuse_ticket *tick;
fdata = arg;
if (fdata == NULL)
return;
fdata_set_dead(fdata);
FUSE_LOCK();
fuse_lck_mtx_lock(fdata->aw_mtx);
/* wakup poll()ers */
selwakeuppri(&fdata->ks_rsel, PZERO + 1);
/* Don't let syscall handlers wait in vain */
while ((tick = fuse_aw_pop(fdata))) {
fuse_lck_mtx_lock(tick->tk_aw_mtx);
fticket_set_answered(tick);
tick->tk_aw_errno = ENOTCONN;
wakeup(tick);
fuse_lck_mtx_unlock(tick->tk_aw_mtx);
FUSE_ASSERT_AW_DONE(tick);
fuse_ticket_drop(tick);
}
fuse_lck_mtx_unlock(fdata->aw_mtx);
/* Cleanup unsent operations */
fuse_lck_mtx_lock(fdata->ms_mtx);
while ((tick = fuse_ms_pop(fdata))) {
fuse_ticket_drop(tick);
}
fuse_lck_mtx_unlock(fdata->ms_mtx);
FUSE_UNLOCK();
fdata_trydestroy(fdata);
}
static int
fuse_device_filter(struct cdev *dev, struct knote *kn)
{
struct fuse_data *data;
int error;
error = devfs_get_cdevpriv((void **)&data);
/* EVFILT_WRITE is not supported; the device is always ready to write */
if (error == 0 && kn->kn_filter == EVFILT_READ) {
kn->kn_fop = &fuse_device_rfiltops;
kn->kn_hook = data;
knlist_add(&data->ks_rsel.si_note, kn, 0);
error = 0;
} else if (error == 0) {
error = EINVAL;
kn->kn_data = error;
}
return (error);
}
static void
fuse_device_filt_detach(struct knote *kn)
{
struct fuse_data *data;
data = (struct fuse_data*)kn->kn_hook;
MPASS(data != NULL);
knlist_remove(&data->ks_rsel.si_note, kn, 0);
kn->kn_hook = NULL;
}
static int
fuse_device_filt_read(struct knote *kn, long hint)
{
struct fuse_data *data;
int ready;
data = (struct fuse_data*)kn->kn_hook;
MPASS(data != NULL);
mtx_assert(&data->ms_mtx, MA_OWNED);
if (fdata_get_dead(data)) {
kn->kn_flags |= EV_EOF;
kn->kn_fflags = ENODEV;
kn->kn_data = 1;
ready = 1;
} else if (STAILQ_FIRST(&data->ms_head)) {
MPASS(data->ms_count >= 1);
kn->kn_data = data->ms_count;
ready = 1;
} else {
ready = 0;
}
return (ready);
}
/*
* Resources are set up on a per-open basis
*/
@ -133,52 +237,17 @@ fuse_device_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
struct fuse_data *fdata;
int error;
SDT_PROBE2(fuse, , device, trace, 1, "device open");
SDT_PROBE2(fusefs, , device, trace, 1, "device open");
fdata = fdata_alloc(dev, td->td_ucred);
error = devfs_set_cdevpriv(fdata, fdata_dtor);
if (error != 0)
fdata_trydestroy(fdata);
else
SDT_PROBE2(fuse, , device, trace, 1, "device open success");
SDT_PROBE2(fusefs, , device, trace, 1, "device open success");
return (error);
}
static int
fuse_device_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
struct fuse_data *data;
struct fuse_ticket *tick;
int error;
error = devfs_get_cdevpriv((void **)&data);
if (error != 0)
return (error);
if (!data)
panic("no fuse data upon fuse device close");
fdata_set_dead(data);
FUSE_LOCK();
fuse_lck_mtx_lock(data->aw_mtx);
/* wakup poll()ers */
selwakeuppri(&data->ks_rsel, PZERO + 1);
/* Don't let syscall handlers wait in vain */
while ((tick = fuse_aw_pop(data))) {
fuse_lck_mtx_lock(tick->tk_aw_mtx);
fticket_set_answered(tick);
tick->tk_aw_errno = ENOTCONN;
wakeup(tick);
fuse_lck_mtx_unlock(tick->tk_aw_mtx);
FUSE_ASSERT_AW_DONE(tick);
fuse_ticket_drop(tick);
}
fuse_lck_mtx_unlock(data->aw_mtx);
FUSE_UNLOCK();
SDT_PROBE2(fuse, , device, trace, 1, "device close");
return (0);
}
int
fuse_device_poll(struct cdev *dev, int events, struct thread *td)
{
@ -219,7 +288,7 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
int buflen[3];
int i;
SDT_PROBE2(fuse, , device, trace, 1, "fuse device read");
SDT_PROBE2(fusefs, , device, trace, 1, "fuse device read");
err = devfs_get_cdevpriv((void **)&data);
if (err != 0)
@ -228,7 +297,7 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
fuse_lck_mtx_lock(data->ms_mtx);
again:
if (fdata_get_dead(data)) {
SDT_PROBE2(fuse, , device, trace, 2,
SDT_PROBE2(fusefs, , device, trace, 2,
"we know early on that reader should be kicked so we "
"don't wait for news");
fuse_lck_mtx_unlock(data->ms_mtx);
@ -256,7 +325,7 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
* -- and some other cases, too, tho not totally clear, when
* (cv_signal/wakeup_one signals the whole process ?)
*/
SDT_PROBE2(fuse, , device, trace, 1, "no message on thread");
SDT_PROBE2(fusefs, , device, trace, 1, "no message on thread");
goto again;
}
fuse_lck_mtx_unlock(data->ms_mtx);
@ -266,9 +335,10 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
* somebody somewhere -- eg., umount routine --
* wants this liaison finished off
*/
SDT_PROBE2(fuse, , device, trace, 2, "reader is to be sacked");
SDT_PROBE2(fusefs, , device, trace, 2,
"reader is to be sacked");
if (tick) {
SDT_PROBE2(fuse, , device, trace, 2, "weird -- "
SDT_PROBE2(fusefs, , device, trace, 2, "weird -- "
"\"kick\" is set tho there is message");
FUSE_ASSERT_MS_DONE(tick);
fuse_ticket_drop(tick);
@ -276,7 +346,7 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
return (ENODEV); /* This should make the daemon get off
* of us */
}
SDT_PROBE2(fuse, , device, trace, 1,
SDT_PROBE2(fusefs, , device, trace, 1,
"fuse device read message successfully");
KASSERT(tick->tk_ms_bufdata || tick->tk_ms_bufsize == 0,
@ -311,7 +381,7 @@ fuse_device_read(struct cdev *dev, struct uio *uio, int ioflag)
*/
if (uio->uio_resid < buflen[i]) {
fdata_set_dead(data);
SDT_PROBE2(fuse, , device, trace, 2,
SDT_PROBE2(fusefs, , device, trace, 2,
"daemon is stupid, kick it off...");
err = ENODEV;
break;
@ -331,23 +401,26 @@ static inline int
fuse_ohead_audit(struct fuse_out_header *ohead, struct uio *uio)
{
if (uio->uio_resid + sizeof(struct fuse_out_header) != ohead->len) {
SDT_PROBE2(fuse, , device, trace, 1, "Format error: body size "
SDT_PROBE2(fusefs, , device, trace, 1,
"Format error: body size "
"differs from size claimed by header");
return (EINVAL);
}
if (uio->uio_resid && ohead->error) {
SDT_PROBE2(fuse, , device, trace, 1,
if (uio->uio_resid && ohead->unique != 0 && ohead->error) {
SDT_PROBE2(fusefs, , device, trace, 1,
"Format error: non zero error but message had a body");
return (EINVAL);
}
/* Sanitize the linuxism of negative errnos */
ohead->error = -(ohead->error);
return (0);
}
SDT_PROBE_DEFINE1(fuse, , device, fuse_device_write_bumped_into_callback,
"uint64_t");
SDT_PROBE_DEFINE1(fusefs, , device, fuse_device_write_notify,
"struct fuse_out_header*");
SDT_PROBE_DEFINE1(fusefs, , device, fuse_device_write_missing_ticket,
"uint64_t");
SDT_PROBE_DEFINE1(fusefs, , device, fuse_device_write_found,
"struct fuse_ticket*");
/*
* fuse_device_write first reads the header sent by the daemon.
* If that's OK, looks up ticket/callback node by the unique id seen in header.
@ -360,15 +433,17 @@ fuse_device_write(struct cdev *dev, struct uio *uio, int ioflag)
struct fuse_out_header ohead;
int err = 0;
struct fuse_data *data;
struct fuse_ticket *tick, *x_tick;
struct mount *mp;
struct fuse_ticket *tick, *itick, *x_tick;
int found = 0;
err = devfs_get_cdevpriv((void **)&data);
if (err != 0)
return (err);
mp = data->mp;
if (uio->uio_resid < sizeof(struct fuse_out_header)) {
SDT_PROBE2(fuse, , device, trace, 1,
SDT_PROBE2(fusefs, , device, trace, 1,
"fuse_device_write got less than a header!");
fdata_set_dead(data);
return (EINVAL);
@ -393,15 +468,29 @@ fuse_device_write(struct cdev *dev, struct uio *uio, int ioflag)
fuse_lck_mtx_lock(data->aw_mtx);
TAILQ_FOREACH_SAFE(tick, &data->aw_head, tk_aw_link,
x_tick) {
SDT_PROBE1(fuse, , device,
fuse_device_write_bumped_into_callback,
tick->tk_unique);
if (tick->tk_unique == ohead.unique) {
SDT_PROBE1(fusefs, , device, fuse_device_write_found,
tick);
found = 1;
fuse_aw_remove(tick);
break;
}
}
if (found && tick->irq_unique > 0) {
/*
* Discard the FUSE_INTERRUPT ticket that tried to interrupt
* this operation
*/
TAILQ_FOREACH_SAFE(itick, &data->aw_head, tk_aw_link,
x_tick) {
if (itick->tk_unique == tick->irq_unique) {
fuse_aw_remove(itick);
fuse_ticket_drop(itick);
break;
}
}
tick->irq_unique = 0;
}
fuse_lck_mtx_unlock(data->aw_mtx);
if (found) {
@ -414,13 +503,15 @@ fuse_device_write(struct cdev *dev, struct uio *uio, int ioflag)
* via ticket_drop(), so no manual mucking
* around...)
*/
SDT_PROBE2(fuse, , device, trace, 1,
SDT_PROBE2(fusefs, , device, trace, 1,
"pass ticket to a callback");
/* Sanitize the linuxism of negative errnos */
ohead.error *= -1;
memcpy(&tick->tk_aw_ohead, &ohead, sizeof(ohead));
err = tick->tk_aw_handler(tick, uio);
} else {
/* pretender doesn't wanna do anything with answer */
SDT_PROBE2(fuse, , device, trace, 1,
SDT_PROBE2(fusefs, , device, trace, 1,
"stuff devalidated, so we drop it");
}
@ -430,11 +521,51 @@ fuse_device_write(struct cdev *dev, struct uio *uio, int ioflag)
* because fuse_ticket_drop() will deal with refcount anyway.
*/
fuse_ticket_drop(tick);
} else if (ohead.unique == 0){
/* unique == 0 means asynchronous notification */
SDT_PROBE1(fusefs, , device, fuse_device_write_notify, &ohead);
switch (ohead.error) {
case FUSE_NOTIFY_INVAL_ENTRY:
err = fuse_internal_invalidate_entry(mp, uio);
break;
case FUSE_NOTIFY_INVAL_INODE:
err = fuse_internal_invalidate_inode(mp, uio);
break;
case FUSE_NOTIFY_RETRIEVE:
case FUSE_NOTIFY_STORE:
/*
* Unimplemented. I don't know of any file systems
* that use them, and the protocol isn't sound anyway,
* since the notification messages don't include the
* inode's generation number. Without that, it's
* possible to manipulate the cache of the wrong vnode.
* Finally, it's not defined what this message should
* do for a file with dirty cache.
*/
case FUSE_NOTIFY_POLL:
/* Unimplemented. See comments in fuse_vnops */
default:
/* Not implemented */
err = ENOSYS;
}
} else {
/* no callback at all! */
SDT_PROBE2(fuse, , device, trace, 1,
"erhm, no handler for this response");
err = EINVAL;
SDT_PROBE1(fusefs, , device, fuse_device_write_missing_ticket,
ohead.unique);
if (ohead.error == -EAGAIN) {
/*
* This was probably a response to a FUSE_INTERRUPT
* operation whose original operation is already
* complete. We can't store FUSE_INTERRUPT tickets
* indefinitely because their responses are optional.
* So we delete them when the original operation
* completes. And sadly the fuse_header_out doesn't
* identify the opcode, so we have to guess.
*/
err = 0;
} else {
err = EINVAL;
}
}
return (err);
@ -445,7 +576,7 @@ fuse_device_init(void)
{
fuse_dev = make_dev(&fuse_device_cdevsw, 0, UID_ROOT, GID_OPERATOR,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP, "fuse");
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, "fuse");
if (fuse_dev == NULL)
return (ENOMEM);
return (0);

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -59,8 +64,9 @@
__FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/counter.h>
#include <sys/module.h>
#include <sys/errno.h>
#include <sys/kernel.h>
#include <sys/conf.h>
@ -79,52 +85,61 @@ __FBSDID("$FreeBSD$");
#include "fuse.h"
#include "fuse_file.h"
#include "fuse_internal.h"
#include "fuse_io.h"
#include "fuse_ipc.h"
#include "fuse_node.h"
SDT_PROVIDER_DECLARE(fuse);
MALLOC_DEFINE(M_FUSE_FILEHANDLE, "fuse_filefilehandle", "FUSE file handle");
SDT_PROVIDER_DECLARE(fusefs);
/*
* Fuse trace probe:
* arg0: verbosity. Higher numbers give more verbose messages
* arg1: Textual message
*/
SDT_PROBE_DEFINE2(fuse, , file, trace, "int", "char*");
SDT_PROBE_DEFINE2(fusefs, , file, trace, "int", "char*");
static int fuse_fh_count = 0;
static counter_u64_t fuse_fh_count;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, filehandle_count, CTLFLAG_RD,
&fuse_fh_count, 0, "number of open FUSE filehandles");
SYSCTL_COUNTER_U64(_vfs_fusefs_stats, OID_AUTO, filehandle_count, CTLFLAG_RD,
&fuse_fh_count, "number of open FUSE filehandles");
/* Get the FUFH type for a particular access mode */
static inline fufh_type_t
fflags_2_fufh_type(int fflags)
{
if ((fflags & FREAD) && (fflags & FWRITE))
return FUFH_RDWR;
else if (fflags & (FWRITE))
return FUFH_WRONLY;
else if (fflags & (FREAD))
return FUFH_RDONLY;
else if (fflags & (FEXEC))
return FUFH_EXEC;
else
panic("FUSE: What kind of a flag is this (%x)?", fflags);
}
int
fuse_filehandle_open(struct vnode *vp, fufh_type_t fufh_type,
fuse_filehandle_open(struct vnode *vp, int a_mode,
struct fuse_filehandle **fufhp, struct thread *td, struct ucred *cred)
{
struct fuse_dispatcher fdi;
struct fuse_open_in *foi;
struct fuse_open_out *foo;
fufh_type_t fufh_type;
int err = 0;
int oflags = 0;
int op = FUSE_OPEN;
if (fuse_filehandle_valid(vp, fufh_type)) {
panic("FUSE: filehandle_open called despite valid fufh (type=%d)",
fufh_type);
/* NOTREACHED */
}
/*
* Note that this means we are effectively FILTERING OUT open() flags.
*/
oflags = fuse_filehandle_xlate_to_oflags(fufh_type);
fufh_type = fflags_2_fufh_type(a_mode);
oflags = fufh_type_2_fflags(fufh_type);
if (vnode_isdir(vp)) {
op = FUSE_OPENDIR;
if (fufh_type != FUFH_RDONLY) {
SDT_PROBE2(fuse, , file, trace, 1,
"non-rdonly fh requested for a directory?");
printf("FUSE:non-rdonly fh requested for a directory?\n");
fufh_type = FUFH_RDONLY;
}
/* vn_open_vnode already rejects FWRITE on directories */
MPASS(fufh_type == FUFH_RDONLY || fufh_type == FUFH_EXEC);
}
fdisp_init(&fdi, sizeof(*foi));
fdisp_make_vp(&fdi, op, vp, td, cred);
@ -133,7 +148,7 @@ fuse_filehandle_open(struct vnode *vp, fufh_type_t fufh_type,
foi->flags = oflags;
if ((err = fdisp_wait_answ(&fdi))) {
SDT_PROBE2(fuse, , file, trace, 1,
SDT_PROBE2(fusefs, , file, trace, 1,
"OUCH ... daemon didn't give fh");
if (err == ENOENT) {
fuse_internal_vnode_disappear(vp);
@ -142,18 +157,8 @@ fuse_filehandle_open(struct vnode *vp, fufh_type_t fufh_type,
}
foo = fdi.answ;
fuse_filehandle_init(vp, fufh_type, fufhp, foo->fh);
/*
* For WRONLY opens, force DIRECT_IO. This is necessary
* since writing a partial block through the buffer cache
* will result in a read of the block and that read won't
* be allowed by the WRONLY open.
*/
if (fufh_type == FUFH_WRONLY)
fuse_vnode_open(vp, foo->open_flags | FOPEN_DIRECT_IO, td);
else
fuse_vnode_open(vp, foo->open_flags, td);
fuse_filehandle_init(vp, fufh_type, fufhp, td, cred, foo);
fuse_vnode_open(vp, foo->open_flags, td);
out:
fdisp_destroy(&fdi);
@ -161,23 +166,15 @@ fuse_filehandle_open(struct vnode *vp, fufh_type_t fufh_type,
}
int
fuse_filehandle_close(struct vnode *vp, fufh_type_t fufh_type,
fuse_filehandle_close(struct vnode *vp, struct fuse_filehandle *fufh,
struct thread *td, struct ucred *cred)
{
struct fuse_dispatcher fdi;
struct fuse_release_in *fri;
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh = NULL;
int err = 0;
int op = FUSE_RELEASE;
fufh = &(fvdat->fufh[fufh_type]);
if (!FUFH_IS_VALID(fufh)) {
panic("FUSE: filehandle_put called on invalid fufh (type=%d)",
fufh_type);
/* NOTREACHED */
}
if (fuse_isdeadfs(vp)) {
goto out;
}
@ -187,96 +184,193 @@ fuse_filehandle_close(struct vnode *vp, fufh_type_t fufh_type,
fdisp_make_vp(&fdi, op, vp, td, cred);
fri = fdi.indata;
fri->fh = fufh->fh_id;
fri->flags = fuse_filehandle_xlate_to_oflags(fufh_type);
fri->flags = fufh_type_2_fflags(fufh->fufh_type);
/*
* If the file has a POSIX lock then we're supposed to set lock_owner.
* If not, then lock_owner is undefined. So we may as well always set
* it.
*/
fri->lock_owner = td->td_proc->p_pid;
err = fdisp_wait_answ(&fdi);
fdisp_destroy(&fdi);
out:
atomic_subtract_acq_int(&fuse_fh_count, 1);
fufh->fh_id = (uint64_t)-1;
fufh->fh_type = FUFH_INVALID;
counter_u64_add(fuse_fh_count, -1);
LIST_REMOVE(fufh, next);
free(fufh, M_FUSE_FILEHANDLE);
return err;
}
int
fuse_filehandle_valid(struct vnode *vp, fufh_type_t fufh_type)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
fufh = &(fvdat->fufh[fufh_type]);
return FUFH_IS_VALID(fufh);
}
/*
* Check for a valid file handle, first the type requested, but if that
* isn't valid, try for FUFH_RDWR.
* Return the FUFH type that is valid or FUFH_INVALID if there are none.
* This is a variant of fuse_filehandle_vaild() analogous to
* fuse_filehandle_getrw().
* Return true if there is any file handle with the correct credentials and
* a fufh type that includes the provided one.
* A pid of 0 means "don't care"
*/
fufh_type_t
fuse_filehandle_validrw(struct vnode *vp, fufh_type_t fufh_type)
bool
fuse_filehandle_validrw(struct vnode *vp, int mode,
struct ucred *cred, pid_t pid)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
fufh_type_t fufh_type = fflags_2_fufh_type(mode);
fufh = &fvdat->fufh[fufh_type];
if (FUFH_IS_VALID(fufh) != 0)
return (fufh_type);
fufh = &fvdat->fufh[FUFH_RDWR];
if (FUFH_IS_VALID(fufh) != 0)
return (FUFH_RDWR);
return (FUFH_INVALID);
/*
* Unlike fuse_filehandle_get, we want to search for a filehandle with
* the exact cred, and no fallback
*/
LIST_FOREACH(fufh, &fvdat->handles, next) {
if (fufh->fufh_type == fufh_type &&
fufh->uid == cred->cr_uid &&
fufh->gid == cred->cr_rgid &&
(pid == 0 || fufh->pid == pid))
return true;
}
if (fufh_type == FUFH_EXEC)
return false;
/* Fallback: find a RDWR list entry with the right cred */
LIST_FOREACH(fufh, &fvdat->handles, next) {
if (fufh->fufh_type == FUFH_RDWR &&
fufh->uid == cred->cr_uid &&
fufh->gid == cred->cr_rgid &&
(pid == 0 || fufh->pid == pid))
return true;
}
return false;
}
int
fuse_filehandle_get(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp)
fuse_filehandle_get(struct vnode *vp, int fflag,
struct fuse_filehandle **fufhp, struct ucred *cred, pid_t pid)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
fufh_type_t fufh_type;
fufh_type = fflags_2_fufh_type(fflag);
/* cred can be NULL for in-kernel clients */
if (cred == NULL)
goto fallback;
LIST_FOREACH(fufh, &fvdat->handles, next) {
if (fufh->fufh_type == fufh_type &&
fufh->uid == cred->cr_uid &&
fufh->gid == cred->cr_rgid &&
(pid == 0 || fufh->pid == pid))
goto found;
}
fallback:
/* Fallback: find a list entry with the right flags */
LIST_FOREACH(fufh, &fvdat->handles, next) {
if (fufh->fufh_type == fufh_type)
break;
}
if (fufh == NULL)
return EBADF;
found:
if (fufhp != NULL)
*fufhp = fufh;
return 0;
}
/* Get a file handle with any kind of flags */
int
fuse_filehandle_get_anyflags(struct vnode *vp,
struct fuse_filehandle **fufhp, struct ucred *cred, pid_t pid)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
fufh = &(fvdat->fufh[fufh_type]);
if (!FUFH_IS_VALID(fufh))
if (cred == NULL)
goto fallback;
LIST_FOREACH(fufh, &fvdat->handles, next) {
if (fufh->uid == cred->cr_uid &&
fufh->gid == cred->cr_rgid &&
(pid == 0 || fufh->pid == pid))
goto found;
}
fallback:
/* Fallback: find any list entry */
fufh = LIST_FIRST(&fvdat->handles);
if (fufh == NULL)
return EBADF;
found:
if (fufhp != NULL)
*fufhp = fufh;
return 0;
}
int
fuse_filehandle_getrw(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp)
fuse_filehandle_getrw(struct vnode *vp, int fflag,
struct fuse_filehandle **fufhp, struct ucred *cred, pid_t pid)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
int err;
fufh = &(fvdat->fufh[fufh_type]);
if (!FUFH_IS_VALID(fufh)) {
fufh_type = FUFH_RDWR;
}
return fuse_filehandle_get(vp, fufh_type, fufhp);
err = fuse_filehandle_get(vp, fflag, fufhp, cred, pid);
if (err)
err = fuse_filehandle_get(vp, FREAD | FWRITE, fufhp, cred, pid);
return err;
}
void
fuse_filehandle_init(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp, uint64_t fh_id)
struct fuse_filehandle **fufhp, struct thread *td, struct ucred *cred,
struct fuse_open_out *foo)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_filehandle *fufh;
fufh = &(fvdat->fufh[fufh_type]);
MPASS(!FUFH_IS_VALID(fufh));
fufh->fh_id = fh_id;
fufh->fh_type = fufh_type;
fufh = malloc(sizeof(struct fuse_filehandle), M_FUSE_FILEHANDLE,
M_WAITOK);
MPASS(fufh != NULL);
fufh->fh_id = foo->fh;
fufh->fufh_type = fufh_type;
fufh->gid = cred->cr_rgid;
fufh->uid = cred->cr_uid;
fufh->pid = td->td_proc->p_pid;
fufh->fuse_open_flags = foo->open_flags;
if (!FUFH_IS_VALID(fufh)) {
panic("FUSE: init: invalid filehandle id (type=%d)", fufh_type);
}
LIST_INSERT_HEAD(&fvdat->handles, fufh, next);
if (fufhp != NULL)
*fufhp = fufh;
atomic_add_acq_int(&fuse_fh_count, 1);
counter_u64_add(fuse_fh_count, 1);
if (foo->open_flags & FOPEN_DIRECT_IO) {
ASSERT_VOP_ELOCKED(vp, __func__);
VTOFUD(vp)->flag |= FN_DIRECTIO;
fuse_io_invalbuf(vp, td);
} else {
if ((foo->open_flags & FOPEN_KEEP_CACHE) == 0)
fuse_io_invalbuf(vp, td);
VTOFUD(vp)->flag &= ~FN_DIRECTIO;
}
}
void
fuse_file_init(void)
{
fuse_fh_count = counter_u64_alloc(M_WAITOK);
}
void
fuse_file_destroy(void)
{
counter_u64_free(fuse_fh_count);
}

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -66,52 +71,115 @@
#include <sys/mman.h>
#include <sys/vnode.h>
/*
* The fufh type is the access mode of the fuse file handle. It's the portion
* of the open(2) flags related to permission.
*/
typedef enum fufh_type {
FUFH_INVALID = -1,
FUFH_RDONLY = 0,
FUFH_WRONLY = 1,
FUFH_RDWR = 2,
FUFH_MAXTYPE = 3,
FUFH_RDONLY = O_RDONLY,
FUFH_WRONLY = O_WRONLY,
FUFH_RDWR = O_RDWR,
FUFH_EXEC = O_EXEC,
} fufh_type_t;
_Static_assert(FUFH_RDONLY == O_RDONLY, "RDONLY");
_Static_assert(FUFH_WRONLY == O_WRONLY, "WRONLY");
_Static_assert(FUFH_RDWR == O_RDWR, "RDWR");
/*
* FUSE File Handles
*
* The FUSE protocol says that a server may assign a unique 64-bit file handle
* every time that a file is opened. Effectively, that's once for each file
* descriptor.
*
* Unfortunately, the VFS doesn't help us here. VOPs don't have a
* struct file* argument. fileops do, but many syscalls bypass the fileops
* layer and go straight to a vnode. Some, like writing from cache, can't
* track a file handle even in theory. The entire concept of the file handle
* is a product of FUSE's Linux origins; Linux lacks vnodes and almost every
* file system operation takes a struct file* argument.
*
* Since FreeBSD's VFS is more file descriptor-agnostic, we must store FUSE
* filehandles in the vnode. One option would be to only store a single file
* handle and never open FUSE files concurrently. That's what NetBSD does.
* But that violates FUSE's security model. FUSE expects the server to do all
* authorization (except when mounted with -o default_permissions). In order
* to do that, the server needs us to send FUSE_OPEN every time somebody opens
* a new file descriptor.
*
* Another option would be to never open FUSE files concurrently, but send a
* FUSE_ACCESS prior to every open after the first. That would give the server
* the opportunity to authorize the access. Unfortunately, the FUSE protocol
* makes ACCESS optional. File systems that don't implement it are assumed to
* authorize everything. A survey of 32 fuse file systems showed that only 14
* implemented access. Among the laggards were a few that really ought to be
* doing server-side authorization.
*
* So we do something hacky, similar to what OpenBSD, Illumos, and OSXFuse do.
* we store a list of file handles, one for each combination of vnode, uid,
* gid, pid, and access mode. When opening a file, we first check whether
* there's already a matching file handle. If so, we reuse it. If not, we
* send FUSE_OPEN and create a new file handle. That minimizes the number of
* open file handles while still allowing the server to authorize stuff.
*
* VOPs that need a file handle search through the list for a close match.
* They can't be guaranteed of finding an exact match because, for example, a
* process may have changed its UID since opening the file. Also, most VOPs
* don't know exactly what permission they need. Is O_RDWR required or is
* O_RDONLY good enough? So the file handle we end up using may not be exactly
* the one we're supposed to use with that file descriptor. But if the FUSE
* file system isn't too picky, it will work. (FWIW even Linux sometimes
* guesses the file handle, during writes from cache or most SETATTR
* operations).
*
* I suspect this mess is part of the reason why neither NFS nor 9P have an
* equivalent of FUSE file handles.
*/
struct fuse_filehandle {
LIST_ENTRY(fuse_filehandle) next;
/* The filehandle returned by FUSE_OPEN */
uint64_t fh_id;
fufh_type_t fh_type;
/*
* flags returned by FUSE_OPEN
* Supported flags: FOPEN_DIRECT_IO, FOPEN_KEEP_CACHE
* Unsupported:
* FOPEN_NONSEEKABLE: Adding support would require a new per-file
* or per-vnode attribute, which would have to be checked by
* kern_lseek (and others) for every file system. The benefit is
* dubious, since I'm unaware of any file systems in ports that use
* this flag.
*/
uint32_t fuse_open_flags;
/* The access mode of the file handle */
fufh_type_t fufh_type;
/* Credentials used to open the file */
gid_t gid;
pid_t pid;
uid_t uid;
};
#define FUFH_IS_VALID(f) ((f)->fh_type != FUFH_INVALID)
static inline fufh_type_t
fuse_filehandle_xlate_from_mmap(int fflags)
{
if (fflags & (PROT_READ | PROT_WRITE))
return FUFH_RDWR;
else if (fflags & (PROT_WRITE))
return FUFH_WRONLY;
else if ((fflags & PROT_READ) || (fflags & PROT_EXEC))
return FUFH_RDONLY;
else
return FUFH_INVALID;
}
static inline fufh_type_t
fuse_filehandle_xlate_from_fflags(int fflags)
{
if ((fflags & FREAD) && (fflags & FWRITE))
return FUFH_RDWR;
else if (fflags & (FWRITE))
return FUFH_WRONLY;
else if (fflags & (FREAD))
return FUFH_RDONLY;
else
panic("FUSE: What kind of a flag is this (%x)?", fflags);
}
#define FUFH_IS_VALID(f) ((f)->fufh_type != FUFH_INVALID)
/*
* Get the flags to use for FUSE_CREATE, FUSE_OPEN and FUSE_RELEASE
*
* These are supposed to be the same as the flags argument to open(2).
* However, since we can't reliably associate a fuse_filehandle with a specific
* file descriptor it would would be dangerous to include anything more than
* the access mode flags. For example, suppose we open a file twice, once with
* O_APPEND and once without. Then the user pwrite(2)s to offset using the
* second file descriptor. If fusefs uses the first file handle, then the
* server may append the write to the end of the file rather than at offset 0.
* To prevent problems like this, we only ever send the portion of flags
* related to access mode.
*
* It's essential to send that portion, because FUSE uses it for server-side
* authorization.
*/
static inline int
fuse_filehandle_xlate_to_oflags(fufh_type_t type)
fufh_type_2_fflags(fufh_type_t type)
{
int oflags = -1;
@ -119,6 +187,7 @@ fuse_filehandle_xlate_to_oflags(fufh_type_t type)
case FUFH_RDONLY:
case FUFH_WRONLY:
case FUFH_RDWR:
case FUFH_EXEC:
oflags = type;
break;
default:
@ -128,19 +197,28 @@ fuse_filehandle_xlate_to_oflags(fufh_type_t type)
return oflags;
}
int fuse_filehandle_valid(struct vnode *vp, fufh_type_t fufh_type);
fufh_type_t fuse_filehandle_validrw(struct vnode *vp, fufh_type_t fufh_type);
int fuse_filehandle_get(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp);
int fuse_filehandle_getrw(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp);
bool fuse_filehandle_validrw(struct vnode *vp, int mode,
struct ucred *cred, pid_t pid);
int fuse_filehandle_get(struct vnode *vp, int fflag,
struct fuse_filehandle **fufhp, struct ucred *cred,
pid_t pid);
int fuse_filehandle_get_anyflags(struct vnode *vp,
struct fuse_filehandle **fufhp, struct ucred *cred,
pid_t pid);
int fuse_filehandle_getrw(struct vnode *vp, int fflag,
struct fuse_filehandle **fufhp, struct ucred *cred,
pid_t pid);
void fuse_filehandle_init(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp, uint64_t fh_id);
int fuse_filehandle_open(struct vnode *vp, fufh_type_t fufh_type,
struct fuse_filehandle **fufhp, struct thread *td,
struct ucred *cred, struct fuse_open_out *foo);
int fuse_filehandle_open(struct vnode *vp, int mode,
struct fuse_filehandle **fufhp, struct thread *td,
struct ucred *cred);
int fuse_filehandle_close(struct vnode *vp, fufh_type_t fufh_type,
int fuse_filehandle_close(struct vnode *vp, struct fuse_filehandle *fufh,
struct thread *td, struct ucred *cred);
void fuse_file_init(void);
void fuse_file_destroy(void);
#endif /* _FUSE_FILE_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -61,6 +66,8 @@
#define _FUSE_INTERNAL_H_
#include <sys/types.h>
#include <sys/counter.h>
#include <sys/limits.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <sys/vnode.h>
@ -68,6 +75,9 @@
#include "fuse_ipc.h"
#include "fuse_node.h"
extern counter_u64_t fuse_lookup_cache_hits;
extern counter_u64_t fuse_lookup_cache_misses;
static inline bool
vfs_isrdonly(struct mount *mp)
{
@ -80,12 +90,6 @@ vnode_mount(struct vnode *vp)
return (vp->v_mount);
}
static inline bool
vnode_mountedhere(struct vnode *vp)
{
return (vp->v_mountedhere != NULL);
}
static inline enum vtype
vnode_vtype(struct vnode *vp)
{
@ -134,12 +138,6 @@ uio_setoffset(struct uio *uio, off_t offset)
uio->uio_offset = offset;
}
static inline void
uio_setresid(struct uio *uio, ssize_t resid)
{
uio->uio_resid = resid;
}
/* miscellaneous */
static inline bool
@ -156,25 +154,57 @@ fuse_iosize(struct vnode *vp)
return (vp->v_mount->mnt_stat.f_iosize);
}
/*
* Make a cacheable timeout in bintime format value based on a fuse_attr_out
* response
*/
static inline void
fuse_validity_2_bintime(uint64_t attr_valid, uint32_t attr_valid_nsec,
struct bintime *timeout)
{
struct timespec now, duration, timeout_ts;
getnanouptime(&now);
/* "+ 2" is the bound of attr_valid_nsec + now.tv_nsec */
/* Why oh why isn't there a TIME_MAX defined? */
if (attr_valid >= INT_MAX || attr_valid + now.tv_sec + 2 >= INT_MAX) {
timeout->sec = INT_MAX;
} else {
duration.tv_sec = attr_valid;
duration.tv_nsec = attr_valid_nsec;
timespecadd(&duration, &now, &timeout_ts);
timespec2bintime(&timeout_ts, timeout);
}
}
/*
* Make a cacheable timeout value in timespec format based on the fuse_entry_out
* response
*/
static inline void
fuse_validity_2_timespec(const struct fuse_entry_out *feo,
struct timespec *timeout)
{
struct timespec duration, now;
getnanouptime(&now);
/* "+ 2" is the bound of entry_valid_nsec + now.tv_nsec */
if (feo->entry_valid >= INT_MAX ||
feo->entry_valid + now.tv_sec + 2 >= INT_MAX) {
timeout->tv_sec = INT_MAX;
} else {
duration.tv_sec = feo->entry_valid;
duration.tv_nsec = feo->entry_valid_nsec;
timespecadd(&duration, &now, timeout);
}
}
/* VFS ops */
int
fuse_internal_get_cached_vnode(struct mount*, ino_t, int, struct vnode**);
/* access */
#define FVP_ACCESS_NOOP 0x01
#define FACCESS_VA_VALID 0x01
#define FACCESS_DO_ACCESS 0x02
#define FACCESS_STICKY 0x04
#define FACCESS_CHOWN 0x08
#define FACCESS_NOCHECKSPY 0x10
#define FACCESS_SETGID 0x12
#define FACCESS_XQUERIES (FACCESS_STICKY | FACCESS_CHOWN | FACCESS_SETGID)
struct fuse_access_param {
uid_t xuid;
gid_t xgid;
uint32_t facc_flags;
};
static inline int
fuse_match_cred(struct ucred *basecred, struct ucred *usercred)
{
@ -189,8 +219,8 @@ fuse_match_cred(struct ucred *basecred, struct ucred *usercred)
return (EPERM);
}
int fuse_internal_access(struct vnode *vp, mode_t mode,
struct fuse_access_param *facp, struct thread *td, struct ucred *cred);
int fuse_internal_access(struct vnode *vp, accmode_t mode,
struct thread *td, struct ucred *cred);
/* attributes */
void fuse_internal_cache_attrs(struct vnode *vp, struct fuse_attr *attr,
@ -198,20 +228,34 @@ void fuse_internal_cache_attrs(struct vnode *vp, struct fuse_attr *attr,
/* fsync */
int fuse_internal_fsync(struct vnode *vp, struct thread *td,
struct ucred *cred, struct fuse_filehandle *fufh);
int fuse_internal_fsync(struct vnode *vp, struct thread *td, int waitfor,
bool datasync);
int fuse_internal_fsync_callback(struct fuse_ticket *tick, struct uio *uio);
/* readdir */
/* getattr */
int fuse_internal_do_getattr(struct vnode *vp, struct vattr *vap,
struct ucred *cred, struct thread *td);
int fuse_internal_getattr(struct vnode *vp, struct vattr *vap,
struct ucred *cred, struct thread *td);
/* asynchronous invalidation */
int fuse_internal_invalidate_entry(struct mount *mp, struct uio *uio);
int fuse_internal_invalidate_inode(struct mount *mp, struct uio *uio);
/* mknod */
int fuse_internal_mknod(struct vnode *dvp, struct vnode **vpp,
struct componentname *cnp, struct vattr *vap);
/* readdir */
struct pseudo_dirent {
uint32_t d_namlen;
};
int fuse_internal_readdir(struct vnode *vp, struct uio *uio,
struct fuse_filehandle *fufh, struct fuse_iov *cookediov);
int fuse_internal_readdir_processdata(struct uio *uio, size_t reqsize,
void *buf, size_t bufsize, void *param);
int fuse_internal_readdir(struct vnode *vp, struct uio *uio, off_t startoff,
struct fuse_filehandle *fufh, struct fuse_iov *cookediov, int *ncookies,
u_long *cookies);
int fuse_internal_readdir_processdata(struct uio *uio, off_t startoff,
int *fnd_start, size_t reqsize, void *buf, size_t bufsize,
struct fuse_iov *cookediov, int *ncookies, u_long **cookiesp);
/* remove */
@ -227,6 +271,10 @@ int fuse_internal_rename(struct vnode *fdvp, struct componentname *fcnp,
void fuse_internal_vnode_disappear(struct vnode *vp);
/* setattr */
int fuse_internal_setattr(struct vnode *vp, struct vattr *va,
struct thread *td, struct ucred *cred);
/* strategy */
/* entity creation */
@ -271,4 +319,8 @@ void fuse_internal_forget_send(struct mount *mp, struct thread *td,
int fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio);
void fuse_internal_send_init(struct fuse_data *data, struct thread *td);
/* module load/unload */
void fuse_internal_init(void);
void fuse_internal_destroy(void);
#endif /* _FUSE_INTERNAL_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -61,7 +66,7 @@
#define _FUSE_IO_H_
int fuse_io_dispatch(struct vnode *vp, struct uio *uio, int ioflag,
struct ucred *cred);
struct ucred *cred, pid_t pid);
int fuse_io_strategy(struct vnode *vp, struct buf *bp);
int fuse_io_flushbuf(struct vnode *vp, int waitfor, struct thread *td);
int fuse_io_invalbuf(struct vnode *vp, struct thread *td);

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -61,6 +66,7 @@ __FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/counter.h>
#include <sys/errno.h>
#include <sys/kernel.h>
#include <sys/conf.h>
@ -84,14 +90,17 @@ __FBSDID("$FreeBSD$");
#include "fuse_ipc.h"
#include "fuse_internal.h"
SDT_PROVIDER_DECLARE(fuse);
SDT_PROVIDER_DECLARE(fusefs);
/*
* Fuse trace probe:
* arg0: verbosity. Higher numbers give more verbose messages
* arg1: Textual message
*/
SDT_PROBE_DEFINE2(fuse, , ipc, trace, "int", "char*");
SDT_PROBE_DEFINE2(fusefs, , ipc, trace, "int", "char*");
static void fdisp_make_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct fuse_data *data, uint64_t nid, pid_t pid, struct ucred *cred);
static void fuse_interrupt_send(struct fuse_ticket *otick, int err);
static struct fuse_ticket *fticket_alloc(struct fuse_data *data);
static void fticket_refresh(struct fuse_ticket *ftick);
static void fticket_destroy(struct fuse_ticket *ftick);
@ -104,13 +113,10 @@ static int fuse_body_audit(struct fuse_ticket *ftick, size_t blen);
static fuse_handler_t fuse_standard_handler;
SYSCTL_NODE(_vfs, OID_AUTO, fusefs, CTLFLAG_RW, 0, "FUSE tunables");
SYSCTL_STRING(_vfs_fusefs, OID_AUTO, version, CTLFLAG_RD,
FUSE_FREEBSD_VERSION, 0, "fuse-freebsd version");
static int fuse_ticket_count = 0;
static counter_u64_t fuse_ticket_count;
SYSCTL_COUNTER_U64(_vfs_fusefs_stats, OID_AUTO, ticket_count, CTLFLAG_RD,
&fuse_ticket_count, "Number of allocated tickets");
SYSCTL_INT(_vfs_fusefs, OID_AUTO, ticket_count, CTLFLAG_RW,
&fuse_ticket_count, 0, "number of allocated tickets");
static long fuse_iov_permanent_bufsize = 1 << 19;
SYSCTL_LONG(_vfs_fusefs, OID_AUTO, iov_permanent_bufsize, CTLFLAG_RW,
@ -125,25 +131,131 @@ SYSCTL_INT(_vfs_fusefs, OID_AUTO, iov_credit, CTLFLAG_RW,
MALLOC_DEFINE(M_FUSEMSG, "fuse_msgbuf", "fuse message buffer");
static uma_zone_t ticket_zone;
static void
fuse_block_sigs(sigset_t *oldset)
/*
* TODO: figure out how to timeout INTERRUPT requests, because the daemon may
* leagally never respond
*/
static int
fuse_interrupt_callback(struct fuse_ticket *tick, struct uio *uio)
{
sigset_t newset;
struct fuse_ticket *otick, *x_tick;
struct fuse_interrupt_in *fii;
struct fuse_data *data = tick->tk_data;
bool found = false;
SIGFILLSET(newset);
SIGDELSET(newset, SIGKILL);
if (kern_sigprocmask(curthread, SIG_BLOCK, &newset, oldset, 0))
panic("%s: Invalid operation for kern_sigprocmask()",
__func__);
fii = (struct fuse_interrupt_in*)((char*)tick->tk_ms_fiov.base +
sizeof(struct fuse_in_header));
fuse_lck_mtx_lock(data->aw_mtx);
TAILQ_FOREACH_SAFE(otick, &data->aw_head, tk_aw_link, x_tick) {
if (otick->tk_unique == fii->unique) {
found = true;
break;
}
}
fuse_lck_mtx_unlock(data->aw_mtx);
if (!found) {
/* Original is already complete. Just return */
return 0;
}
/* Clear the original ticket's interrupt association */
otick->irq_unique = 0;
if (tick->tk_aw_ohead.error == ENOSYS) {
fsess_set_notimpl(data->mp, FUSE_INTERRUPT);
return 0;
} else if (tick->tk_aw_ohead.error == EAGAIN) {
/*
* There are two reasons we might get this:
* 1) the daemon received the INTERRUPT request before the
* original, or
* 2) the daemon received the INTERRUPT request after it
* completed the original request.
* In the first case we should re-send the INTERRUPT. In the
* second, we should ignore it.
*/
/* Resend */
fuse_interrupt_send(otick, EINTR);
return 0;
} else {
/* Illegal FUSE_INTERRUPT response */
return EINVAL;
}
}
static void
fuse_restore_sigs(sigset_t *oldset)
/* Interrupt the operation otick. Return err as its error code */
void
fuse_interrupt_send(struct fuse_ticket *otick, int err)
{
struct fuse_dispatcher fdi;
struct fuse_interrupt_in *fii;
struct fuse_in_header *ftick_hdr;
struct fuse_data *data = otick->tk_data;
struct fuse_ticket *tick, *xtick;
struct ucred reused_creds;
gid_t reused_groups[1];
if (kern_sigprocmask(curthread, SIG_SETMASK, oldset, NULL, 0))
panic("%s: Invalid operation for kern_sigprocmask()",
__func__);
if (otick->irq_unique == 0) {
/*
* If the daemon hasn't yet received otick, then we can answer
* it ourselves and return.
*/
fuse_lck_mtx_lock(data->ms_mtx);
STAILQ_FOREACH_SAFE(tick, &otick->tk_data->ms_head, tk_ms_link,
xtick) {
if (tick == otick) {
STAILQ_REMOVE(&otick->tk_data->ms_head, tick,
fuse_ticket, tk_ms_link);
otick->tk_data->ms_count--;
otick->tk_ms_link.stqe_next = NULL;
fuse_lck_mtx_unlock(data->ms_mtx);
fuse_lck_mtx_lock(otick->tk_aw_mtx);
if (!fticket_answered(otick)) {
fticket_set_answered(otick);
otick->tk_aw_errno = err;
wakeup(otick);
}
fuse_lck_mtx_unlock(otick->tk_aw_mtx);
fuse_ticket_drop(tick);
return;
}
}
fuse_lck_mtx_unlock(data->ms_mtx);
/*
* If the fuse daemon doesn't support interrupts, then there's
* nothing more that we can do
*/
if (!fsess_isimpl(data->mp, FUSE_INTERRUPT))
return;
/*
* If the fuse daemon has already received otick, then we must
* send FUSE_INTERRUPT.
*/
ftick_hdr = fticket_in_header(otick);
reused_creds.cr_uid = ftick_hdr->uid;
reused_groups[0] = ftick_hdr->gid;
reused_creds.cr_groups = reused_groups;
fdisp_init(&fdi, sizeof(*fii));
fdisp_make_pid(&fdi, FUSE_INTERRUPT, data, ftick_hdr->nodeid,
ftick_hdr->pid, &reused_creds);
fii = fdi.indata;
fii->unique = otick->tk_unique;
fuse_insert_callback(fdi.tick, fuse_interrupt_callback);
otick->irq_unique = fdi.tick->tk_unique;
/* Interrupt ops should be delivered ASAP */
fuse_insert_message(fdi.tick, true);
fdisp_destroy(&fdi);
} else {
/* This ticket has already been interrupted */
}
}
void
@ -181,14 +293,19 @@ fiov_adjust(struct fuse_iov *fiov, size_t size)
}
fiov->allocated_size = FU_AT_LEAST(size);
fiov->credit = fuse_iov_credit;
/* Clear data buffer after reallocation */
bzero(fiov->base, size);
} else if (size > fiov->len) {
/* Clear newly extended portion of data buffer */
bzero((char*)fiov->base + fiov->len, size - fiov->len);
}
fiov->len = size;
}
/* Resize the fiov if needed, and clear it's buffer */
void
fiov_refresh(struct fuse_iov *fiov)
{
bzero(fiov->base, fiov->len);
fiov_adjust(fiov, 0);
}
@ -211,8 +328,10 @@ fticket_ctor(void *mem, int size, void *arg, int flags)
if (ftick->tk_unique == 0)
ftick->tk_unique = atomic_fetchadd_long(&data->ticketer, 1);
ftick->irq_unique = 0;
refcount_init(&ftick->tk_refcount, 1);
atomic_add_acq_int(&fuse_ticket_count, 1);
counter_u64_add(fuse_ticket_count, 1);
return 0;
}
@ -227,7 +346,7 @@ fticket_dtor(void *mem, int size, void *arg)
FUSE_ASSERT_MS_DONE(ftick);
FUSE_ASSERT_AW_DONE(ftick);
atomic_subtract_acq_int(&fuse_ticket_count, 1);
counter_u64_add(fuse_ticket_count, -1);
}
static int
@ -269,7 +388,7 @@ fticket_destroy(struct fuse_ticket *ftick)
return uma_zfree(ticket_zone, ftick);
}
static inline
static inline
void
fticket_refresh(struct fuse_ticket *ftick)
{
@ -292,30 +411,65 @@ fticket_refresh(struct fuse_ticket *ftick)
ftick->tk_flag = 0;
}
/* Prepar the ticket to be reused, but don't clear its data buffers */
static inline void
fticket_reset(struct fuse_ticket *ftick)
{
FUSE_ASSERT_MS_DONE(ftick);
FUSE_ASSERT_AW_DONE(ftick);
ftick->tk_ms_bufdata = NULL;
ftick->tk_ms_bufsize = 0;
ftick->tk_ms_type = FT_M_FIOV;
bzero(&ftick->tk_aw_ohead, sizeof(struct fuse_out_header));
ftick->tk_aw_errno = 0;
ftick->tk_aw_bufdata = NULL;
ftick->tk_aw_bufsize = 0;
ftick->tk_aw_type = FT_A_FIOV;
ftick->tk_flag = 0;
}
static int
fticket_wait_answer(struct fuse_ticket *ftick)
{
sigset_t tset;
int err = 0;
struct fuse_data *data;
struct thread *td = curthread;
sigset_t blockedset, oldset;
int err = 0, stops_deferred;
struct fuse_data *data = ftick->tk_data;
bool interrupted = false;
if (fsess_isimpl(ftick->tk_data->mp, FUSE_INTERRUPT) &&
data->dataflags & FSESS_INTR) {
SIGEMPTYSET(blockedset);
} else {
/* Block all signals except (implicitly) SIGKILL */
SIGFILLSET(blockedset);
}
stops_deferred = sigdeferstop(SIGDEFERSTOP_SILENT);
kern_sigprocmask(td, SIG_BLOCK, NULL, &oldset, 0);
fuse_lck_mtx_lock(ftick->tk_aw_mtx);
retry:
if (fticket_answered(ftick)) {
goto out;
}
data = ftick->tk_data;
if (fdata_get_dead(data)) {
err = ENOTCONN;
fticket_set_answered(ftick);
goto out;
}
fuse_block_sigs(&tset);
kern_sigprocmask(td, SIG_BLOCK, &blockedset, NULL, 0);
err = msleep(ftick, &ftick->tk_aw_mtx, PCATCH, "fu_ans",
data->daemon_timeout * hz);
fuse_restore_sigs(&tset);
if (err == EAGAIN) { /* same as EWOULDBLOCK */
kern_sigprocmask(td, SIG_SETMASK, &oldset, NULL, 0);
if (err == EWOULDBLOCK) {
SDT_PROBE2(fusefs, , ipc, trace, 3,
"fticket_wait_answer: EWOULDBLOCK");
#ifdef XXXIP /* die conditionally */
if (!fdata_get_dead(data)) {
fdata_set_dead(data);
@ -323,14 +477,64 @@ fticket_wait_answer(struct fuse_ticket *ftick)
#endif
err = ETIMEDOUT;
fticket_set_answered(ftick);
} else if ((err == EINTR || err == ERESTART)) {
/*
* Whether we get EINTR or ERESTART depends on whether
* SA_RESTART was set by sigaction(2).
*
* Try to interrupt the operation and wait for an EINTR response
* to the original operation. If the file system does not
* support FUSE_INTERRUPT, then we'll just wait for it to
* complete like normal. If it does support FUSE_INTERRUPT,
* then it will either respond EINTR to the original operation,
* or EAGAIN to the interrupt.
*/
sigset_t tmpset;
SDT_PROBE2(fusefs, , ipc, trace, 4,
"fticket_wait_answer: interrupt");
fuse_lck_mtx_unlock(ftick->tk_aw_mtx);
fuse_interrupt_send(ftick, err);
PROC_LOCK(td->td_proc);
mtx_lock(&td->td_proc->p_sigacts->ps_mtx);
tmpset = td->td_proc->p_siglist;
SIGSETOR(tmpset, td->td_siglist);
mtx_unlock(&td->td_proc->p_sigacts->ps_mtx);
PROC_UNLOCK(td->td_proc);
fuse_lck_mtx_lock(ftick->tk_aw_mtx);
if (!interrupted && !SIGISMEMBER(tmpset, SIGKILL)) {
/*
* Block all signals while we wait for an interrupt
* response. The protocol doesn't discriminate between
* different signals.
*/
SIGFILLSET(blockedset);
interrupted = true;
goto retry;
} else {
/*
* Return immediately for fatal signals, or if this is
* the second interruption. We should only be
* interrupted twice if the thread is stopped, for
* example during sigexit.
*/
}
} else if (err) {
SDT_PROBE2(fusefs, , ipc, trace, 6,
"fticket_wait_answer: other error");
} else {
SDT_PROBE2(fusefs, , ipc, trace, 7, "fticket_wait_answer: OK");
}
out:
if (!(err || fticket_answered(ftick))) {
SDT_PROBE2(fuse, , ipc, trace, 1,
SDT_PROBE2(fusefs, , ipc, trace, 1,
"FUSE: requester was woken up but still no answer");
err = ENXIO;
}
fuse_lck_mtx_unlock(ftick->tk_aw_mtx);
sigallowstop(stops_deferred);
return err;
}
@ -386,6 +590,8 @@ fdata_alloc(struct cdev *fdev, struct ucred *cred)
data->fdev = fdev;
mtx_init(&data->ms_mtx, "fuse message list mutex", NULL, MTX_DEF);
STAILQ_INIT(&data->ms_head);
data->ms_count = 0;
knlist_init_mtx(&data->ks_rsel.si_note, &data->ms_mtx);
mtx_init(&data->aw_mtx, "fuse answer list mutex", NULL, MTX_DEF);
TAILQ_INIT(&data->aw_head);
data->daemoncred = crhold(cred);
@ -405,11 +611,12 @@ fdata_trydestroy(struct fuse_data *data)
return;
/* Driving off stage all that stuff thrown at device... */
mtx_destroy(&data->ms_mtx);
mtx_destroy(&data->aw_mtx);
sx_destroy(&data->rename_lock);
crfree(data->daemoncred);
mtx_destroy(&data->aw_mtx);
knlist_delete(&data->ks_rsel.si_note, curthread, 0);
knlist_destroy(&data->ks_rsel.si_note);
mtx_destroy(&data->ms_mtx);
free(data, M_FUSEMSG);
}
@ -478,8 +685,14 @@ fuse_insert_callback(struct fuse_ticket *ftick, fuse_handler_t * handler)
fuse_lck_mtx_unlock(ftick->tk_data->aw_mtx);
}
/*
* Insert a new upgoing ticket into the message queue
*
* If urgent is true, insert at the front of the queue. Otherwise, insert in
* FIFO order.
*/
void
fuse_insert_message(struct fuse_ticket *ftick)
fuse_insert_message(struct fuse_ticket *ftick, bool urgent)
{
if (ftick->tk_flag & FT_DIRTY) {
panic("FUSE: ticket reused without being refreshed");
@ -490,9 +703,13 @@ fuse_insert_message(struct fuse_ticket *ftick)
return;
}
fuse_lck_mtx_lock(ftick->tk_data->ms_mtx);
fuse_ms_push(ftick);
if (urgent)
fuse_ms_push_head(ftick);
else
fuse_ms_push(ftick);
wakeup_one(ftick->tk_data);
selwakeuppri(&ftick->tk_data->ks_rsel, PZERO + 1);
KNOTE_LOCKED(&ftick->tk_data->ks_rsel.si_note, 0);
fuse_lck_mtx_unlock(ftick->tk_data->ms_mtx);
}
@ -505,8 +722,21 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
opcode = fticket_opcode(ftick);
switch (opcode) {
case FUSE_BMAP:
err = (blen == sizeof(struct fuse_bmap_out)) ? 0 : EINVAL;
break;
case FUSE_LINK:
case FUSE_LOOKUP:
err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL;
case FUSE_MKDIR:
case FUSE_MKNOD:
case FUSE_SYMLINK:
if (fuse_libabi_geq(ftick->tk_data, 7, 9)) {
err = (blen == sizeof(struct fuse_entry_out)) ?
0 : EINVAL;
} else {
err = (blen == FUSE_COMPAT_ENTRY_OUT_SIZE) ? 0 : EINVAL;
}
break;
case FUSE_FORGET:
@ -514,29 +744,19 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
break;
case FUSE_GETATTR:
err = (blen == sizeof(struct fuse_attr_out)) ? 0 : EINVAL;
break;
case FUSE_SETATTR:
err = (blen == sizeof(struct fuse_attr_out)) ? 0 : EINVAL;
if (fuse_libabi_geq(ftick->tk_data, 7, 9)) {
err = (blen == sizeof(struct fuse_attr_out)) ?
0 : EINVAL;
} else {
err = (blen == FUSE_COMPAT_ATTR_OUT_SIZE) ? 0 : EINVAL;
}
break;
case FUSE_READLINK:
err = (PAGE_SIZE >= blen) ? 0 : EINVAL;
break;
case FUSE_SYMLINK:
err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL;
break;
case FUSE_MKNOD:
err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL;
break;
case FUSE_MKDIR:
err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL;
break;
case FUSE_UNLINK:
err = (blen == 0) ? 0 : EINVAL;
break;
@ -549,10 +769,6 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
err = (blen == 0) ? 0 : EINVAL;
break;
case FUSE_LINK:
err = (blen == sizeof(struct fuse_entry_out)) ? 0 : EINVAL;
break;
case FUSE_OPEN:
err = (blen == sizeof(struct fuse_open_out)) ? 0 : EINVAL;
break;
@ -607,7 +823,9 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
break;
case FUSE_INIT:
if (blen == sizeof(struct fuse_init_out) || blen == 8) {
if (blen == sizeof(struct fuse_init_out) ||
blen == FUSE_COMPAT_INIT_OUT_SIZE ||
blen == FUSE_COMPAT_22_INIT_OUT_SIZE) {
err = 0;
} else {
err = EINVAL;
@ -634,15 +852,15 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
break;
case FUSE_GETLK:
panic("FUSE: no response body format check for FUSE_GETLK");
err = (blen == sizeof(struct fuse_lk_out)) ? 0 : EINVAL;
break;
case FUSE_SETLK:
panic("FUSE: no response body format check for FUSE_SETLK");
err = (blen == 0) ? 0 : EINVAL;
break;
case FUSE_SETLKW:
panic("FUSE: no response body format check for FUSE_SETLKW");
err = (blen == 0) ? 0 : EINVAL;
break;
case FUSE_ACCESS:
@ -650,8 +868,13 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen)
break;
case FUSE_CREATE:
err = (blen == sizeof(struct fuse_entry_out) +
sizeof(struct fuse_open_out)) ? 0 : EINVAL;
if (fuse_libabi_geq(ftick->tk_data, 7, 9)) {
err = (blen == sizeof(struct fuse_entry_out) +
sizeof(struct fuse_open_out)) ? 0 : EINVAL;
} else {
err = (blen == FUSE_COMPAT_ENTRY_OUT_SIZE +
sizeof(struct fuse_open_out)) ? 0 : EINVAL;
}
break;
case FUSE_DESTROY:
@ -677,7 +900,7 @@ fuse_setup_ihead(struct fuse_in_header *ihead, struct fuse_ticket *ftick,
ihead->pid = pid;
ihead->uid = cred->cr_uid;
ihead->gid = cred->cr_rgid;
ihead->gid = cred->cr_groups[0];
}
/*
@ -705,18 +928,38 @@ fuse_standard_handler(struct fuse_ticket *ftick, struct uio *uio)
return err;
}
void
fdisp_make_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op,
/*
* Reinitialize a dispatcher from a pid and node id, without resizing or
* clearing its data buffers
*/
static void
fdisp_refresh_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct mount *mp, uint64_t nid, pid_t pid, struct ucred *cred)
{
struct fuse_data *data = fuse_get_mpdata(mp);
MPASS(fdip->tick);
MPASS2(sizeof(fdip->finh) + fdip->iosize <= fdip->tick->tk_ms_fiov.len,
"Must use fdisp_make_pid to increase the size of the fiov");
fticket_reset(fdip->tick);
FUSE_DIMALLOC(&fdip->tick->tk_ms_fiov, fdip->finh,
fdip->indata, fdip->iosize);
fuse_setup_ihead(fdip->finh, fdip->tick, nid, op, fdip->iosize, pid,
cred);
}
/* Initialize a dispatcher from a pid and node id */
static void
fdisp_make_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct fuse_data *data, uint64_t nid, pid_t pid, struct ucred *cred)
{
if (fdip->tick) {
fticket_refresh(fdip->tick);
} else {
fdip->tick = fuse_ticket_fetch(data);
}
/* FUSE_DIMALLOC will bzero the fiovs when it enlarges them */
FUSE_DIMALLOC(&fdip->tick->tk_ms_fiov, fdip->finh,
fdip->indata, fdip->iosize);
@ -727,21 +970,41 @@ void
fdisp_make(struct fuse_dispatcher *fdip, enum fuse_opcode op, struct mount *mp,
uint64_t nid, struct thread *td, struct ucred *cred)
{
struct fuse_data *data = fuse_get_mpdata(mp);
RECTIFY_TDCR(td, cred);
return fdisp_make_pid(fdip, op, mp, nid, td->td_proc->p_pid, cred);
return fdisp_make_pid(fdip, op, data, nid, td->td_proc->p_pid, cred);
}
void
fdisp_make_vp(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct vnode *vp, struct thread *td, struct ucred *cred)
{
struct mount *mp = vnode_mount(vp);
struct fuse_data *data = fuse_get_mpdata(mp);
RECTIFY_TDCR(td, cred);
return fdisp_make_pid(fdip, op, vnode_mount(vp), VTOI(vp),
return fdisp_make_pid(fdip, op, data, VTOI(vp),
td->td_proc->p_pid, cred);
}
SDT_PROBE_DEFINE2(fuse, , ipc, fdisp_wait_answ_error, "char*", "int");
/* Refresh a fuse_dispatcher so it can be reused, but don't zero its data */
void
fdisp_refresh_vp(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct vnode *vp, struct thread *td, struct ucred *cred)
{
RECTIFY_TDCR(td, cred);
return fdisp_refresh_pid(fdip, op, vnode_mount(vp), VTOI(vp),
td->td_proc->p_pid, cred);
}
void
fdisp_refresh(struct fuse_dispatcher *fdip)
{
fticket_refresh(fdip->tick);
}
SDT_PROBE_DEFINE2(fusefs, , ipc, fdisp_wait_answ_error, "char*", "int");
int
fdisp_wait_answ(struct fuse_dispatcher *fdip)
@ -750,7 +1013,7 @@ fdisp_wait_answ(struct fuse_dispatcher *fdip)
fdip->answ_stat = 0;
fuse_insert_callback(fdip->tick, fuse_standard_handler);
fuse_insert_message(fdip->tick);
fuse_insert_message(fdip->tick, false);
if ((err = fticket_wait_answer(fdip->tick))) {
fuse_lck_mtx_lock(fdip->tick->tk_aw_mtx);
@ -761,7 +1024,7 @@ fdisp_wait_answ(struct fuse_dispatcher *fdip)
* the standard handler has completed his job.
* So we drop the ticket and exit as usual.
*/
SDT_PROBE2(fuse, , ipc, fdisp_wait_answ_error,
SDT_PROBE2(fusefs, , ipc, fdisp_wait_answ_error,
"IPC: interrupted, already answered", err);
fuse_lck_mtx_unlock(fdip->tick->tk_aw_mtx);
goto out;
@ -771,7 +1034,7 @@ fdisp_wait_answ(struct fuse_dispatcher *fdip)
* Then by setting the answered flag we get *him*
* to drop the ticket.
*/
SDT_PROBE2(fuse, , ipc, fdisp_wait_answ_error,
SDT_PROBE2(fusefs, , ipc, fdisp_wait_answ_error,
"IPC: interrupted, setting to answered", err);
fticket_set_answered(fdip->tick);
fuse_lck_mtx_unlock(fdip->tick->tk_aw_mtx);
@ -779,14 +1042,22 @@ fdisp_wait_answ(struct fuse_dispatcher *fdip)
}
}
if (fdip->tick->tk_aw_errno) {
SDT_PROBE2(fuse, , ipc, fdisp_wait_answ_error,
if (fdip->tick->tk_aw_errno == ENOTCONN) {
/* The daemon died while we were waiting for a response */
err = ENOTCONN;
goto out;
} else if (fdip->tick->tk_aw_errno) {
/*
* There was some sort of communication error with the daemon
* that the client wouldn't understand.
*/
SDT_PROBE2(fusefs, , ipc, fdisp_wait_answ_error,
"IPC: explicit EIO-ing", fdip->tick->tk_aw_errno);
err = EIO;
goto out;
}
if ((err = fdip->tick->tk_aw_ohead.error)) {
SDT_PROBE2(fuse, , ipc, fdisp_wait_answ_error,
SDT_PROBE2(fusefs, , ipc, fdisp_wait_answ_error,
"IPC: setting status", fdip->tick->tk_aw_ohead.error);
/*
* This means a "proper" fuse syscall error.
@ -815,10 +1086,12 @@ fuse_ipc_init(void)
ticket_zone = uma_zcreate("fuse_ticket", sizeof(struct fuse_ticket),
fticket_ctor, fticket_dtor, fticket_init, fticket_fini,
UMA_ALIGN_PTR, 0);
fuse_ticket_count = counter_u64_alloc(M_WAITOK);
}
void
fuse_ipc_destroy(void)
{
counter_u64_free(fuse_ticket_count);
uma_zdestroy(ticket_zone);
}

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -63,6 +68,12 @@
#include <sys/param.h>
#include <sys/refcount.h>
enum fuse_data_cache_mode {
FUSE_CACHE_UC,
FUSE_CACHE_WT,
FUSE_CACHE_WB,
};
struct fuse_iov {
void *base;
size_t len;
@ -103,6 +114,12 @@ struct fuse_ticket {
struct fuse_data *tk_data;
int tk_flag;
u_int tk_refcount;
/*
* If this ticket's operation has been interrupted, this will hold the
* unique value of the FUSE_INTERRUPT operation. Otherwise, it will be
* 0.
*/
uint64_t irq_unique;
/* fields for initiating an upgoing message */
struct fuse_iov tk_ms_fiov;
@ -147,16 +164,20 @@ fticket_set_answered(struct fuse_ticket *ftick)
ftick->tk_flag |= FT_ANSW;
}
static inline struct fuse_in_header*
fticket_in_header(struct fuse_ticket *ftick)
{
return (struct fuse_in_header *)(ftick->tk_ms_fiov.base);
}
static inline enum fuse_opcode
fticket_opcode(struct fuse_ticket *ftick)
{
return (((struct fuse_in_header *)(ftick->tk_ms_fiov.base))->opcode);
return fticket_in_header(ftick)->opcode;
}
int fticket_pull(struct fuse_ticket *ftick, struct uio *uio);
enum mountpri { FM_NOMOUNTED, FM_PRIMARY, FM_SECONDARY };
/*
* The data representing a FUSE session.
*/
@ -170,10 +191,16 @@ struct fuse_data {
struct mtx ms_mtx;
STAILQ_HEAD(, fuse_ticket) ms_head;
int ms_count;
struct mtx aw_mtx;
TAILQ_HEAD(, fuse_ticket) aw_head;
/*
* Holds the next value of the FUSE operation unique value.
* Also, serves as a wakeup channel to prevent any operations from
* being created before INIT completes.
*/
u_long ticketer;
struct sx rename_lock;
@ -181,6 +208,7 @@ struct fuse_data {
uint32_t fuse_libabi_major;
uint32_t fuse_libabi_minor;
uint32_t max_readahead_blocks;
uint32_t max_write;
uint32_t max_read;
uint32_t subtype;
@ -189,34 +217,27 @@ struct fuse_data {
struct selinfo ks_rsel;
int daemon_timeout;
unsigned time_gran;
uint64_t notimpl;
uint64_t mnt_flag;
enum fuse_data_cache_mode cache_mode;
};
#define FSESS_DEAD 0x0001 /* session is to be closed */
#define FSESS_UNUSED0 0x0002 /* unused */
#define FSESS_INITED 0x0004 /* session has been inited */
#define FSESS_DAEMON_CAN_SPY 0x0010 /* let non-owners access this fs */
/* (and being observed by the daemon) */
#define FSESS_PUSH_SYMLINKS_IN 0x0020 /* prefix absolute symlinks with mp */
#define FSESS_DEFAULT_PERMISSIONS 0x0040 /* kernel does permission checking */
#define FSESS_NO_ATTRCACHE 0x0080 /* no attribute caching */
#define FSESS_NO_READAHEAD 0x0100 /* no readaheads */
#define FSESS_NO_DATACACHE 0x0200 /* disable buffer cache */
#define FSESS_NO_NAMECACHE 0x0400 /* disable name cache */
#define FSESS_NO_MMAP 0x0800 /* disable mmap */
#define FSESS_BROKENIO 0x1000 /* fix broken io */
enum fuse_data_cache_mode {
FUSE_CACHE_UC,
FUSE_CACHE_WT,
FUSE_CACHE_WB,
};
#define FSESS_ASYNC_READ 0x1000 /* allow multiple reads of some file */
#define FSESS_POSIX_LOCKS 0x2000 /* daemon supports POSIX locks */
#define FSESS_EXPORT_SUPPORT 0x10000 /* daemon supports NFS-style lookups */
#define FSESS_INTR 0x20000 /* interruptible mounts */
#define FSESS_MNTOPTS_MASK ( \
FSESS_DAEMON_CAN_SPY | FSESS_PUSH_SYMLINKS_IN | \
FSESS_DEFAULT_PERMISSIONS | FSESS_INTR)
extern int fuse_data_cache_mode;
extern int fuse_data_cache_invalidate;
extern int fuse_mmap_enable;
extern int fuse_sync_resize;
extern int fuse_fix_broken_io;
static inline struct fuse_data *
fuse_get_mpdata(struct mount *mp)
@ -245,34 +266,41 @@ fsess_opt_datacache(struct mount *mp)
{
struct fuse_data *data = fuse_get_mpdata(mp);
return (fuse_data_cache_mode != FUSE_CACHE_UC &&
(data->dataflags & FSESS_NO_DATACACHE) == 0);
return (data->cache_mode != FUSE_CACHE_UC);
}
static inline bool
fsess_opt_mmap(struct mount *mp)
{
struct fuse_data *data = fuse_get_mpdata(mp);
if (!fuse_mmap_enable || fuse_data_cache_mode == FUSE_CACHE_UC)
return (false);
return ((data->dataflags & (FSESS_NO_DATACACHE | FSESS_NO_MMAP)) == 0);
return (fsess_opt_datacache(mp));
}
static inline bool
fsess_opt_brokenio(struct mount *mp)
fsess_opt_writeback(struct mount *mp)
{
struct fuse_data *data = fuse_get_mpdata(mp);
return (fuse_fix_broken_io || (data->dataflags & FSESS_BROKENIO));
return (data->cache_mode == FUSE_CACHE_WB);
}
/* Insert a new upgoing message */
static inline void
fuse_ms_push(struct fuse_ticket *ftick)
{
mtx_assert(&ftick->tk_data->ms_mtx, MA_OWNED);
refcount_acquire(&ftick->tk_refcount);
STAILQ_INSERT_TAIL(&ftick->tk_data->ms_head, ftick, tk_ms_link);
ftick->tk_data->ms_count++;
}
/* Insert a new upgoing message to the front of the queue */
static inline void
fuse_ms_push_head(struct fuse_ticket *ftick)
{
mtx_assert(&ftick->tk_data->ms_mtx, MA_OWNED);
refcount_acquire(&ftick->tk_refcount);
STAILQ_INSERT_HEAD(&ftick->tk_data->ms_head, ftick, tk_ms_link);
ftick->tk_data->ms_count++;
}
static inline struct fuse_ticket *
@ -284,7 +312,9 @@ fuse_ms_pop(struct fuse_data *data)
if ((ftick = STAILQ_FIRST(&data->ms_head))) {
STAILQ_REMOVE_HEAD(&data->ms_head, tk_ms_link);
data->ms_count--;
#ifdef INVARIANTS
MPASS(data->ms_count >= 0);
ftick->tk_ms_link.stqe_next = NULL;
#endif
}
@ -327,7 +357,7 @@ fuse_aw_pop(struct fuse_data *data)
struct fuse_ticket *fuse_ticket_fetch(struct fuse_data *data);
int fuse_ticket_drop(struct fuse_ticket *ftick);
void fuse_insert_callback(struct fuse_ticket *ftick, fuse_handler_t *handler);
void fuse_insert_message(struct fuse_ticket *ftick);
void fuse_insert_message(struct fuse_ticket *ftick, bool irq);
static inline bool
fuse_libabi_geq(struct fuse_data *data, uint32_t abi_maj, uint32_t abi_min)
@ -374,15 +404,17 @@ fdisp_destroy(struct fuse_dispatcher *fdisp)
#endif
}
void fdisp_refresh(struct fuse_dispatcher *fdip);
void fdisp_make(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct mount *mp, uint64_t nid, struct thread *td, struct ucred *cred);
void fdisp_make_pid(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct mount *mp, uint64_t nid, pid_t pid, struct ucred *cred);
void fdisp_make_vp(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct vnode *vp, struct thread *td, struct ucred *cred);
void fdisp_refresh_vp(struct fuse_dispatcher *fdip, enum fuse_opcode op,
struct vnode *vp, struct thread *td, struct ucred *cred);
int fdisp_wait_answ(struct fuse_dispatcher *fdip);
static inline int

View File

@ -1,6 +1,6 @@
/*--
* This file defines the kernel interface of FUSE
* Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
* Copyright (C) 2001-2008 Miklos Szeredi <miklos@szeredi.hu>
*
* This program can be distributed under the terms of the GNU GPL.
* See the file COPYING.
@ -34,69 +34,134 @@
* $FreeBSD$
*/
#ifndef linux
#include <sys/types.h>
#define __u64 uint64_t
#define __u32 uint32_t
#define __s32 int32_t
/*
* This file defines the kernel interface of FUSE
*
* Protocol changelog:
*
* 7.9:
* - new fuse_getattr_in input argument of GETATTR
* - add lk_flags in fuse_lk_in
* - add lock_owner field to fuse_setattr_in, fuse_read_in and fuse_write_in
* - add blksize field to fuse_attr
* - add file flags field to fuse_read_in and fuse_write_in
*
* 7.10
* - add nonseekable open flag
*
* 7.11
* - add IOCTL message
* - add unsolicited notification support
*
* 7.12
* - add umask flag to input argument of open, mknod and mkdir
* - add notification messages for invalidation of inodes and
* directory entries
*
* 7.13
* - make max number of background requests and congestion threshold
* tunables
*
* 7.14
* - add splice support to fuse device
*
* 7.15
* - add store notify
* - add retrieve notify
*
* 7.16
* - add BATCH_FORGET request
* - FUSE_IOCTL_UNRESTRICTED shall now return with array of 'struct
* fuse_ioctl_iovec' instead of ambiguous 'struct iovec'
* - add FUSE_IOCTL_32BIT flag
*
* 7.17
* - add FUSE_FLOCK_LOCKS and FUSE_RELEASE_FLOCK_UNLOCK
*
* 7.18
* - add FUSE_IOCTL_DIR flag
* - add FUSE_NOTIFY_DELETE
*
* 7.19
* - add FUSE_FALLOCATE
*
* 7.20
* - add FUSE_AUTO_INVAL_DATA
* 7.21
* - add FUSE_READDIRPLUS
* - send the requested events in POLL request
*
* 7.22
* - add FUSE_ASYNC_DIO
*
* 7.23
* - add FUSE_WRITEBACK_CACHE
* - add time_gran to fuse_init_out
* - add reserved space to fuse_init_out
* - add FATTR_CTIME
* - add ctime and ctimensec to fuse_setattr_in
* - add FUSE_RENAME2 request
* - add FUSE_NO_OPEN_SUPPORT flag
*/
#ifndef _FUSE_FUSE_KERNEL_H
#define _FUSE_FUSE_KERNEL_H
#ifdef __linux__
#include <linux/types.h>
#else
#include <asm/types.h>
#include <linux/major.h>
#include <sys/types.h>
#endif
/** Version number of this interface */
#define FUSE_KERNEL_VERSION 7
/** Minor version number of this interface */
#define FUSE_KERNEL_MINOR_VERSION 8
#define FUSE_KERNEL_MINOR_VERSION 23
/** The node ID of the root inode */
#define FUSE_ROOT_ID 1
/** The major number of the fuse character device */
#define FUSE_MAJOR MISC_MAJOR
/** The minor number of the fuse character device */
#define FUSE_MINOR 229
/* Make sure all structures are padded to 64bit boundary, so 32bit
userspace works under 64bit kernels */
struct fuse_attr {
__u64 ino;
__u64 size;
__u64 blocks;
__u64 atime;
__u64 mtime;
__u64 ctime;
__u32 atimensec;
__u32 mtimensec;
__u32 ctimensec;
__u32 mode;
__u32 nlink;
__u32 uid;
__u32 gid;
__u32 rdev;
uint64_t ino;
uint64_t size;
uint64_t blocks;
uint64_t atime;
uint64_t mtime;
uint64_t ctime;
uint32_t atimensec;
uint32_t mtimensec;
uint32_t ctimensec;
uint32_t mode;
uint32_t nlink;
uint32_t uid;
uint32_t gid;
uint32_t rdev;
uint32_t blksize;
uint32_t padding;
};
struct fuse_kstatfs {
__u64 blocks;
__u64 bfree;
__u64 bavail;
__u64 files;
__u64 ffree;
__u32 bsize;
__u32 namelen;
__u32 frsize;
__u32 padding;
__u32 spare[6];
uint64_t blocks;
uint64_t bfree;
uint64_t bavail;
uint64_t files;
uint64_t ffree;
uint32_t bsize;
uint32_t namelen;
uint32_t frsize;
uint32_t padding;
uint32_t spare[6];
};
struct fuse_file_lock {
__u64 start;
__u64 end;
__u32 type;
__u32 pid; /* tgid */
uint64_t start;
uint64_t end;
uint32_t type;
uint32_t pid; /* tgid */
};
/**
@ -109,26 +174,127 @@ struct fuse_file_lock {
#define FATTR_ATIME (1 << 4)
#define FATTR_MTIME (1 << 5)
#define FATTR_FH (1 << 6)
#define FATTR_ATIME_NOW (1 << 7)
#define FATTR_MTIME_NOW (1 << 8)
#define FATTR_LOCKOWNER (1 << 9)
#define FATTR_CTIME (1 << 10)
/**
* Flags returned by the OPEN request
*
* FOPEN_DIRECT_IO: bypass page cache for this open file
* FOPEN_KEEP_CACHE: don't invalidate the data cache on open
* FOPEN_NONSEEKABLE: the file is not seekable
*/
#define FOPEN_DIRECT_IO (1 << 0)
#define FOPEN_KEEP_CACHE (1 << 1)
#define FOPEN_NONSEEKABLE (1 << 2)
/**
* INIT request/reply flags
*
* FUSE_ASYNC_READ: asynchronous read requests
* FUSE_POSIX_LOCKS: remote locking for POSIX file locks
* FUSE_FILE_OPS: kernel sends file handle for fstat, etc... (not yet supported)
* FUSE_ATOMIC_O_TRUNC: handles the O_TRUNC open flag in the filesystem
* FUSE_EXPORT_SUPPORT: filesystem handles lookups of "." and ".."
* FUSE_BIG_WRITES: filesystem can handle write size larger than 4kB
* FUSE_DONT_MASK: don't apply umask to file mode on create operations
* FUSE_SPLICE_WRITE: kernel supports splice write on the device
* FUSE_SPLICE_MOVE: kernel supports splice move on the device
* FUSE_SPLICE_READ: kernel supports splice read on the device
* FUSE_FLOCK_LOCKS: remote locking for BSD style file locks
* FUSE_HAS_IOCTL_DIR: kernel supports ioctl on directories
* FUSE_AUTO_INVAL_DATA: automatically invalidate cached pages
* FUSE_DO_READDIRPLUS: do READDIRPLUS (READDIR+LOOKUP in one)
* FUSE_READDIRPLUS_AUTO: adaptive readdirplus
* FUSE_ASYNC_DIO: asynchronous direct I/O submission
* FUSE_WRITEBACK_CACHE: use writeback cache for buffered writes
* FUSE_NO_OPEN_SUPPORT: kernel supports zero-message opens
*/
#define FUSE_ASYNC_READ (1 << 0)
#define FUSE_POSIX_LOCKS (1 << 1)
#define FUSE_FILE_OPS (1 << 2)
#define FUSE_ATOMIC_O_TRUNC (1 << 3)
#define FUSE_EXPORT_SUPPORT (1 << 4)
#define FUSE_BIG_WRITES (1 << 5)
#define FUSE_DONT_MASK (1 << 6)
#define FUSE_SPLICE_WRITE (1 << 7)
#define FUSE_SPLICE_MOVE (1 << 8)
#define FUSE_SPLICE_READ (1 << 9)
#define FUSE_FLOCK_LOCKS (1 << 10)
#define FUSE_HAS_IOCTL_DIR (1 << 11)
#define FUSE_AUTO_INVAL_DATA (1 << 12)
#define FUSE_DO_READDIRPLUS (1 << 13)
#define FUSE_READDIRPLUS_AUTO (1 << 14)
#define FUSE_ASYNC_DIO (1 << 15)
#define FUSE_WRITEBACK_CACHE (1 << 16)
#define FUSE_NO_OPEN_SUPPORT (1 << 17)
#ifdef linux
/**
* CUSE INIT request/reply flags
*
* CUSE_UNRESTRICTED_IOCTL: use unrestricted ioctl
*/
#define CUSE_UNRESTRICTED_IOCTL (1 << 0)
#endif /* linux */
/**
* Release flags
*/
#define FUSE_RELEASE_FLUSH (1 << 0)
#define FUSE_RELEASE_FLOCK_UNLOCK (1 << 1)
/**
* Getattr flags
*/
#define FUSE_GETATTR_FH (1 << 0)
/**
* Lock flags
*/
#define FUSE_LK_FLOCK (1 << 0)
/**
* WRITE flags
*
* FUSE_WRITE_CACHE: delayed write from page cache, file handle is guessed
* FUSE_WRITE_LOCKOWNER: lock_owner field is valid
*/
#define FUSE_WRITE_CACHE (1 << 0)
#define FUSE_WRITE_LOCKOWNER (1 << 1)
/**
* Read flags
*/
#define FUSE_READ_LOCKOWNER (1 << 1)
/**
* Ioctl flags
*
* FUSE_IOCTL_COMPAT: 32bit compat ioctl on 64bit machine
* FUSE_IOCTL_UNRESTRICTED: not restricted to well-formed ioctls, retry allowed
* FUSE_IOCTL_RETRY: retry with new iovecs
* FUSE_IOCTL_32BIT: 32bit ioctl
* FUSE_IOCTL_DIR: is a directory
*
* FUSE_IOCTL_MAX_IOV: maximum of in_iovecs + out_iovecs
*/
#define FUSE_IOCTL_COMPAT (1 << 0)
#define FUSE_IOCTL_UNRESTRICTED (1 << 1)
#define FUSE_IOCTL_RETRY (1 << 2)
#define FUSE_IOCTL_32BIT (1 << 3)
#define FUSE_IOCTL_DIR (1 << 4)
#define FUSE_IOCTL_MAX_IOV 256
/**
* Poll flags
*
* FUSE_POLL_SCHEDULE_NOTIFY: request poll notify
*/
#define FUSE_POLL_SCHEDULE_NOTIFY (1 << 0)
enum fuse_opcode {
FUSE_LOOKUP = 1,
@ -167,107 +333,179 @@ enum fuse_opcode {
FUSE_INTERRUPT = 36,
FUSE_BMAP = 37,
FUSE_DESTROY = 38,
FUSE_IOCTL = 39,
FUSE_POLL = 40,
FUSE_NOTIFY_REPLY = 41,
FUSE_BATCH_FORGET = 42,
FUSE_FALLOCATE = 43,
FUSE_READDIRPLUS = 44,
FUSE_RENAME2 = 45,
#ifdef linux
/* CUSE specific operations */
CUSE_INIT = 4096,
#endif /* linux */
};
enum fuse_notify_code {
FUSE_NOTIFY_POLL = 1,
FUSE_NOTIFY_INVAL_INODE = 2,
FUSE_NOTIFY_INVAL_ENTRY = 3,
FUSE_NOTIFY_STORE = 4,
FUSE_NOTIFY_RETRIEVE = 5,
FUSE_NOTIFY_DELETE = 6,
FUSE_NOTIFY_CODE_MAX,
};
/* The read buffer is required to be at least 8k, but may be much larger */
#define FUSE_MIN_READ_BUFFER 8192
#define FUSE_COMPAT_ENTRY_OUT_SIZE 120
struct fuse_entry_out {
__u64 nodeid; /* Inode ID */
__u64 generation; /* Inode generation: nodeid:gen must
be unique for the fs's lifetime */
__u64 entry_valid; /* Cache timeout for the name */
__u64 attr_valid; /* Cache timeout for the attributes */
__u32 entry_valid_nsec;
__u32 attr_valid_nsec;
uint64_t nodeid; /* Inode ID */
uint64_t generation; /* Inode generation: nodeid:gen must
be unique for the fs's lifetime */
uint64_t entry_valid; /* Cache timeout for the name */
uint64_t attr_valid; /* Cache timeout for the attributes */
uint32_t entry_valid_nsec;
uint32_t attr_valid_nsec;
struct fuse_attr attr;
};
struct fuse_forget_in {
__u64 nlookup;
uint64_t nlookup;
};
struct fuse_forget_one {
uint64_t nodeid;
uint64_t nlookup;
};
struct fuse_batch_forget_in {
uint32_t count;
uint32_t dummy;
};
struct fuse_getattr_in {
uint32_t getattr_flags;
uint32_t dummy;
uint64_t fh;
};
#define FUSE_COMPAT_ATTR_OUT_SIZE 96
struct fuse_attr_out {
__u64 attr_valid; /* Cache timeout for the attributes */
__u32 attr_valid_nsec;
__u32 dummy;
uint64_t attr_valid; /* Cache timeout for the attributes */
uint32_t attr_valid_nsec;
uint32_t dummy;
struct fuse_attr attr;
};
#define FUSE_COMPAT_MKNOD_IN_SIZE 8
struct fuse_mknod_in {
uint32_t mode;
uint32_t rdev;
uint32_t umask;
uint32_t padding;
};
struct fuse_mkdir_in {
__u32 mode;
__u32 padding;
uint32_t mode;
uint32_t umask;
};
struct fuse_rename_in {
__u64 newdir;
uint64_t newdir;
};
struct fuse_rename2_in {
uint64_t newdir;
uint32_t flags;
uint32_t padding;
};
struct fuse_link_in {
__u64 oldnodeid;
uint64_t oldnodeid;
};
struct fuse_setattr_in {
__u32 valid;
__u32 padding;
__u64 fh;
__u64 size;
__u64 unused1;
__u64 atime;
__u64 mtime;
__u64 unused2;
__u32 atimensec;
__u32 mtimensec;
__u32 unused3;
__u32 mode;
__u32 unused4;
__u32 uid;
__u32 gid;
__u32 unused5;
uint32_t valid;
uint32_t padding;
uint64_t fh;
uint64_t size;
uint64_t lock_owner;
uint64_t atime;
uint64_t mtime;
uint64_t ctime;
uint32_t atimensec;
uint32_t mtimensec;
uint32_t ctimensec;
uint32_t mode;
uint32_t unused4;
uint32_t uid;
uint32_t gid;
uint32_t unused5;
};
struct fuse_open_in {
__u32 flags;
__u32 mode;
uint32_t flags;
uint32_t unused;
};
struct fuse_create_in {
uint32_t flags;
uint32_t mode;
uint32_t umask;
uint32_t padding;
};
struct fuse_open_out {
__u64 fh;
__u32 open_flags;
__u32 padding;
uint64_t fh;
uint32_t open_flags;
uint32_t padding;
};
struct fuse_release_in {
__u64 fh;
__u32 flags;
__u32 release_flags;
__u64 lock_owner;
uint64_t fh;
uint32_t flags;
uint32_t release_flags;
uint64_t lock_owner;
};
struct fuse_flush_in {
__u64 fh;
__u32 unused;
__u32 padding;
__u64 lock_owner;
uint64_t fh;
uint32_t unused;
uint32_t padding;
uint64_t lock_owner;
};
struct fuse_read_in {
__u64 fh;
__u64 offset;
__u32 size;
__u32 padding;
uint64_t fh;
uint64_t offset;
uint32_t size;
uint32_t read_flags;
uint64_t lock_owner;
uint32_t flags;
uint32_t padding;
};
#define FUSE_COMPAT_WRITE_IN_SIZE 24
struct fuse_write_in {
__u64 fh;
__u64 offset;
__u32 size;
__u32 write_flags;
uint64_t fh;
uint64_t offset;
uint32_t size;
uint32_t write_flags;
uint64_t lock_owner;
uint32_t flags;
uint32_t padding;
};
struct fuse_write_out {
__u32 size;
__u32 padding;
uint32_t size;
uint32_t padding;
};
#define FUSE_COMPAT_STATFS_SIZE 48
@ -277,40 +515,42 @@ struct fuse_statfs_out {
};
struct fuse_fsync_in {
__u64 fh;
__u32 fsync_flags;
__u32 padding;
};
struct fuse_listxattr_in {
__u32 size;
__u32 flags;
};
struct fuse_listxattr_out {
__u32 size;
__u32 flags;
};
struct fuse_getxattr_in {
__u32 size;
__u32 padding;
};
struct fuse_getxattr_out {
__u32 size;
__u32 padding;
uint64_t fh;
uint32_t fsync_flags;
uint32_t padding;
};
struct fuse_setxattr_in {
__u32 size;
__u32 flags;
uint32_t size;
uint32_t flags;
};
struct fuse_listxattr_in {
uint32_t size;
uint32_t padding;
};
struct fuse_listxattr_out {
uint32_t size;
uint32_t padding;
};
struct fuse_getxattr_in {
uint32_t size;
uint32_t padding;
};
struct fuse_getxattr_out {
uint32_t size;
uint32_t padding;
};
struct fuse_lk_in {
__u64 fh;
__u64 owner;
uint64_t fh;
uint64_t owner;
struct fuse_file_lock lk;
uint32_t lk_flags;
uint32_t padding;
};
struct fuse_lk_out {
@ -318,66 +558,197 @@ struct fuse_lk_out {
};
struct fuse_access_in {
__u32 mask;
__u32 padding;
uint32_t mask;
uint32_t padding;
};
struct fuse_init_in {
__u32 major;
__u32 minor;
__u32 max_readahead;
__u32 flags;
uint32_t major;
uint32_t minor;
uint32_t max_readahead;
uint32_t flags;
};
#define FUSE_COMPAT_INIT_OUT_SIZE 8
#define FUSE_COMPAT_22_INIT_OUT_SIZE 24
struct fuse_init_out {
__u32 major;
__u32 minor;
__u32 max_readahead;
__u32 flags;
__u32 unused;
__u32 max_write;
uint32_t major;
uint32_t minor;
uint32_t max_readahead;
uint32_t flags;
uint16_t max_background;
uint16_t congestion_threshold;
uint32_t max_write;
uint32_t time_gran;
uint32_t unused[9];
};
#ifdef linux
#define CUSE_INIT_INFO_MAX 4096
struct cuse_init_in {
uint32_t major;
uint32_t minor;
uint32_t unused;
uint32_t flags;
};
struct cuse_init_out {
uint32_t major;
uint32_t minor;
uint32_t unused;
uint32_t flags;
uint32_t max_read;
uint32_t max_write;
uint32_t dev_major; /* chardev major */
uint32_t dev_minor; /* chardev minor */
uint32_t spare[10];
};
#endif /* linux */
struct fuse_interrupt_in {
__u64 unique;
uint64_t unique;
};
struct fuse_bmap_in {
__u64 block;
__u32 blocksize;
__u32 padding;
uint64_t block;
uint32_t blocksize;
uint32_t padding;
};
struct fuse_bmap_out {
__u64 block;
uint64_t block;
};
struct fuse_ioctl_in {
uint64_t fh;
uint32_t flags;
uint32_t cmd;
uint64_t arg;
uint32_t in_size;
uint32_t out_size;
};
struct fuse_ioctl_iovec {
uint64_t base;
uint64_t len;
};
struct fuse_ioctl_out {
int32_t result;
uint32_t flags;
uint32_t in_iovs;
uint32_t out_iovs;
};
struct fuse_poll_in {
uint64_t fh;
uint64_t kh;
uint32_t flags;
uint32_t events;
};
struct fuse_poll_out {
uint32_t revents;
uint32_t padding;
};
struct fuse_notify_poll_wakeup_out {
uint64_t kh;
};
struct fuse_fallocate_in {
uint64_t fh;
uint64_t offset;
uint64_t length;
uint32_t mode;
uint32_t padding;
};
struct fuse_in_header {
__u32 len;
__u32 opcode;
__u64 unique;
__u64 nodeid;
__u32 uid;
__u32 gid;
__u32 pid;
__u32 padding;
uint32_t len;
uint32_t opcode;
uint64_t unique;
uint64_t nodeid;
uint32_t uid;
uint32_t gid;
uint32_t pid;
uint32_t padding;
};
struct fuse_out_header {
__u32 len;
__s32 error;
__u64 unique;
uint32_t len;
int32_t error;
uint64_t unique;
};
struct fuse_dirent {
__u64 ino;
__u64 off;
__u32 namelen;
__u32 type;
char name[0];
uint64_t ino;
uint64_t off;
uint32_t namelen;
uint32_t type;
char name[];
};
#define FUSE_NAME_OFFSET offsetof(struct fuse_dirent, name)
#define FUSE_DIRENT_ALIGN(x) (((x) + sizeof(__u64) - 1) & ~(sizeof(__u64) - 1))
#define FUSE_DIRENT_ALIGN(x) \
(((x) + sizeof(uint64_t) - 1) & ~(sizeof(uint64_t) - 1))
#define FUSE_DIRENT_SIZE(d) \
FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + (d)->namelen)
struct fuse_direntplus {
struct fuse_entry_out entry_out;
struct fuse_dirent dirent;
};
#define FUSE_NAME_OFFSET_DIRENTPLUS \
offsetof(struct fuse_direntplus, dirent.name)
#define FUSE_DIRENTPLUS_SIZE(d) \
FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET_DIRENTPLUS + (d)->dirent.namelen)
struct fuse_notify_inval_inode_out {
uint64_t ino;
int64_t off;
int64_t len;
};
struct fuse_notify_inval_entry_out {
uint64_t parent;
uint32_t namelen;
uint32_t padding;
};
struct fuse_notify_delete_out {
uint64_t parent;
uint64_t child;
uint32_t namelen;
uint32_t padding;
};
struct fuse_notify_store_out {
uint64_t nodeid;
uint64_t offset;
uint32_t size;
uint32_t padding;
};
struct fuse_notify_retrieve_out {
uint64_t notify_unique;
uint64_t nodeid;
uint64_t offset;
uint32_t size;
uint32_t padding;
};
/* Matches the size of fuse_write_in */
struct fuse_notify_retrieve_in {
uint64_t dummy1;
uint64_t offset;
uint32_t size;
uint32_t dummy2;
uint64_t dummy3;
uint64_t dummy4;
};
#endif /* _FUSE_FUSE_KERNEL_H */

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -77,6 +82,10 @@ __FBSDID("$FreeBSD$");
#include <sys/sysctl.h>
#include "fuse.h"
#include "fuse_file.h"
#include "fuse_ipc.h"
#include "fuse_internal.h"
#include "fuse_node.h"
static void fuse_bringdown(eventhandler_tag eh_tag);
static int fuse_loader(struct module *m, int what, void *arg);
@ -85,7 +94,7 @@ struct mtx fuse_mtx;
extern struct vfsops fuse_vfsops;
extern struct cdevsw fuse_cdevsw;
extern struct vop_vector fuse_vnops;
extern struct vop_vector fuse_fifonops;
extern uma_zone_t fuse_pbuf_zone;
static struct vfsconf fuse_vfsconf = {
@ -96,11 +105,13 @@ static struct vfsconf fuse_vfsconf = {
.vfc_flags = VFCF_JAIL | VFCF_SYNTHETIC
};
SYSCTL_NODE(_vfs, OID_AUTO, fusefs, CTLFLAG_RW, 0, "FUSE tunables");
SYSCTL_NODE(_vfs_fusefs, OID_AUTO, stats, CTLFLAG_RW, 0, "FUSE statistics");
SYSCTL_INT(_vfs_fusefs, OID_AUTO, kernelabi_major, CTLFLAG_RD,
SYSCTL_NULL_INT_PTR, FUSE_KERNEL_VERSION, "FUSE kernel abi major version");
SYSCTL_INT(_vfs_fusefs, OID_AUTO, kernelabi_minor, CTLFLAG_RD,
SYSCTL_NULL_INT_PTR, FUSE_KERNEL_MINOR_VERSION, "FUSE kernel abi minor version");
SDT_PROVIDER_DEFINE(fuse);
SDT_PROVIDER_DEFINE(fusefs);
/******************************
*
@ -111,7 +122,9 @@ SDT_PROVIDER_DEFINE(fuse);
static void
fuse_bringdown(eventhandler_tag eh_tag)
{
fuse_node_destroy();
fuse_internal_destroy();
fuse_file_destroy();
fuse_ipc_destroy();
fuse_device_destroy();
mtx_destroy(&fuse_mtx);
@ -132,16 +145,14 @@ fuse_loader(struct module *m, int what, void *arg)
return (err);
}
fuse_ipc_init();
fuse_file_init();
fuse_internal_init();
fuse_node_init();
fuse_pbuf_zone = pbuf_zsecond_create("fusepbuf", nswbuf / 2);
/* vfs_modevent ignores its first arg */
if ((err = vfs_modevent(NULL, what, &fuse_vfsconf)))
fuse_bringdown(eh_tag);
else
printf("fuse-freebsd: version %s, FUSE ABI %d.%d\n",
FUSE_FREEBSD_VERSION,
FUSE_KERNEL_VERSION, FUSE_KERNEL_MINOR_VERSION);
break;
case MOD_UNLOAD:
if ((err = vfs_modevent(NULL, what, &fuse_vfsconf)))

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -59,8 +64,9 @@
__FBSDID("$FreeBSD$");
#include <sys/types.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/counter.h>
#include <sys/module.h>
#include <sys/errno.h>
#include <sys/param.h>
#include <sys/kernel.h>
@ -77,8 +83,8 @@ __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>
#include <vm/vm.h>
#include <vm/vm_extern.h>
@ -89,65 +95,40 @@ __FBSDID("$FreeBSD$");
#include "fuse_io.h"
#include "fuse_ipc.h"
SDT_PROVIDER_DECLARE(fuse);
SDT_PROVIDER_DECLARE(fusefs);
/*
* Fuse trace probe:
* arg0: verbosity. Higher numbers give more verbose messages
* arg1: Textual message
*/
SDT_PROBE_DEFINE2(fuse, , node, trace, "int", "char*");
SDT_PROBE_DEFINE2(fusefs, , node, trace, "int", "char*");
MALLOC_DEFINE(M_FUSEVN, "fuse_vnode", "fuse vnode private data");
static int sysctl_fuse_cache_mode(SYSCTL_HANDLER_ARGS);
static int fuse_node_count = 0;
static counter_u64_t fuse_node_count;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, node_count, CTLFLAG_RD,
&fuse_node_count, 0, "Count of FUSE vnodes");
SYSCTL_COUNTER_U64(_vfs_fusefs_stats, OID_AUTO, node_count, CTLFLAG_RD,
&fuse_node_count, "Count of FUSE vnodes");
int fuse_data_cache_mode = FUSE_CACHE_WT;
/*
* DEPRECATED
* This sysctl is no longer needed as of fuse protocol 7.23. Individual
* servers can select the cache behavior they need for each mountpoint:
* - writethrough: the default
* - writeback: set FUSE_WRITEBACK_CACHE in fuse_init_out.flags
* - uncached: set FOPEN_DIRECT_IO for every file
* The sysctl is retained primarily for use by jails supporting older FUSE
* protocols. It may be removed entirely once FreeBSD 11.3 and 12.0 are EOL.
*/
SYSCTL_PROC(_vfs_fusefs, OID_AUTO, data_cache_mode, CTLTYPE_INT|CTLFLAG_RW,
&fuse_data_cache_mode, 0, sysctl_fuse_cache_mode, "I",
"Zero: disable caching of FUSE file data; One: write-through caching "
"(default); Two: write-back caching (generally unsafe)");
int fuse_data_cache_invalidate = 0;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, data_cache_invalidate, CTLFLAG_RW,
&fuse_data_cache_invalidate, 0,
"If non-zero, discard cached clean file data when there are no active file"
" users");
int fuse_mmap_enable = 1;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, mmap_enable, CTLFLAG_RW,
&fuse_mmap_enable, 0,
"If non-zero, and data_cache_mode is also non-zero, enable mmap(2) of "
"FUSE files");
int fuse_refresh_size = 0;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, refresh_size, CTLFLAG_RW,
&fuse_refresh_size, 0,
"If non-zero, and no dirty file extension data is buffered, fetch file "
"size before write operations");
int fuse_sync_resize = 1;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, sync_resize, CTLFLAG_RW,
&fuse_sync_resize, 0,
"If a cached write extended a file, inform FUSE filesystem of the changed"
"size immediately subsequent to the issued writes");
int fuse_fix_broken_io = 0;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, fix_broken_io, CTLFLAG_RW,
&fuse_fix_broken_io, 0,
"If non-zero, print a diagnostic warning if a userspace filesystem returns"
" EIO on reads of recently extended portions of files");
static int
sysctl_fuse_cache_mode(SYSCTL_HANDLER_ARGS)
{
@ -174,9 +155,8 @@ static void
fuse_vnode_init(struct vnode *vp, struct fuse_vnode_data *fvdat,
uint64_t nodeid, enum vtype vtyp)
{
int i;
fvdat->nid = nodeid;
LIST_INIT(&fvdat->handles);
vattr_null(&fvdat->cached_attrs);
if (nodeid == FUSE_ROOT_ID) {
vp->v_vflag |= VV_ROOT;
@ -184,10 +164,7 @@ fuse_vnode_init(struct vnode *vp, struct fuse_vnode_data *fvdat,
vp->v_type = vtyp;
vp->v_data = fvdat;
for (i = 0; i < FUFH_MAXTYPE; i++)
fvdat->fufh[i].fh_type = FUFH_INVALID;
atomic_add_acq_int(&fuse_node_count, 1);
counter_u64_add(fuse_node_count, 1);
}
void
@ -196,23 +173,21 @@ fuse_vnode_destroy(struct vnode *vp)
struct fuse_vnode_data *fvdat = vp->v_data;
vp->v_data = NULL;
KASSERT(LIST_EMPTY(&fvdat->handles),
("Destroying fuse vnode with open files!"));
free(fvdat, M_FUSEVN);
atomic_subtract_acq_int(&fuse_node_count, 1);
counter_u64_add(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
fuse_vnode_alloc(struct mount *mp,
struct thread *td,
@ -220,10 +195,12 @@ fuse_vnode_alloc(struct mount *mp,
enum vtype vtyp,
struct vnode **vpp)
{
struct fuse_data *data;
struct fuse_vnode_data *fvdat;
struct vnode *vp2;
int err = 0;
data = fuse_get_mpdata(mp);
if (vtyp == VNON) {
return EINVAL;
}
@ -234,12 +211,34 @@ fuse_vnode_alloc(struct mount *mp,
return (err);
if (*vpp) {
MPASS((*vpp)->v_type == vtyp && (*vpp)->v_data != NULL);
SDT_PROBE2(fuse, , node, trace, 1, "vnode taken from hash");
if ((*vpp)->v_type != vtyp) {
/*
* STALE vnode! This probably indicates a buggy
* server, but it could also be the result of a race
* between FUSE_LOOKUP and another client's
* FUSE_UNLINK/FUSE_CREATE
*/
SDT_PROBE3(fusefs, , node, stale_vnode, *vpp, vtyp,
nodeid);
fuse_internal_vnode_disappear(*vpp);
lockmgr((*vpp)->v_vnlock, LK_RELEASE, NULL);
*vpp = NULL;
return (EAGAIN);
}
MPASS((*vpp)->v_data != NULL);
MPASS(VTOFUD(*vpp)->nid == nodeid);
SDT_PROBE2(fusefs, , node, trace, 1, "vnode taken from hash");
return (0);
}
fvdat = malloc(sizeof(*fvdat), M_FUSEVN, M_WAITOK | M_ZERO);
err = getnewvnode("fuse", mp, &fuse_vnops, vpp);
switch (vtyp) {
case VFIFO:
err = getnewvnode("fuse", mp, &fuse_fifoops, vpp);
break;
default:
err = getnewvnode("fuse", mp, &fuse_vnops, vpp);
break;
}
if (err) {
free(fvdat, M_FUSEVN);
return (err);
@ -249,14 +248,23 @@ fuse_vnode_alloc(struct mount *mp,
err = insmntque(*vpp, mp);
ASSERT_VOP_ELOCKED(*vpp, "fuse_vnode_alloc");
if (err) {
lockmgr((*vpp)->v_vnlock, LK_RELEASE, NULL);
free(fvdat, M_FUSEVN);
*vpp = NULL;
return (err);
}
/* Disallow async reads for fifos because UFS does. I don't know why */
if (data->dataflags & FSESS_ASYNC_READ && vtyp != VFIFO)
VN_LOCK_ASHARE(*vpp);
err = vfs_hash_insert(*vpp, fuse_vnode_hash(nodeid), LK_EXCLUSIVE,
td, &vp2, fuse_vnode_cmp, &nodeid);
if (err)
if (err) {
lockmgr((*vpp)->v_vnlock, LK_RELEASE, NULL);
free(fvdat, M_FUSEVN);
*vpp = NULL;
return (err);
}
if (vp2 != NULL) {
*vpp = vp2;
return (0);
@ -277,6 +285,11 @@ fuse_vnode_get(struct mount *mp,
enum vtype vtyp)
{
struct thread *td = (cnp != NULL ? cnp->cn_thread : curthread);
/*
* feo should only be NULL for the root directory, which (when libfuse
* is used) always has generation 0
*/
uint64_t generation = feo ? feo->generation : 0;
int err = 0;
err = fuse_vnode_alloc(mp, td, nodeid, vtyp, vpp);
@ -284,22 +297,28 @@ fuse_vnode_get(struct mount *mp,
return err;
}
if (dvp != NULL) {
MPASS((cnp->cn_flags & ISDOTDOT) == 0);
MPASS(!(cnp->cn_namelen == 1 && cnp->cn_nameptr[0] == '.'));
MPASS(cnp && (cnp->cn_flags & ISDOTDOT) == 0);
MPASS(cnp &&
!(cnp->cn_namelen == 1 && cnp->cn_nameptr[0] == '.'));
fuse_vnode_setparent(*vpp, dvp);
}
if (dvp != NULL && cnp != NULL && (cnp->cn_flags & MAKEENTRY) != 0 &&
feo != NULL &&
(feo->entry_valid != 0 || feo->entry_valid_nsec != 0)) {
struct timespec timeout;
ASSERT_VOP_LOCKED(*vpp, "fuse_vnode_get");
ASSERT_VOP_LOCKED(dvp, "fuse_vnode_get");
cache_enter(dvp, *vpp, cnp);
fuse_validity_2_timespec(feo, &timeout);
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 &&
@ -309,44 +328,19 @@ fuse_vnode_get(struct mount *mp,
return 0;
}
/*
* Called for every fusefs vnode open to initialize the vnode (not
* fuse_filehandle) for use
*/
void
fuse_vnode_open(struct vnode *vp, int32_t fuse_open_flags, struct thread *td)
{
/*
* Funcation is called for every vnode open.
* Merge fuse_open_flags it may be 0
*/
/*
* Ideally speaking, direct io should be enabled on
* fd's but do not see of any way of providing that
* this implementation.
*
* Also cannot think of a reason why would two
* different fd's on same vnode would like
* have DIRECT_IO turned on and off. But linux
* based implementation works on an fd not an
* inode and provides such a feature.
*
* XXXIP: Handle fd based DIRECT_IO
*/
if (fuse_open_flags & FOPEN_DIRECT_IO) {
ASSERT_VOP_ELOCKED(vp, __func__);
VTOFUD(vp)->flag |= FN_DIRECTIO;
fuse_io_invalbuf(vp, td);
} else {
if ((fuse_open_flags & FOPEN_KEEP_CACHE) == 0)
fuse_io_invalbuf(vp, td);
VTOFUD(vp)->flag &= ~FN_DIRECTIO;
}
if (vnode_vtype(vp) == VREG) {
/* XXXIP prevent getattr, by using cached node size */
if (vnode_vtype(vp) == VREG)
vnode_create_vobject(vp, 0, td);
}
}
int
fuse_vnode_savesize(struct vnode *vp, struct ucred *cred)
fuse_vnode_savesize(struct vnode *vp, struct ucred *cred, pid_t pid)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct thread *td = curthread;
@ -375,10 +369,11 @@ fuse_vnode_savesize(struct vnode *vp, struct ucred *cred)
fsai->valid = 0;
/* Truncate to a new value. */
fsai->size = fvdat->filesize;
MPASS((fvdat->flag & FN_SIZECHANGE) != 0);
fsai->size = fvdat->cached_attrs.va_size;
fsai->valid |= FATTR_SIZE;
fuse_filehandle_getrw(vp, FUFH_WRONLY, &fufh);
fuse_filehandle_getrw(vp, FWRITE, &fufh, cred, pid);
if (fufh) {
fsai->fh = fufh->fh_id;
fsai->valid |= FATTR_FH;
@ -391,38 +386,115 @@ fuse_vnode_savesize(struct vnode *vp, struct ucred *cred)
return err;
}
void
fuse_vnode_refreshsize(struct vnode *vp, struct ucred *cred)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct vattr va;
if ((fvdat->flag & FN_SIZECHANGE) != 0 ||
fuse_data_cache_mode == FUSE_CACHE_UC ||
(fuse_refresh_size == 0 && fvdat->filesize != 0))
return;
VOP_GETATTR(vp, &va, cred);
SDT_PROBE2(fuse, , node, trace, 1, "refreshed file size");
}
/*
* Adjust the vnode's size to a new value, such as that provided by
* FUSE_GETATTR.
*/
int
fuse_vnode_setsize(struct vnode *vp, off_t newsize)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct vattr *attrs;
off_t oldsize;
size_t iosize;
struct buf *bp = NULL;
int err = 0;
ASSERT_VOP_ELOCKED(vp, "fuse_vnode_setsize");
oldsize = fvdat->filesize;
fvdat->filesize = newsize;
fvdat->flag |= FN_SIZECHANGE;
iosize = fuse_iosize(vp);
oldsize = fvdat->cached_attrs.va_size;
fvdat->cached_attrs.va_size = newsize;
if ((attrs = VTOVA(vp)) != NULL)
attrs->va_size = newsize;
if (newsize < oldsize) {
daddr_t lbn;
err = vtruncbuf(vp, newsize, fuse_iosize(vp));
if (err)
goto out;
if (newsize % iosize == 0)
goto out;
/*
* Zero the contents of the last partial block.
* Sure seems like vtruncbuf should do this for us.
*/
lbn = newsize / iosize;
bp = getblk(vp, lbn, iosize, PCATCH, 0, 0);
if (!bp) {
err = EINTR;
goto out;
}
if (!(bp->b_flags & B_CACHE))
goto out; /* Nothing to do */
MPASS(bp->b_flags & B_VMIO);
vfs_bio_clrbuf(bp);
bp->b_dirtyend = MIN(bp->b_dirtyend, newsize - lbn * iosize);
}
out:
if (bp)
brelse(bp);
vnode_pager_setsize(vp, newsize);
return err;
}
/* Get the current, possibly dirty, size of the file */
int
fuse_vnode_size(struct vnode *vp, off_t *filesize, struct ucred *cred,
struct thread *td)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
int error = 0;
if (!(fvdat->flag & FN_SIZECHANGE) &&
(VTOVA(vp) == NULL || fvdat->cached_attrs.va_size == VNOVAL))
error = fuse_internal_do_getattr(vp, NULL, cred, td);
if (!error)
*filesize = fvdat->cached_attrs.va_size;
return error;
}
void
fuse_vnode_undirty_cached_timestamps(struct vnode *vp)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
fvdat->flag &= ~(FN_MTIMECHANGE | FN_CTIMECHANGE);
}
/* Update a fuse file's cached timestamps */
void
fuse_vnode_update(struct vnode *vp, int flags)
{
struct fuse_vnode_data *fvdat = VTOFUD(vp);
struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp));
struct timespec ts;
vfs_timestamp(&ts);
if (data->time_gran > 1)
ts.tv_nsec = rounddown(ts.tv_nsec, data->time_gran);
if (flags & FN_MTIMECHANGE)
fvdat->cached_attrs.va_mtime = ts;
if (flags & FN_CTIMECHANGE)
fvdat->cached_attrs.va_ctime = ts;
fvdat->flag |= flags;
}
void
fuse_node_init(void)
{
fuse_node_count = counter_u64_alloc(M_WAITOK);
}
void
fuse_node_destroy(void)
{
counter_u64_free(fuse_node_count);
}

View File

@ -32,6 +32,11 @@
*
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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
@ -60,60 +65,121 @@
#ifndef _FUSE_NODE_H_
#define _FUSE_NODE_H_
#include <sys/fnv_hash.h>
#include <sys/types.h>
#include <sys/mutex.h>
#include "fuse_file.h"
#define FN_REVOKED 0x00000020
#define FN_FLUSHINPROG 0x00000040
#define FN_FLUSHWANT 0x00000080
#define FN_SIZECHANGE 0x00000100
#define FN_DIRECTIO 0x00000200
#define FN_REVOKED 0x00000020
#define FN_FLUSHINPROG 0x00000040
#define FN_FLUSHWANT 0x00000080
/*
* Indicates that the file's size is dirty; the kernel has changed it but not
* yet send the change to the daemon. When this bit is set, the
* cache_attrs.va_size field does not time out.
*/
#define FN_SIZECHANGE 0x00000100
#define FN_DIRECTIO 0x00000200
/* Indicates that parent_nid is valid */
#define FN_PARENT_NID 0x00000400
/*
* Indicates that the file's cached timestamps are dirty. They will be flushed
* during the next SETATTR or WRITE. Until then, the cached fields will not
* time out.
*/
#define FN_MTIMECHANGE 0x00000800
#define FN_CTIMECHANGE 0x00001000
struct fuse_vnode_data {
/** self **/
uint64_t nid;
uint64_t generation;
/** parent **/
/* XXXIP very likely to be stale, it's not updated in rename() */
uint64_t parent_nid;
/** I/O **/
struct fuse_filehandle fufh[FUFH_MAXTYPE];
/* List of file handles for all of the vnode's open file descriptors */
LIST_HEAD(, fuse_filehandle) handles;
/** flags **/
uint32_t flag;
/** meta **/
bool valid_attr_cache;
/* The monotonic time after which the attr cache is invalid */
struct bintime attr_cache_timeout;
/*
* Monotonic time after which the entry is invalid. Used for lookups
* by nodeid instead of pathname.
*/
struct bintime entry_cache_timeout;
struct vattr cached_attrs;
off_t filesize;
uint64_t nlookup;
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)
#define VTOVA(vp) \
(VTOFUD(vp)->valid_attr_cache ? \
&(VTOFUD(vp)->cached_attrs) : NULL)
static inline struct vattr*
VTOVA(struct vnode *vp)
{
struct bintime now;
getbinuptime(&now);
if (bintime_cmp(&(VTOFUD(vp)->attr_cache_timeout), &now, >))
return &(VTOFUD(vp)->cached_attrs);
else
return NULL;
}
static inline void
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
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;
}
}
int fuse_vnode_size(struct vnode *vp, off_t *filesize, struct ucred *cred,
struct thread *td);
void fuse_vnode_destroy(struct vnode *vp);
int fuse_vnode_get(struct mount *mp, struct fuse_entry_out *feo,
@ -123,10 +189,14 @@ int fuse_vnode_get(struct mount *mp, struct fuse_entry_out *feo,
void fuse_vnode_open(struct vnode *vp, int32_t fuse_open_flags,
struct thread *td);
void fuse_vnode_refreshsize(struct vnode *vp, struct ucred *cred);
int fuse_vnode_savesize(struct vnode *vp, struct ucred *cred);
int fuse_vnode_savesize(struct vnode *vp, struct ucred *cred, pid_t pid);
int fuse_vnode_setsize(struct vnode *vp, off_t newsize);
void fuse_vnode_undirty_cached_timestamps(struct vnode *vp);
void fuse_vnode_update(struct vnode *vp, int flags);
void fuse_node_init(void);
void fuse_node_destroy(void);
#endif /* _FUSE_NODE_H_ */

View File

@ -1,82 +0,0 @@
/*-
* SPDX-License-Identifier: BSD-3-Clause
*
* Copyright (c) 2007-2009 Google Inc. and Amit Singh
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
* OWNER 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.
*
* $FreeBSD$
*/
#ifndef _FUSE_PARAM_H_
#define _FUSE_PARAM_H_
/*
* This is the prefix ("fuse" by default) of the name of a FUSE device node
* in devfs. The suffix is the device number. "/dev/fuse0" is the first FUSE
* device by default. If you change the prefix from the default to something
* else, the user-space FUSE library will need to know about it too.
*/
#define FUSE_DEVICE_BASENAME "fuse"
/*
* This is the number of /dev/fuse<n> nodes we will create. <n> goes from
* 0 to (FUSE_NDEVICES - 1).
*/
#define FUSE_NDEVICES 16
/*
* This is the default block size of the virtual storage devices that are
* implicitly implemented by the FUSE kernel extension. This can be changed
* on a per-mount basis (there's one such virtual device for each mount).
*/
#define FUSE_DEFAULT_BLOCKSIZE 4096
/*
* This is default I/O size used while accessing the virtual storage devices.
* This can be changed on a per-mount basis.
*/
#define FUSE_DEFAULT_IOSIZE 4096
#ifdef KERNEL
/*
* This is the soft upper limit on the number of "request tickets" FUSE's
* user-kernel IPC layer can have for a given mount. This can be modified
* through the fuse.* sysctl interface.
*/
#define FUSE_DEFAULT_MAX_FREE_TICKETS 1024
#define FUSE_DEFAULT_IOV_PERMANENT_BUFSIZE (1L << 19)
#define FUSE_DEFAULT_IOV_CREDIT 16
#endif
#define FUSE_LINK_MAX UINT32_MAX
#endif /* _FUSE_PARAM_H_ */

View File

@ -33,6 +33,11 @@
* Copyright (C) 2005 Csaba Henk.
* All rights reserved.
*
* Copyright (c) 2019 The FreeBSD Foundation
*
* Portions of this software were 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:
@ -81,7 +86,6 @@ __FBSDID("$FreeBSD$");
#include <sys/fcntl.h>
#include "fuse.h"
#include "fuse_param.h"
#include "fuse_node.h"
#include "fuse_ipc.h"
#include "fuse_internal.h"
@ -89,13 +93,13 @@ __FBSDID("$FreeBSD$");
#include <sys/priv.h>
#include <security/mac/mac_framework.h>
SDT_PROVIDER_DECLARE(fuse);
SDT_PROVIDER_DECLARE(fusefs);
/*
* Fuse trace probe:
* arg0: verbosity. Higher numbers give more verbose messages
* arg1: Textual message
*/
SDT_PROBE_DEFINE2(fuse, , vfsops, trace, "int", "char*");
SDT_PROBE_DEFINE2(fusefs, , vfsops, trace, "int", "char*");
/* This will do for privilege types for now */
#ifndef PRIV_VFS_FUSE_ALLOWOTHER
@ -108,29 +112,27 @@ SDT_PROBE_DEFINE2(fuse, , 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,
};
SYSCTL_INT(_vfs_fusefs, OID_AUTO, init_backgrounded, CTLFLAG_RD,
SYSCTL_NULL_INT_PTR, 1, "indicate async handshake");
static int fuse_enforce_dev_perms = 0;
SYSCTL_INT(_vfs_fusefs, OID_AUTO, enforce_dev_perms, CTLFLAG_RW,
&fuse_enforce_dev_perms, 0,
"enforce fuse device permissions for secondary mounts");
static unsigned sync_unmount = 1;
SYSCTL_UINT(_vfs_fusefs, OID_AUTO, sync_unmount, CTLFLAG_RW,
&sync_unmount, 0, "specify when to use synchronous unmount");
MALLOC_DEFINE(M_FUSEVFS, "fuse_filesystem", "buffer for fuse vfs layer");
@ -208,10 +210,89 @@ fuse_getdevice(const char *fspec, struct thread *td, struct cdev **fdevp)
vfs_flagopt(opts, "__" #fnam, &__mntopts, fval); \
} while (0)
SDT_PROBE_DEFINE1(fuse, , vfsops, mntopts, "uint64_t");
SDT_PROBE_DEFINE4(fuse, , vfsops, mount_err, "char*", "struct fuse_data*",
SDT_PROBE_DEFINE1(fusefs, , vfsops, mntopts, "uint64_t");
SDT_PROBE_DEFINE4(fusefs, , vfsops, mount_err, "char*", "struct fuse_data*",
"struct mount*", "int");
static int
fuse_vfs_remount(struct mount *mp, struct thread *td, uint64_t mntopts,
uint32_t max_read, int daemon_timeout)
{
int err = 0;
struct fuse_data *data = fuse_get_mpdata(mp);
/* Don't allow these options to be changed */
const static unsigned long long cant_update_opts =
MNT_USER; /* Mount owner must be the user running the daemon */
FUSE_LOCK();
if ((mp->mnt_flag ^ data->mnt_flag) & cant_update_opts) {
err = EOPNOTSUPP;
SDT_PROBE4(fusefs, , vfsops, mount_err,
"Can't change these mount options during remount",
data, mp, err);
goto out;
}
if (((data->dataflags ^ mntopts) & FSESS_MNTOPTS_MASK) ||
(data->max_read != max_read) ||
(data->daemon_timeout != daemon_timeout)) {
// TODO: allow changing options where it makes sense
err = EOPNOTSUPP;
SDT_PROBE4(fusefs, , vfsops, mount_err,
"Can't change fuse mount options during remount",
data, mp, err);
goto out;
}
if (fdata_get_dead(data)) {
err = ENOTCONN;
SDT_PROBE4(fusefs, , vfsops, mount_err,
"device is dead during mount", data, mp, err);
goto out;
}
/* Sanity + permission checks */
if (!data->daemoncred)
panic("fuse daemon found, but identity unknown");
if (mntopts & FSESS_DAEMON_CAN_SPY)
err = priv_check(td, PRIV_VFS_FUSE_ALLOWOTHER);
if (err == 0 && td->td_ucred->cr_uid != data->daemoncred->cr_uid)
/* are we allowed to do the first mount? */
err = priv_check(td, PRIV_VFS_FUSE_MOUNT_NONUSER);
out:
FUSE_UNLOCK();
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)
{
@ -238,13 +319,6 @@ fuse_vfsop_mount(struct mount *mp)
__mntopts = 0;
td = curthread;
if (mp->mnt_flag & MNT_UPDATE)
return EOPNOTSUPP;
MNT_ILOCK(mp);
mp->mnt_flag |= MNT_SYNCHRONOUS;
mp->mnt_data = NULL;
MNT_IUNLOCK(mp);
/* Get the new options passed to mount */
opts = mp->mnt_optnew;
@ -255,6 +329,33 @@ fuse_vfsop_mount(struct mount *mp)
if (!vfs_getopts(opts, "fspath", &err))
return err;
/*
* With the help of underscored options the mount program
* can inform us from the flags it sets by default
*/
FUSE_FLAGOPT(allow_other, FSESS_DAEMON_CAN_SPY);
FUSE_FLAGOPT(push_symlinks_in, FSESS_PUSH_SYMLINKS_IN);
FUSE_FLAGOPT(default_permissions, FSESS_DEFAULT_PERMISSIONS);
FUSE_FLAGOPT(intr, FSESS_INTR);
(void)vfs_scanopt(opts, "max_read=", "%u", &max_read);
if (vfs_scanopt(opts, "timeout=", "%u", &daemon_timeout) == 1) {
if (daemon_timeout < FUSE_MIN_DAEMON_TIMEOUT)
daemon_timeout = FUSE_MIN_DAEMON_TIMEOUT;
else if (daemon_timeout > FUSE_MAX_DAEMON_TIMEOUT)
daemon_timeout = FUSE_MAX_DAEMON_TIMEOUT;
} else {
daemon_timeout = FUSE_DEFAULT_DAEMON_TIMEOUT;
}
subtype = vfs_getopts(opts, "subtype=", &err);
SDT_PROBE1(fusefs, , vfsops, mntopts, mntopts);
if (mp->mnt_flag & MNT_UPDATE) {
return fuse_vfs_remount(mp, td, mntopts, max_read,
daemon_timeout);
}
/* `from' contains the device name (eg. /dev/fuse0); REQUIRED */
fspec = vfs_getopts(opts, "from", &err);
if (!fspec)
@ -268,36 +369,9 @@ fuse_vfsop_mount(struct mount *mp)
if (err != 0)
return err;
/*
* With the help of underscored options the mount program
* can inform us from the flags it sets by default
*/
FUSE_FLAGOPT(allow_other, FSESS_DAEMON_CAN_SPY);
FUSE_FLAGOPT(push_symlinks_in, FSESS_PUSH_SYMLINKS_IN);
FUSE_FLAGOPT(default_permissions, FSESS_DEFAULT_PERMISSIONS);
FUSE_FLAGOPT(no_attrcache, FSESS_NO_ATTRCACHE);
FUSE_FLAGOPT(no_readahed, FSESS_NO_READAHEAD);
FUSE_FLAGOPT(no_datacache, FSESS_NO_DATACACHE);
FUSE_FLAGOPT(no_namecache, FSESS_NO_NAMECACHE);
FUSE_FLAGOPT(no_mmap, FSESS_NO_MMAP);
FUSE_FLAGOPT(brokenio, FSESS_BROKENIO);
(void)vfs_scanopt(opts, "max_read=", "%u", &max_read);
if (vfs_scanopt(opts, "timeout=", "%u", &daemon_timeout) == 1) {
if (daemon_timeout < FUSE_MIN_DAEMON_TIMEOUT)
daemon_timeout = FUSE_MIN_DAEMON_TIMEOUT;
else if (daemon_timeout > FUSE_MAX_DAEMON_TIMEOUT)
daemon_timeout = FUSE_MAX_DAEMON_TIMEOUT;
} else {
daemon_timeout = FUSE_DEFAULT_DAEMON_TIMEOUT;
}
subtype = vfs_getopts(opts, "subtype=", &err);
SDT_PROBE1(fuse, , vfsops, mntopts, mntopts);
err = fget(td, fd, &cap_read_rights, &fp);
if (err != 0) {
SDT_PROBE2(fuse, , vfsops, trace, 1,
SDT_PROBE2(fusefs, , vfsops, trace, 1,
"invalid or not opened device");
goto out;
}
@ -307,16 +381,17 @@ 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(fuse, , vfsops, mount_err,
SDT_PROBE4(fusefs, , vfsops, mount_err,
"invalid or not opened device", data, mp, err);
FUSE_UNLOCK();
goto out;
}
if (fdata_get_dead(data)) {
err = ENOTCONN;
SDT_PROBE4(fuse, , vfsops, mount_err,
SDT_PROBE4(fusefs, , vfsops, mount_err,
"device is dead during mount", data, mp, err);
FUSE_UNLOCK();
goto out;
@ -338,12 +413,17 @@ fuse_vfsop_mount(struct mount *mp)
data->dataflags |= mntopts;
data->max_read = max_read;
data->daemon_timeout = daemon_timeout;
data->mnt_flag = mp->mnt_flag & MNT_UPDATEMASK;
FUSE_UNLOCK();
vfs_getnewfsid(mp);
MNT_ILOCK(mp);
mp->mnt_data = data;
mp->mnt_flag |= MNT_LOCAL;
/*
* FUSE file systems can be either local or remote, but the kernel
* can't tell the difference.
*/
mp->mnt_flag &= ~MNT_LOCAL;
mp->mnt_kern_flag |= MNTK_USES_BCACHE;
MNT_IUNLOCK(mp);
/* We need this here as this slot is used by getnewvnode() */
@ -354,6 +434,7 @@ fuse_vfsop_mount(struct mount *mp)
}
copystr(fspec, mp->mnt_stat.f_mntfromname, MNAMELEN - 1, &len);
bzero(mp->mnt_stat.f_mntfromname + len, MNAMELEN - len);
mp->mnt_iosize_max = MAXPHYS;
/* Now handshaking with daemon */
fuse_internal_send_init(data, td);
@ -366,9 +447,10 @@ fuse_vfsop_mount(struct mount *mp)
* Destroy device only if we acquired reference to
* it
*/
SDT_PROBE4(fuse, , vfsops, mount_err,
SDT_PROBE4(fusefs, , vfsops, mount_err,
"mount failed, destroy device", data, mp, err);
data->mp = NULL;
mp->mnt_data = NULL;
fdata_trydestroy(data);
}
FUSE_UNLOCK();
@ -412,11 +494,13 @@ fuse_vfsop_unmount(struct mount *mp, int mntflags)
if (fdata_get_dead(data)) {
goto alreadydead;
}
fdisp_init(&fdi, 0);
fdisp_make(&fdi, FUSE_DESTROY, mp, 0, td, NULL);
if (fsess_isimpl(mp, FUSE_DESTROY)) {
fdisp_init(&fdi, 0);
fdisp_make(&fdi, FUSE_DESTROY, mp, 0, td, NULL);
err = fdisp_wait_answ(&fdi);
fdisp_destroy(&fdi);
(void)fdisp_wait_answ(&fdi);
fdisp_destroy(&fdi);
}
fdata_set_dead(data);
@ -429,7 +513,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);
@ -437,6 +520,86 @@ fuse_vfsop_unmount(struct mount *mp, int mntflags)
return 0;
}
SDT_PROBE_DEFINE1(fusefs, , vfsops, invalidate_without_export,
"struct mount*");
static int
fuse_vfsop_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
{
struct fuse_data *data = fuse_get_mpdata(mp);
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;
if (!(data->dataflags & FSESS_EXPORT_SUPPORT)) {
/*
* Unreachable unless you do something stupid, like export a
* nullfs mount of a fusefs file system.
*/
SDT_PROBE1(fusefs, , vfsops, invalidate_without_export, mp);
return (EOPNOTSUPP);
}
error = fuse_internal_get_cached_vnode(mp, ino, flags, vpp);
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, &feo->attr, feo->attr_valid,
feo->attr_valid_nsec, NULL);
fuse_validity_2_bintime(feo->entry_valid, feo->entry_valid_nsec,
&fvdat->entry_cache_timeout);
out:
fdisp_destroy(&fdi);
return error;
}
static int
fuse_vfsop_root(struct mount *mp, int lkflags, struct vnode **vpp)
{
@ -454,13 +617,13 @@ fuse_vfsop_root(struct mount *mp, int lkflags, struct vnode **vpp)
FUSE_LOCK();
MPASS(data->vroot == NULL || data->vroot == *vpp);
if (data->vroot == NULL) {
SDT_PROBE2(fuse, , vfsops, trace, 1,
SDT_PROBE2(fusefs, , vfsops, trace, 1,
"new root vnode");
data->vroot = *vpp;
FUSE_UNLOCK();
vref(*vpp);
} else if (data->vroot != *vpp) {
SDT_PROBE2(fuse, , vfsops, trace, 1,
SDT_PROBE2(fusefs, , vfsops, trace, 1,
"root vnode race");
FUSE_UNLOCK();
VOP_UNLOCK(*vpp, 0);
@ -523,7 +686,7 @@ fuse_vfsop_statfs(struct mount *mp, struct statfs *sbp)
sbp->f_files = 0;
sbp->f_ffree = 0;
sbp->f_namemax = 0;
sbp->f_bsize = FUSE_DEFAULT_BLOCKSIZE;
sbp->f_bsize = S_BLKSIZE;
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -60,7 +60,7 @@
* in the range 5 to 9.
*/
#undef __FreeBSD_version
#define __FreeBSD_version 1300038 /* Master, propagated to newvers */
#define __FreeBSD_version 1300039 /* Master, propagated to newvers */
/*
* __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD,

View File

@ -1,5 +1,7 @@
# $FreeBSD$
.include <bsd.compiler.mk>
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/fs
@ -7,6 +9,9 @@ TESTSDIR= ${TESTSBASE}/sys/fs
TESTSRC= ${SRCTOP}/contrib/netbsd-tests/fs
#TESTS_SUBDIRS+= nullfs # XXX: needs rump
.if ${COMPILER_FEATURES:Mc++14}
TESTS_SUBDIRS+= fusefs
.endif
TESTS_SUBDIRS+= tmpfs
${PACKAGE}FILES+= h_funcs.subr

View File

@ -0,0 +1,81 @@
# $FreeBSD$
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
# We could simply link all of these files into a single executable. But since
# Kyua treats googletest programs as plain tests, it's better to separate them
# out, so we get more granular reporting.
GTESTS+= access
GTESTS+= allow_other
GTESTS+= bmap
GTESTS+= create
GTESTS+= default_permissions
GTESTS+= default_permissions_privileged
GTESTS+= destroy
GTESTS+= dev_fuse_poll
GTESTS+= fifo
GTESTS+= flush
GTESTS+= forget
GTESTS+= fsync
GTESTS+= fsyncdir
GTESTS+= getattr
GTESTS+= interrupt
GTESTS+= io
GTESTS+= link
GTESTS+= locks
GTESTS+= lookup
GTESTS+= mkdir
GTESTS+= mknod
GTESTS+= mount
GTESTS+= nfs
GTESTS+= notify
GTESTS+= open
GTESTS+= opendir
GTESTS+= read
GTESTS+= readdir
GTESTS+= readlink
GTESTS+= release
GTESTS+= releasedir
GTESTS+= rename
GTESTS+= rmdir
GTESTS+= setattr
GTESTS+= statfs
GTESTS+= symlink
GTESTS+= unlink
GTESTS+= write
GTESTS+= xattr
.for p in ${GTESTS}
SRCS.$p+= ${p}.cc
SRCS.$p+= getmntopts.c
SRCS.$p+= mockfs.cc
SRCS.$p+= utils.cc
.endfor
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
FUSEFS= ${SRCTOP}/sys/fs/fuse
MOUNT= ${SRCTOP}/sbin/mount
# Suppress warnings that GCC generates for the libc++ and gtest headers.
CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes -Wno-class-memaccess
CXXFLAGS+= -I${SRCTOP}/tests
CXXFLAGS+= -I${FUSEFS}
CXXFLAGS+= -I${MOUNT}
.PATH: ${MOUNT}
CXXSTD= c++14
LIBADD+= pthread
LIBADD+= gmock gtest
LIBADD+= util
WARNS?= 6
.include <bsd.test.mk>

View File

@ -0,0 +1,119 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Access: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
}
};
class RofsAccess: public Access {
public:
virtual void SetUp() {
m_ro = true;
Access::SetUp();
}
};
/* The error case of FUSE_ACCESS. */
TEST_F(Access, eaccess)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = X_OK;
expect_access(FUSE_ROOT_ID, X_OK, 0);
expect_lookup(RELPATH, ino);
expect_access(ino, access_mode, EACCES);
ASSERT_NE(0, access(FULLPATH, access_mode));
ASSERT_EQ(EACCES, errno);
}
/*
* If the filesystem returns ENOSYS, then it is treated as a permanent success,
* and subsequent VOP_ACCESS calls will succeed automatically without querying
* the daemon.
*/
TEST_F(Access, enosys)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = R_OK;
expect_access(FUSE_ROOT_ID, X_OK, ENOSYS);
FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
}
TEST_F(RofsAccess, erofs)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = W_OK;
expect_access(FUSE_ROOT_ID, X_OK, 0);
expect_lookup(RELPATH, ino);
ASSERT_NE(0, access(FULLPATH, access_mode));
ASSERT_EQ(EROFS, errno);
}
/* The successful case of FUSE_ACCESS. */
TEST_F(Access, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = R_OK;
expect_access(FUSE_ROOT_ID, X_OK, 0);
expect_lookup(RELPATH, ino);
expect_access(ino, access_mode, 0);
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
}

View File

@ -0,0 +1,303 @@
/*-
* 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.
*/
/*
* Tests for the "allow_other" mount option. They must be in their own
* file so they can be run as root
*/
extern "C" {
#include <sys/types.h>
#include <sys/extattr.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
const static char FULLPATH[] = "mountpoint/some_file.txt";
const static char RELPATH[] = "some_file.txt";
class NoAllowOther: public FuseTest {
public:
/* Unprivileged user id */
int m_uid;
virtual void SetUp() {
if (geteuid() != 0) {
GTEST_SKIP() << "This test must be run as root";
}
FuseTest::SetUp();
}
};
class AllowOther: public NoAllowOther {
public:
virtual void SetUp() {
m_allow_other = true;
NoAllowOther::SetUp();
}
};
TEST_F(AllowOther, allowed)
{
int status;
fork(true, &status, [&] {
uint64_t ino = 42;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, FH);
}, []() {
int fd;
fd = open(FULLPATH, O_RDONLY);
if (fd < 0) {
perror("open");
return(1);
}
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
}
/* Check that fusefs uses the correct credentials for FUSE operations */
TEST_F(AllowOther, creds)
{
int status;
uid_t uid;
gid_t gid;
get_unprivileged_id(&uid, &gid);
fork(true, &status, [=] {
EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) {
return (in.header.opcode == FUSE_LOOKUP &&
in.header.uid == uid &&
in.header.gid == gid);
}, Eq(true)),
_)
).Times(1)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
}, []() {
eaccess(FULLPATH, F_OK);
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
}
/*
* A variation of the Open.multiple_creds test showing how the bug can lead to a
* privilege elevation. The first process is privileged and opens a file only
* visible to root. The second process is unprivileged and shouldn't be able
* to open the file, but does thanks to the bug
*/
TEST_F(AllowOther, privilege_escalation)
{
int fd1, status;
const static uint64_t ino = 42;
const static uint64_t fh = 100;
/* Fork a child to open the file with different credentials */
fork(true, &status, [&] {
expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.pid == (uint32_t)getpid() &&
in.header.uid == (uint32_t)geteuid() &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(
ReturnImmediate([](auto in __unused, auto& out) {
out.body.open.fh = fh;
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.pid != (uint32_t)getpid() &&
in.header.uid != (uint32_t)geteuid() &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(AnyNumber())
.WillRepeatedly(Invoke(ReturnErrno(EPERM)));
fd1 = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd1) << strerror(errno);
}, [] {
int fd0;
fd0 = open(FULLPATH, O_RDONLY);
if (fd0 >= 0) {
fprintf(stderr, "Privilege escalation!\n");
return 1;
}
if (errno != EPERM) {
fprintf(stderr, "Unexpected error %s\n",
strerror(errno));
return 1;
}
leak(fd0);
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
leak(fd1);
}
TEST_F(NoAllowOther, disallowed)
{
int status;
fork(true, &status, [] {
}, []() {
int fd;
fd = open(FULLPATH, O_RDONLY);
if (fd >= 0) {
fprintf(stderr, "open should've failed\n");
return(1);
} else if (errno != EPERM) {
fprintf(stderr, "Unexpected error: %s\n",
strerror(errno));
return(1);
}
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
}
/*
* When -o allow_other is not used, users other than the owner aren't allowed
* to open anything inside of the mount point, not just the mountpoint itself
* This is a regression test for bug 237052
*/
TEST_F(NoAllowOther, disallowed_beneath_root)
{
const static char RELPATH2[] = "other_dir";
const static uint64_t ino = 42;
const static uint64_t ino2 = 43;
int dfd, status;
expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1);
EXPECT_LOOKUP(ino, RELPATH2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino2;
out.body.entry.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
})));
expect_opendir(ino);
dfd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, dfd) << strerror(errno);
fork(true, &status, [] {
}, [&]() {
int fd;
fd = openat(dfd, RELPATH2, O_RDONLY);
if (fd >= 0) {
fprintf(stderr, "openat should've failed\n");
return(1);
} else if (errno != EPERM) {
fprintf(stderr, "Unexpected error: %s\n",
strerror(errno));
return(1);
}
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
}
/*
* Provide coverage for the extattr methods, which have a slightly different
* code path
*/
TEST_F(NoAllowOther, setextattr)
{
int ino = 42, status;
fork(true, &status, [&] {
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
})));
/*
* lookup the file to get it into the cache.
* Otherwise, the unprivileged lookup will fail with
* EACCES
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}, [&]() {
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
r = extattr_set_file(FULLPATH, ns, "foo",
(const void*)value, value_len);
if (r >= 0) {
fprintf(stderr, "should've failed\n");
return(1);
} else if (errno != EPERM) {
fprintf(stderr, "Unexpected error: %s\n",
strerror(errno));
return(1);
}
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
}

159
tests/sys/fs/fusefs/bmap.cc Normal file
View File

@ -0,0 +1,159 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/filio.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
class Bmap: public FuseTest {
public:
virtual void SetUp() {
m_maxreadahead = UINT32_MAX;
FuseTest::SetUp();
}
void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_BMAP &&
in.header.nodeid == ino &&
in.body.bmap.block == lbn &&
in.body.bmap.blocksize == blocksize);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, bmap);
out.body.bmap.block = pbn;
})));
}
void expect_lookup(const char *relpath, uint64_t ino, off_t size)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1,
UINT64_MAX);
}
};
/*
* Test FUSE_BMAP
* XXX The FUSE protocol does not include the runp and runb variables, so those
* must be guessed in-kernel.
*/
TEST_F(Bmap, bmap)
{
struct fiobmap2_arg arg;
const off_t filesize = 1 << 20;
const ino_t ino = 42;
int64_t lbn = 10;
int64_t pbn = 12345;
int fd;
expect_lookup(RELPATH, 42, filesize);
expect_open(ino, 0, 1);
expect_bmap(ino, lbn, m_maxbcachebuf, pbn);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
arg.bn = lbn;
arg.runp = -1;
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, pbn);
EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
}
/*
* If the daemon does not implement VOP_BMAP, fusefs should return sensible
* defaults.
*/
TEST_F(Bmap, default_)
{
struct fiobmap2_arg arg;
const off_t filesize = 1 << 20;
const ino_t ino = 42;
int64_t lbn;
int fd;
expect_lookup(RELPATH, 42, filesize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_BMAP);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(ENOSYS)));
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
/* First block */
lbn = 0;
arg.bn = lbn;
arg.runp = -1;
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, 0);
EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
EXPECT_EQ(arg.runb, 0);
/* In the middle */
lbn = filesize / m_maxbcachebuf / 2;
arg.bn = lbn;
arg.runp = -1;
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
/* Last block */
lbn = filesize / m_maxbcachebuf - 1;
arg.bn = lbn;
arg.runp = -1;
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
EXPECT_EQ(arg.runp, 0);
EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
}

View File

@ -0,0 +1,449 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Create: public FuseTest {
public:
void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
{
mode_t mask = umask(0);
(void)umask(mask);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_create_in);
return (in.header.opcode == FUSE_CREATE &&
in.body.create.mode == mode &&
in.body.create.umask == mask &&
(0 == strcmp(relpath, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
/* FUSE_CREATE operations for a protocol 7.8 server */
class Create_7_8: public Create {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
Create::SetUp();
}
void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_open_in);
return (in.header.opcode == FUSE_CREATE &&
in.body.create.mode == mode &&
(0 == strcmp(relpath, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
/* FUSE_CREATE operations for a server built at protocol <= 7.11 */
class Create_7_11: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 11;
FuseTest::SetUp();
}
void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_open_in);
return (in.header.opcode == FUSE_CREATE &&
in.body.create.mode == mode &&
(0 == strcmp(relpath, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
/*
* If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the
* attribute cache
*/
TEST_F(Create, attr_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(0);
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
/* A successful CREATE operation should purge the parent dir's attr cache */
TEST_F(Create, clear_attr_cache)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = FUSE_ROOT_ID;
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
leak(fd);
}
/*
* The fuse daemon fails the request with EEXIST. This usually indicates a
* race condition: some other FUSE client created the file in between when the
* kernel checked for it with lookup and tried to create it with create
*/
TEST_F(Create, eexist)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode, ReturnErrno(EEXIST));
EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode));
EXPECT_EQ(EEXIST, errno);
}
/*
* If the daemon doesn't implement FUSE_CREATE, then the kernel should fallback
* to FUSE_MKNOD/FUSE_OPEN
*/
TEST_F(Create, Enosys)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode, ReturnErrno(ENOSYS));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mknod_in);
return (in.header.opcode == FUSE_MKNOD &&
in.body.mknod.mode == (S_IFREG | mode) &&
in.body.mknod.rdev == 0 &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).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.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
/*
* Creating a new file after FUSE_LOOKUP returned a negative cache entry
*/
TEST_F(Create, entry_cache_negative)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
/*
* Set entry_valid = 0 because this test isn't concerned with whether
* or not we actually cache negative entries, only with whether we
* interpret negative cache responses correctly.
*/
struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
/* create will first do a LOOKUP, adding a negative cache entry */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(ReturnNegativeCache(&entry_valid));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
ASSERT_LE(0, fd) << strerror(errno);
leak(fd);
}
/*
* Creating a new file should purge any negative namecache entries
*/
TEST_F(Create, entry_cache_negative_purge)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
/* create will first do a LOOKUP, adding a negative cache entry */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH).Times(1)
.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
.RetiresOnSaturation();
/* Then the CREATE should purge the negative cache entry */
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
ASSERT_LE(0, fd) << strerror(errno);
/* Finally, a subsequent lookup should query the daemon */
expect_lookup(RELPATH, ino, S_IFREG | mode, 0, 1);
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
leak(fd);
}
/*
* The daemon is responsible for checking file permissions (unless the
* default_permissions mount option was used)
*/
TEST_F(Create, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode, ReturnErrno(EPERM));
EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode));
EXPECT_EQ(EPERM, errno);
}
TEST_F(Create, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
/*
* A regression test for a bug that affected old FUSE implementations:
* open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming
* contradiction between O_WRONLY and 0444
*
* For example:
* https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886
*/
TEST_F(Create, wronly_0444)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0444;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_WRONLY, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
TEST_F(Create_7_8, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create_7_8);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
TEST_F(Create_7_11, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = S_IFREG | 0755;
uint64_t ino = 42;
int fd;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, mode,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
}));
fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,124 @@
/*-
* 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.
*/
/*
* Tests for the "default_permissions" mount option that require a privileged
* user.
*/
extern "C" {
#include <sys/types.h>
#include <sys/extattr.h>
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class DefaultPermissionsPrivileged: public FuseTest {
virtual void SetUp() {
m_default_permissions = true;
FuseTest::SetUp();
if (HasFatalFailure() || IsSkipped())
return;
if (geteuid() != 0) {
GTEST_SKIP() << "This test requires a privileged user";
}
/* With -o default_permissions, FUSE_ACCESS should never be called */
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_ACCESS);
}, Eq(true)),
_)
).Times(0);
}
public:
void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times,
uid_t uid = 0, gid_t gid = 0)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = mode;
out.body.attr.attr.size = 0;
out.body.attr.attr.uid = uid;
out.body.attr.attr.uid = gid;
out.body.attr.attr_valid = attr_valid;
})));
}
void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0)
{
FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid);
}
};
class Setattr: public DefaultPermissionsPrivileged {};
TEST_F(Setattr, sticky_regular_file)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0644;
const mode_t newmode = 01644;
expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | newmode;
})));
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}

View File

@ -0,0 +1,158 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/* Tests for orderly unmounts */
class Destroy: public FuseTest {};
/* Tests for unexpected deaths of the server */
class Death: public FuseTest{};
static void* open_th(void* arg) {
int fd;
const char *path = (const char*)arg;
fd = open(path, O_RDONLY);
EXPECT_EQ(-1, fd);
EXPECT_EQ(ENOTCONN, errno);
return 0;
}
/*
* The server dies with unsent operations still on the message queue.
* Check for any memory leaks like this:
* 1) kldunload fusefs, if necessary
* 2) kldload fusefs
* 3) ./destroy --gtest_filter=Destroy.unsent_operations
* 4) kldunload fusefs
* 5) check /var/log/messages for anything like this:
Freed UMA keg (fuse_ticket) was not empty (31 items). Lost 2 pages of memory.
Warning: memory type fuse_msgbuf leaked memory on destroy (68 allocations, 428800 bytes leaked).
*/
TEST_F(Death, unsent_operations)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH0[] = "some_file.txt";
const char RELPATH1[] = "other_file.txt";
pthread_t th0, th1;
ino_t ino0 = 42, ino1 = 43;
sem_t sem;
mode_t mode = S_IFREG | 0644;
sem_init(&sem, 0, 0);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino0;
out.body.entry.attr.nlink = 1;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino1;
out.body.entry.attr.nlink = 1;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_OPEN);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
sem_post(&sem);
pause();
}));
/*
* One thread's operation will be sent to the daemon and block, and the
* other's will be stuck in the message queue.
*/
ASSERT_EQ(0, pthread_create(&th0, NULL, open_th,
__DECONST(void*, FULLPATH0))) << strerror(errno);
ASSERT_EQ(0, pthread_create(&th1, NULL, open_th,
__DECONST(void*, FULLPATH1))) << strerror(errno);
/* Wait for the first thread to block */
sem_wait(&sem);
/* Give the second thread time to block */
nap();
m_mock->kill_daemon();
pthread_join(th0, NULL);
pthread_join(th1, NULL);
sem_destroy(&sem);
}
/*
* On unmount the kernel should send a FUSE_DESTROY operation. It should also
* send FUSE_FORGET operations for all inodes with lookup_count > 0.
*/
TEST_F(Destroy, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
expect_forget(FUSE_ROOT_ID, 1);
expect_forget(ino, 2);
expect_destroy(0);
/*
* access(2) the file to force a lookup. Access it twice to double its
* lookup count.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
/*
* Unmount, triggering a FUSE_DESTROY and also causing a VOP_RECLAIM
* for every vnode on this mp, triggering FUSE_FORGET for each of them.
*/
m_mock->unmount();
}

View File

@ -0,0 +1,227 @@
/*-
* 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 different polling methods for the /dev/fuse device
*/
extern "C" {
#include <fcntl.h>
#include <semaphore.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t access_mode = R_OK;
/*
* Translate a poll method's string representation to the enum value.
* Using strings with ::testing::Values gives better output with
* --gtest_list_tests
*/
enum poll_method poll_method_from_string(const char *s)
{
if (0 == strcmp("BLOCKING", s))
return BLOCKING;
else if (0 == strcmp("KQ", s))
return KQ;
else if (0 == strcmp("POLL", s))
return POLL;
else
return SELECT;
}
class DevFusePoll: public FuseTest, public WithParamInterface<const char *> {
virtual void SetUp() {
m_pm = poll_method_from_string(GetParam());
FuseTest::SetUp();
}
};
class Kqueue: public FuseTest {
virtual void SetUp() {
m_pm = KQ;
FuseTest::SetUp();
}
};
TEST_P(DevFusePoll, access)
{
expect_access(FUSE_ROOT_ID, X_OK, 0);
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_access(ino, access_mode, 0);
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
}
/* Ensure that we wake up pollers during unmount */
TEST_P(DevFusePoll, destroy)
{
expect_forget(FUSE_ROOT_ID, 1);
expect_destroy(0);
m_mock->unmount();
}
INSTANTIATE_TEST_CASE_P(PM, DevFusePoll,
::testing::Values("BLOCKING", "KQ", "POLL", "SELECT"));
static void* statter(void* arg) {
const char *name;
struct stat sb;
name = (const char*)arg;
stat(name, &sb);
return 0;
}
/*
* A kevent's data field should contain the number of operations available to
* be immediately rea.
*/
TEST_F(Kqueue, data)
{
pthread_t th0, th1, th2;
sem_t sem0, sem1;
int nready0, nready1, nready2;
uint64_t foo_ino = 42;
uint64_t bar_ino = 43;
uint64_t baz_ino = 44;
Sequence seq;
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = foo_ino;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, "bar")
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = bar_ino;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, "baz")
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = baz_ino;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == foo_ino);
}, Eq(true)),
_)
)
.WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
nready0 = m_mock->m_nready;
sem_post(&sem0);
// Block the daemon so we can accumulate a few more ops
sem_wait(&sem1);
out.header.unique = in.header.unique;
out.header.error = -EIO;
out.header.len = sizeof(out.header);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
(in.header.nodeid == bar_ino ||
in.header.nodeid == baz_ino));
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
nready1 = m_mock->m_nready;
out.header.unique = in.header.unique;
out.header.error = -EIO;
out.header.len = sizeof(out.header);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
(in.header.nodeid == bar_ino ||
in.header.nodeid == baz_ino));
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
nready2 = m_mock->m_nready;
out.header.unique = in.header.unique;
out.header.error = -EIO;
out.header.len = sizeof(out.header);
})));
/*
* Create cached lookup entries for these files. It seems that only
* one thread at a time can be in VOP_LOOKUP for a given directory
*/
access("mountpoint/foo", F_OK);
access("mountpoint/bar", F_OK);
access("mountpoint/baz", F_OK);
ASSERT_EQ(0, pthread_create(&th0, NULL, statter,
__DECONST(void*, "mountpoint/foo"))) << strerror(errno);
EXPECT_EQ(0, sem_wait(&sem0)) << strerror(errno);
ASSERT_EQ(0, pthread_create(&th1, NULL, statter,
__DECONST(void*, "mountpoint/bar"))) << strerror(errno);
ASSERT_EQ(0, pthread_create(&th2, NULL, statter,
__DECONST(void*, "mountpoint/baz"))) << strerror(errno);
nap(); // Allow th1 and th2 to send their ops to the daemon
EXPECT_EQ(0, sem_post(&sem1)) << strerror(errno);
pthread_join(th0, NULL);
pthread_join(th1, NULL);
pthread_join(th2, NULL);
EXPECT_EQ(1, nready0);
EXPECT_EQ(2, nready1);
EXPECT_EQ(1, nready2);
sem_destroy(&sem0);
sem_destroy(&sem1);
}

207
tests/sys/fs/fusefs/fifo.cc Normal file
View File

@ -0,0 +1,207 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
const char FULLPATH[] = "mountpoint/some_fifo";
const char RELPATH[] = "some_fifo";
const char MESSAGE[] = "Hello, World!\n";
const int msgsize = sizeof(MESSAGE);
class Fifo: public FuseTest {
public:
pthread_t m_child;
Fifo(): m_child(NULL) {};
void TearDown() {
if (m_child != NULL) {
pthread_join(m_child, NULL);
}
FuseTest::TearDown();
}
};
class Socket: public Fifo {};
/* Writer thread */
static void* writer(void* arg) {
ssize_t sent = 0;
int fd;
fd = *(int*)arg;
while (sent < msgsize) {
ssize_t r;
r = write(fd, MESSAGE + sent, msgsize - sent);
if (r < 0)
return (void*)(intptr_t)errno;
else
sent += r;
}
return 0;
}
/*
* Reading and writing FIFOs works. None of the I/O actually goes through FUSE
*/
TEST_F(Fifo, read_write)
{
mode_t mode = S_IFIFO | 0755;
const int bufsize = 80;
char message[bufsize];
ssize_t recvd = 0, r;
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, mode, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, pthread_create(&m_child, NULL, writer, &fd))
<< strerror(errno);
while (recvd < msgsize) {
r = read(fd, message + recvd, bufsize - recvd);
ASSERT_LE(0, r) << strerror(errno);
ASSERT_LT(0, r) << "unexpected EOF";
recvd += r;
}
ASSERT_STREQ(message, MESSAGE);
leak(fd);
}
/* Writer thread */
static void* socket_writer(void* arg __unused) {
ssize_t sent = 0;
int fd, err;
struct sockaddr_un sa;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return (void*)(intptr_t)errno;
}
sa.sun_family = AF_UNIX;
strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
err = connect(fd, (struct sockaddr*)&sa, sizeof(sa));
if (err < 0) {
perror("connect");
return (void*)(intptr_t)errno;
}
while (sent < msgsize) {
ssize_t r;
r = write(fd, MESSAGE + sent, msgsize - sent);
if (r < 0)
return (void*)(intptr_t)errno;
else
sent += r;
}
return 0;
}
/*
* Reading and writing unix-domain sockets works. None of the I/O actually
* goes through FUSE.
*/
TEST_F(Socket, read_write)
{
mode_t mode = S_IFSOCK | 0755;
const int bufsize = 80;
char message[bufsize];
struct sockaddr_un sa;
ssize_t recvd = 0, r;
uint64_t ino = 42;
int fd, connected;
Sequence seq;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKNOD);
}, Eq(true)),
_)
).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.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.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.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
fd = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, fd) << strerror(errno);
sa.sun_family = AF_UNIX;
strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)))
<< strerror(errno);
listen(fd, 5);
ASSERT_EQ(0, pthread_create(&m_child, NULL, socket_writer, NULL))
<< strerror(errno);
connected = accept(fd, 0, 0);
ASSERT_LE(0, connected) << strerror(errno);
while (recvd < msgsize) {
r = read(connected, message + recvd, bufsize - recvd);
ASSERT_LE(0, r) << strerror(errno);
ASSERT_LT(0, r) << "unexpected EOF";
recvd += r;
}
ASSERT_STREQ(message, MESSAGE);
leak(fd);
}

View File

@ -0,0 +1,232 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Flush: public FuseTest {
public:
void
expect_flush(uint64_t ino, int times, pid_t lo, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FLUSH &&
in.header.nodeid == ino &&
in.body.flush.lock_owner == (uint64_t)lo &&
in.body.flush.fh == FH);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(r));
}
void expect_lookup(const char *relpath, uint64_t ino, int times)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times);
}
/*
* When testing FUSE_FLUSH, the FUSE_RELEASE calls are uninteresting. This
* expectation will silence googlemock warnings
*/
void expect_release()
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASE);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnErrno(0)));
}
};
class FlushWithLocks: public Flush {
virtual void SetUp() {
m_init_flags = FUSE_POSIX_LOCKS;
Flush::SetUp();
}
};
/*
* If multiple file descriptors refer to the same file handle, closing each
* should send FUSE_FLUSH
*/
TEST_F(Flush, open_twice)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd, fd2;
expect_lookup(RELPATH, ino, 2);
expect_open(ino, 0, 1);
expect_flush(ino, 2, getpid(), ReturnErrno(0));
expect_release();
fd = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd) << strerror(errno);
fd2 = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd2) << strerror(errno);
EXPECT_EQ(0, close(fd2)) << strerror(errno);
EXPECT_EQ(0, close(fd)) << strerror(errno);
}
/*
* Some FUSE filesystem cache data internally and flush it on release. Such
* filesystems may generate errors during release. On Linux, these get
* returned by close(2). However, POSIX does not require close(2) to return
* this error. FreeBSD's fuse(4) should return EIO if it returns an error at
* all.
*/
/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */
TEST_F(Flush, eio)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, getpid(), ReturnErrno(EIO));
expect_release();
fd = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno);
}
/*
* If the filesystem returns ENOSYS, it will be treated as success and
* no more FUSE_FLUSH operations will be sent to the daemon
*/
TEST_F(Flush, enosys)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char RELPATH0[] = "some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
uint64_t ino0 = 42;
uint64_t ino1 = 43;
int fd0, fd1;
expect_lookup(RELPATH0, ino0, 1);
expect_open(ino0, 0, 1);
/* On the 2nd close, FUSE_FLUSH won't be sent at all */
expect_flush(ino0, 1, getpid(), ReturnErrno(ENOSYS));
expect_release();
expect_lookup(RELPATH1, ino1, 1);
expect_open(ino1, 0, 1);
/* On the 2nd close, FUSE_FLUSH won't be sent at all */
expect_release();
fd0 = open(FULLPATH0, O_WRONLY);
ASSERT_LE(0, fd0) << strerror(errno);
fd1 = open(FULLPATH1, O_WRONLY);
ASSERT_LE(0, fd1) << strerror(errno);
EXPECT_EQ(0, close(fd0)) << strerror(errno);
EXPECT_EQ(0, close(fd1)) << strerror(errno);
}
/* A FUSE_FLUSH should be sent on close(2) */
TEST_F(Flush, flush)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, getpid(), ReturnErrno(0));
expect_release();
fd = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_TRUE(0 == close(fd)) << strerror(errno);
}
/*
* When closing a file with a POSIX file lock, flush should release the lock,
* _even_if_ it's not the process's last file descriptor for this file.
*/
TEST_F(FlushWithLocks, unlock_on_close)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd, fd2;
struct flock fl;
pid_t pid = getpid();
expect_lookup(RELPATH, ino, 2);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETLK &&
in.header.nodeid == ino &&
in.body.setlk.fh == FH);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
expect_flush(ino, 1, pid, ReturnErrno(0));
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 0;
fl.l_len = 0;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
fd2 = open(FULLPATH, O_WRONLY);
ASSERT_LE(0, fd2) << strerror(errno);
ASSERT_EQ(0, close(fd2)) << strerror(errno);
leak(fd);
leak(fd2);
}

View File

@ -0,0 +1,154 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <semaphore.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
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();
}
};
/*
* When a fusefs vnode is reclaimed, it should send a FUSE_FORGET operation.
*/
TEST_F(Forget, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
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);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(3)
.WillRepeatedly(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.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
})));
expect_forget(ino, 3, &sem);
/*
* access(2) the file to force a lookup. Access it twice to double its
* lookup count.
*/
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);
sem_wait(&sem);
sem_destroy(&sem);
}
/*
* When a directory is reclaimed, the names of its entries vanish from the
* namecache
*/
TEST_F(Forget, invalidate_names)
{
const char FULLFPATH[] = "mountpoint/some_dir/some_file.txt";
const char FULLDPATH[] = "mountpoint/some_dir";
const char DNAME[] = "some_dir";
const char FNAME[] = "some_file.txt";
uint64_t dir_ino = 42;
uint64_t file_ino = 43;
int err;
EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
.WillRepeatedly(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 = dir_ino;
out.body.entry.attr.nlink = 2;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
/*
* Even though we don't reclaim FNAME and its entry is cacheable, we
* should get two lookups because the reclaim of DNAME will invalidate
* the cached FNAME entry.
*/
EXPECT_LOOKUP(dir_ino, FNAME)
.Times(2)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = file_ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
expect_forget(dir_ino, 2);
/* Access the file to cache its name */
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);
/* Access the file again, causing another lookup */
ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno);
}

View File

@ -0,0 +1,249 @@
/*-
* 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.
*/
extern "C" {
#include <aio.h>
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/*
* TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28.
* This bit was actually part of kernel protocol version 5.2, but never
* documented until after 7.28
*/
#ifndef FUSE_FSYNC_FDATASYNC
#define FUSE_FSYNC_FDATASYNC 1
#endif
class Fsync: public FuseTest {
public:
void expect_fsync(uint64_t ino, uint32_t flags, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FSYNC &&
in.header.nodeid == ino &&
/*
* TODO: reenable pid check after fixing
* bug 236379
*/
//(pid_t)in.header.pid == getpid() &&
in.body.fsync.fh == FH &&
in.body.fsync.fsync_flags == flags);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
}
void expect_write(uint64_t ino, uint64_t size, const void *contents)
{
FuseTest::expect_write(ino, 0, size, size, 0, 0, contents);
}
};
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
TEST_F(Fsync, aio_fsync)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
struct aiocb iocb, *piocb;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
expect_fsync(ino, 0, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
bzero(&iocb, sizeof(iocb));
iocb.aio_fildes = fd;
ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno);
ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
leak(fd);
}
/*
* fuse(4) should NOT fsync during VOP_RELEASE or VOP_INACTIVE
*
* This test only really make sense in writeback caching mode, but it should
* still pass in any cache mode.
*/
TEST_F(Fsync, close)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FSYNC);
}, Eq(true)),
_)
).Times(0);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, FH);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
close(fd);
}
TEST_F(Fsync, eio)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
expect_fsync(ino, FUSE_FSYNC_FDATASYNC, EIO);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
ASSERT_NE(0, fdatasync(fd));
ASSERT_EQ(EIO, errno);
leak(fd);
}
/*
* If the filesystem returns ENOSYS, it will be treated as success and
* subsequent calls to VOP_FSYNC will succeed automatically without being sent
* to the filesystem daemon
*/
TEST_F(Fsync, enosys)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
expect_fsync(ino, FUSE_FSYNC_FDATASYNC, ENOSYS);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
EXPECT_EQ(0, fdatasync(fd));
/* Subsequent calls shouldn't query the daemon*/
EXPECT_EQ(0, fdatasync(fd));
leak(fd);
}
TEST_F(Fsync, fdatasync)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
expect_fsync(ino, FUSE_FSYNC_FDATASYNC, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
ASSERT_EQ(0, fdatasync(fd)) << strerror(errno);
leak(fd);
}
TEST_F(Fsync, fsync)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
ssize_t bufsize = strlen(CONTENTS);
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_write(ino, bufsize, CONTENTS);
expect_fsync(ino, 0, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
ASSERT_EQ(0, fsync(fd)) << strerror(errno);
leak(fd);
}

View File

@ -0,0 +1,186 @@
/*-
* 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.
*/
extern "C" {
#include <aio.h>
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/*
* TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28.
* This bit was actually part of kernel protocol version 5.2, but never
* documented until after 7.28
*/
#ifndef FUSE_FSYNC_FDATASYNC
#define FUSE_FSYNC_FDATASYNC 1
#endif
class FsyncDir: public FuseTest {
public:
void expect_fsyncdir(uint64_t ino, uint32_t flags, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FSYNCDIR &&
in.header.nodeid == ino &&
/*
* TODO: reenable pid check after fixing
* bug 236379
*/
//(pid_t)in.header.pid == getpid() &&
in.body.fsyncdir.fh == FH &&
in.body.fsyncdir.fsync_flags == flags);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
}
};
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
TEST_F(FsyncDir, aio_fsync)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct aiocb iocb, *piocb;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_fsyncdir(ino, 0, 0);
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
bzero(&iocb, sizeof(iocb));
iocb.aio_fildes = fd;
ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno);
ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
leak(fd);
}
TEST_F(FsyncDir, eio)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_fsyncdir(ino, 0, EIO);
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_NE(0, fsync(fd));
ASSERT_EQ(EIO, errno);
leak(fd);
}
/*
* If the filesystem returns ENOSYS, it will be treated as success and
* subsequent calls to VOP_FSYNC will succeed automatically without being sent
* to the filesystem daemon
*/
TEST_F(FsyncDir, enosys)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_fsyncdir(ino, 0, ENOSYS);
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
EXPECT_EQ(0, fsync(fd)) << strerror(errno);
/* Subsequent calls shouldn't query the daemon*/
EXPECT_EQ(0, fsync(fd)) << strerror(errno);
leak(fd);
}
TEST_F(FsyncDir, fsyncdata)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_fsyncdir(ino, FUSE_FSYNC_FDATASYNC, 0);
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, fdatasync(fd)) << strerror(errno);
leak(fd);
}
/*
* Unlike regular files, the kernel doesn't know whether a directory is or
* isn't dirty, so fuse(4) should always send FUSE_FSYNCDIR on fsync(2)
*/
TEST_F(FsyncDir, fsync)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_fsyncdir(ino, 0, 0);
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, fsync(fd)) << strerror(errno);
leak(fd);
}

View File

@ -0,0 +1,300 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Getattr : public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
uint64_t size, int times, uint64_t attr_valid, uint32_t attr_valid_nsec)
{
EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
.Times(times)
.WillRepeatedly(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.attr.nlink = 1;
out.body.entry.attr_valid = attr_valid;
out.body.entry.attr_valid_nsec = attr_valid_nsec;
out.body.entry.attr.size = size;
out.body.entry.entry_valid = UINT64_MAX;
})));
}
};
class Getattr_7_8: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
FuseTest::SetUp();
}
};
/*
* If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
* should use the cached attributes, rather than query the daemon
*/
TEST_F(Getattr, attr_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.entry_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr_valid = UINT64_MAX;
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
})));
EXPECT_EQ(0, stat(FULLPATH, &sb));
/* The second stat(2) should use cached attributes */
EXPECT_EQ(0, stat(FULLPATH, &sb));
}
/*
* If getattr returns a finite but non-zero cache timeout, then we should
* discard the cached attributes and requery the daemon after the timeout
* period passes.
*/
TEST_F(Getattr, attr_cache_timeout)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr_valid_nsec = NAP_NS / 2;
out.body.attr.attr_valid = 0;
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
})));
EXPECT_EQ(0, stat(FULLPATH, &sb));
nap();
/* Timeout has expired. stat(2) should requery the daemon */
EXPECT_EQ(0, stat(FULLPATH, &sb));
}
/*
* If attr.blksize is zero, then the kernel should use a default value for
* st_blksize
*/
TEST_F(Getattr, blksize_zero)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.blksize = 0;
})));
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ((blksize_t)PAGE_SIZE, sb.st_blksize);
}
TEST_F(Getattr, enoent)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
struct stat sb;
const uint64_t ino = 42;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_NE(0, stat(FULLPATH, &sb));
EXPECT_EQ(ENOENT, errno);
}
TEST_F(Getattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.size = 1;
out.body.attr.attr.blocks = 2;
out.body.attr.attr.atime = 3;
out.body.attr.attr.mtime = 4;
out.body.attr.attr.ctime = 5;
out.body.attr.attr.atimensec = 6;
out.body.attr.attr.mtimensec = 7;
out.body.attr.attr.ctimensec = 8;
out.body.attr.attr.nlink = 9;
out.body.attr.attr.uid = 10;
out.body.attr.attr.gid = 11;
out.body.attr.attr.rdev = 12;
out.body.attr.attr.blksize = 12345;
})));
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(1, sb.st_size);
EXPECT_EQ(2, sb.st_blocks);
EXPECT_EQ(3, sb.st_atim.tv_sec);
EXPECT_EQ(6, sb.st_atim.tv_nsec);
EXPECT_EQ(4, sb.st_mtim.tv_sec);
EXPECT_EQ(7, sb.st_mtim.tv_nsec);
EXPECT_EQ(5, sb.st_ctim.tv_sec);
EXPECT_EQ(8, sb.st_ctim.tv_nsec);
EXPECT_EQ(9ull, sb.st_nlink);
EXPECT_EQ(10ul, sb.st_uid);
EXPECT_EQ(11ul, sb.st_gid);
EXPECT_EQ(12ul, sb.st_rdev);
EXPECT_EQ((blksize_t)12345, sb.st_blksize);
EXPECT_EQ(ino, sb.st_ino);
EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
//st_birthtim and st_flags are not supported by protocol 7.8. They're
//only supported as OS-specific extensions to OSX.
//EXPECT_EQ(, sb.st_birthtim);
//EXPECT_EQ(, sb.st_flags);
//FUSE can't set st_blksize until protocol 7.9
}
TEST_F(Getattr_7_8, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr.size = 1;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr_7_8);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.size = 1;
out.body.attr.attr.blocks = 2;
out.body.attr.attr.atime = 3;
out.body.attr.attr.mtime = 4;
out.body.attr.attr.ctime = 5;
out.body.attr.attr.atimensec = 6;
out.body.attr.attr.mtimensec = 7;
out.body.attr.attr.ctimensec = 8;
out.body.attr.attr.nlink = 9;
out.body.attr.attr.uid = 10;
out.body.attr.attr.gid = 11;
out.body.attr.attr.rdev = 12;
})));
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(1, sb.st_size);
EXPECT_EQ(2, sb.st_blocks);
EXPECT_EQ(3, sb.st_atim.tv_sec);
EXPECT_EQ(6, sb.st_atim.tv_nsec);
EXPECT_EQ(4, sb.st_mtim.tv_sec);
EXPECT_EQ(7, sb.st_mtim.tv_nsec);
EXPECT_EQ(5, sb.st_ctim.tv_sec);
EXPECT_EQ(8, sb.st_ctim.tv_nsec);
EXPECT_EQ(9ull, sb.st_nlink);
EXPECT_EQ(10ul, sb.st_uid);
EXPECT_EQ(11ul, sb.st_gid);
EXPECT_EQ(12ul, sb.st_rdev);
EXPECT_EQ(ino, sb.st_ino);
EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
//st_birthtim and st_flags are not supported by protocol 7.8. They're
//only supported as OS-specific extensions to OSX.
}

View File

@ -0,0 +1,790 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <sys/extattr.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/* Initial size of files used by these tests */
const off_t FILESIZE = 1000;
/* Access mode used by all directories in these tests */
const mode_t MODE = 0755;
const char FULLDIRPATH0[] = "mountpoint/some_dir";
const char RELDIRPATH0[] = "some_dir";
const char FULLDIRPATH1[] = "mountpoint/other_dir";
const char RELDIRPATH1[] = "other_dir";
static sem_t *blocked_semaphore;
static sem_t *signaled_semaphore;
static bool killer_should_sleep = false;
/* Don't do anything; all we care about is that the syscall gets interrupted */
void sigusr2_handler(int __unused sig) {
if (verbosity > 1) {
printf("Signaled! thread %p\n", pthread_self());
}
}
void* killer(void* target) {
/* Wait until the main thread is blocked in fdisp_wait_answ */
if (killer_should_sleep)
nap();
else
sem_wait(blocked_semaphore);
if (verbosity > 1)
printf("Signalling! thread %p\n", target);
pthread_kill((pthread_t)target, SIGUSR2);
if (signaled_semaphore != NULL)
sem_post(signaled_semaphore);
return(NULL);
}
class Interrupt: public FuseTest {
public:
pthread_t m_child;
Interrupt(): m_child(NULL) {};
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, FILESIZE, 1);
}
/*
* Expect a FUSE_MKDIR but don't reply. Instead, just record the unique value
* to the provided pointer
*/
void expect_mkdir(uint64_t *mkdir_unique)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).WillOnce(Invoke([=](auto in, auto &out __unused) {
*mkdir_unique = in.header.unique;
sem_post(blocked_semaphore);
}));
}
/*
* Expect a FUSE_READ but don't reply. Instead, just record the unique value
* to the provided pointer
*/
void expect_read(uint64_t ino, uint64_t *read_unique)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke([=](auto in, auto &out __unused) {
*read_unique = in.header.unique;
sem_post(blocked_semaphore);
}));
}
/*
* Expect a FUSE_WRITE but don't reply. Instead, just record the unique value
* to the provided pointer
*/
void expect_write(uint64_t ino, uint64_t *write_unique)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_WRITE &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke([=](auto in, auto &out __unused) {
*write_unique = in.header.unique;
sem_post(blocked_semaphore);
}));
}
void setup_interruptor(pthread_t target, bool sleep = false)
{
ASSERT_NE(SIG_ERR, signal(SIGUSR2, sigusr2_handler)) << strerror(errno);
killer_should_sleep = sleep;
ASSERT_EQ(0, pthread_create(&m_child, NULL, killer, (void*)target))
<< strerror(errno);
}
void SetUp() {
const int mprot = PROT_READ | PROT_WRITE;
const int mflags = MAP_ANON | MAP_SHARED;
signaled_semaphore = NULL;
blocked_semaphore = (sem_t*)mmap(NULL, sizeof(*blocked_semaphore),
mprot, mflags, -1, 0);
ASSERT_NE(MAP_FAILED, blocked_semaphore) << strerror(errno);
ASSERT_EQ(0, sem_init(blocked_semaphore, 1, 0)) << strerror(errno);
ASSERT_EQ(0, siginterrupt(SIGUSR2, 1));
FuseTest::SetUp();
}
void TearDown() {
struct sigaction sa;
if (m_child != NULL) {
pthread_join(m_child, NULL);
}
bzero(&sa, sizeof(sa));
sa.sa_handler = SIG_DFL;
sigaction(SIGUSR2, &sa, NULL);
sem_destroy(blocked_semaphore);
munmap(blocked_semaphore, sizeof(*blocked_semaphore));
FuseTest::TearDown();
}
};
class Intr: public Interrupt {};
class Nointr: public Interrupt {
void SetUp() {
m_nointr = true;
Interrupt::SetUp();
}
};
static void* mkdir0(void* arg __unused) {
ssize_t r;
r = mkdir(FULLDIRPATH0, MODE);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
static void* read1(void* arg) {
const size_t bufsize = FILESIZE;
char buf[bufsize];
int fd = (int)(intptr_t)arg;
ssize_t r;
r = read(fd, buf, bufsize);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
/*
* An interrupt operation that gets received after the original command is
* complete should generate an EAGAIN response.
*/
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
TEST_F(Intr, already_complete)
{
uint64_t ino = 42;
pthread_t self;
uint64_t mkdir_unique = 0;
Sequence seq;
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.InSequence(seq)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_mkdir(&mkdir_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in, auto &out) {
// First complete the mkdir request
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.unique = mkdir_unique;
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
out0->body.create.entry.nodeid = ino;
out.push_back(std::move(out0));
// Then, respond EAGAIN to the interrupt request
std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
out1->header.unique = in.header.unique;
out1->header.error = -EAGAIN;
out1->header.len = sizeof(out1->header);
out.push_back(std::move(out1));
}));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | MODE;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 2;
})));
setup_interruptor(self);
EXPECT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
/*
* The final syscall simply ensures that the test's main thread doesn't
* end before the daemon finishes responding to the FUSE_INTERRUPT.
*/
EXPECT_EQ(0, access(FULLDIRPATH0, F_OK)) << strerror(errno);
}
/*
* If a FUSE file system returns ENOSYS for a FUSE_INTERRUPT operation, the
* kernel should not attempt to interrupt any other operations on that mount
* point.
*/
TEST_F(Intr, enosys)
{
uint64_t ino0 = 42, ino1 = 43;;
uint64_t mkdir_unique;
pthread_t self, th0;
sem_t sem0, sem1;
void *thr0_value;
Sequence seq;
self = pthread_self();
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_mkdir(&mkdir_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke([&](auto in, auto &out) {
// reject FUSE_INTERRUPT and respond to the FUSE_MKDIR
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
out0->header.unique = in.header.unique;
out0->header.error = -ENOSYS;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
SET_OUT_HEADER_LEN(*out1, entry);
out1->body.create.entry.attr.mode = S_IFDIR | MODE;
out1->body.create.entry.nodeid = ino1;
out1->header.unique = mkdir_unique;
out.push_back(std::move(out1));
}));
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke([&](auto in, auto &out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
sem_post(&sem0);
sem_wait(&sem1);
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
out0->body.create.entry.nodeid = ino0;
out0->header.unique = in.header.unique;
out.push_back(std::move(out0));
}));
setup_interruptor(self);
/* First mkdir operation should finish synchronously */
ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
<< strerror(errno);
sem_wait(&sem0);
/*
* th0 should be blocked waiting for the fuse daemon thread.
* Signal it. No FUSE_INTERRUPT should result
*/
pthread_kill(th0, SIGUSR1);
/* Allow the daemon thread to proceed */
sem_post(&sem1);
pthread_join(th0, &thr0_value);
/* Second mkdir should've finished without error */
EXPECT_EQ(0, (intptr_t)thr0_value);
}
/*
* A FUSE filesystem is legally allowed to ignore INTERRUPT operations, and
* complete the original operation whenever it damn well pleases.
*/
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
TEST_F(Intr, ignore)
{
uint64_t ino = 42;
pthread_t self;
uint64_t mkdir_unique;
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_mkdir(&mkdir_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out) {
// Ignore FUSE_INTERRUPT; respond to the FUSE_MKDIR
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.unique = mkdir_unique;
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
out0->body.create.entry.nodeid = ino;
out.push_back(std::move(out0));
}));
setup_interruptor(self);
ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
}
/*
* A restartable operation (basically, anything except write or setextattr)
* that hasn't yet been sent to userland can be interrupted without sending
* FUSE_INTERRUPT, and will be automatically restarted.
*/
TEST_F(Intr, in_kernel_restartable)
{
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
uint64_t ino0 = 42, ino1 = 43;
int fd1;
pthread_t self, th0, th1;
sem_t sem0, sem1;
void *thr0_value, *thr1_value;
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_lookup(RELPATH1, ino1);
expect_open(ino1, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
/* Let the next write proceed */
sem_post(&sem1);
/* Pause the daemon thread so it won't read the next op */
sem_wait(&sem0);
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | MODE;
out.body.create.entry.nodeid = ino0;
})));
FuseTest::expect_read(ino1, 0, FILESIZE, 0, NULL);
fd1 = open(FULLPATH1, O_RDONLY);
ASSERT_LE(0, fd1) << strerror(errno);
/* Use a separate thread for each operation */
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
<< strerror(errno);
sem_wait(&sem1); /* Sequence the two operations */
ASSERT_EQ(0, pthread_create(&th1, NULL, read1, (void*)(intptr_t)fd1))
<< strerror(errno);
setup_interruptor(self, true);
pause(); /* Wait for signal */
/* Unstick the daemon */
ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
/* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
nap();
pthread_join(th1, &thr1_value);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr1_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
sem_destroy(&sem1);
sem_destroy(&sem0);
}
/*
* An operation that hasn't yet been sent to userland can be interrupted
* without sending FUSE_INTERRUPT. If it's a non-restartable operation (write
* or setextattr) it will return EINTR.
*/
TEST_F(Intr, in_kernel_nonrestartable)
{
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
uint64_t ino0 = 42, ino1 = 43;
int ns = EXTATTR_NAMESPACE_USER;
int fd1;
pthread_t self, th0;
sem_t sem0, sem1;
void *thr0_value;
ssize_t r;
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_lookup(RELPATH1, ino1);
expect_open(ino1, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
/* Let the next write proceed */
sem_post(&sem1);
/* Pause the daemon thread so it won't read the next op */
sem_wait(&sem0);
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | MODE;
out.body.create.entry.nodeid = ino0;
})));
fd1 = open(FULLPATH1, O_WRONLY);
ASSERT_LE(0, fd1) << strerror(errno);
/* Use a separate thread for the first write */
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
<< strerror(errno);
sem_wait(&sem1); /* Sequence the two operations */
setup_interruptor(self, true);
r = extattr_set_fd(fd1, ns, "foo", (const void*)value, value_len);
EXPECT_NE(0, r);
EXPECT_EQ(EINTR, errno);
/* Unstick the daemon */
ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
/* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
nap();
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
sem_destroy(&sem1);
sem_destroy(&sem0);
}
/*
* A syscall that gets interrupted while blocking on FUSE I/O should send a
* FUSE_INTERRUPT command to the fuse filesystem, which should then send EINTR
* in response to the _original_ operation. The kernel should ultimately
* return EINTR to userspace
*/
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
TEST_F(Intr, in_progress)
{
pthread_t self;
uint64_t mkdir_unique;
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_mkdir(&mkdir_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.error = -EINTR;
out0->header.unique = mkdir_unique;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
}));
setup_interruptor(self);
ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
EXPECT_EQ(EINTR, errno);
}
/* Reads should also be interruptible */
TEST_F(Intr, in_progress_read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const size_t bufsize = 80;
char buf[bufsize];
uint64_t ino = 42;
int fd;
pthread_t self;
uint64_t read_unique;
self = pthread_self();
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_read(ino, &read_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == read_unique);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.error = -EINTR;
out0->header.unique = read_unique;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
}));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
setup_interruptor(self);
ASSERT_EQ(-1, read(fd, buf, bufsize));
EXPECT_EQ(EINTR, errno);
}
/*
* When mounted with -o nointr, fusefs will block signals while waiting for the
* server.
*/
TEST_F(Nointr, block)
{
uint64_t ino = 42;
pthread_t self;
sem_t sem0;
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
signaled_semaphore = &sem0;
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
/* Let the killer proceed */
sem_post(blocked_semaphore);
/* Wait until after the signal has been sent */
sem_wait(signaled_semaphore);
/* Allow time for the mkdir thread to receive the signal */
nap();
/* Finally, complete the original op */
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | MODE;
out.body.create.entry.nodeid = ino;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT);
}, Eq(true)),
_)
).Times(0);
setup_interruptor(self);
ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
sem_destroy(&sem0);
}
/* FUSE_INTERRUPT operations should take priority over other pending ops */
TEST_F(Intr, priority)
{
Sequence seq;
uint64_t ino1 = 43;
uint64_t mkdir_unique;
pthread_t th0;
sem_t sem0, sem1;
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
mkdir_unique = in.header.unique;
/* Let the next mkdir proceed */
sem_post(&sem1);
/* Pause the daemon thread so it won't read the next op */
sem_wait(&sem0);
/* Finally, interrupt the original op */
out.header.error = -EINTR;
out.header.unique = mkdir_unique;
out.header.len = sizeof(out.header);
})));
/*
* FUSE_INTERRUPT should be received before the second FUSE_MKDIR,
* even though it was generated later
*/
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnErrno(EAGAIN)));
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_MKDIR);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | MODE;
out.body.create.entry.nodeid = ino1;
})));
/* Use a separate thread for the first mkdir */
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
<< strerror(errno);
signaled_semaphore = &sem0;
sem_wait(&sem1); /* Sequence the two mkdirs */
setup_interruptor(th0, true);
ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
pthread_join(th0, NULL);
sem_destroy(&sem1);
sem_destroy(&sem0);
}
/*
* If the FUSE filesystem receives the FUSE_INTERRUPT operation before
* processing the original, then it should wait for "some timeout" for the
* original operation to arrive. If not, it should send EAGAIN to the
* INTERRUPT operation, and the kernel should requeue the INTERRUPT.
*
* In this test, we'll pretend that the INTERRUPT arrives too soon, gets
* EAGAINed, then the kernel requeues it, and the second time around it
* successfully interrupts the original
*/
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
TEST_F(Intr, too_soon)
{
Sequence seq;
pthread_t self;
uint64_t mkdir_unique;
self = pthread_self();
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_mkdir(&mkdir_unique);
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke(ReturnErrno(EAGAIN)));
EXPECT_CALL(*m_mock, process(
ResultOf([&](auto in) {
return (in.header.opcode == FUSE_INTERRUPT &&
in.body.interrupt.unique == mkdir_unique);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.error = -EINTR;
out0->header.unique = mkdir_unique;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
}));
setup_interruptor(self);
ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
EXPECT_EQ(EINTR, errno);
}
// TODO: add a test where write returns EWOULDBLOCK

543
tests/sys/fs/fusefs/io.cc Normal file
View File

@ -0,0 +1,543 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
/*
* For testing I/O like fsx does, but deterministically and without a real
* underlying file system
*
* TODO: after fusefs gains the options to select cache mode for each mount
* point, run each of these tests for all cache modes.
*/
using namespace testing;
enum cache_mode {
Uncached,
Writethrough,
Writeback,
WritebackAsync
};
const char *cache_mode_to_s(enum cache_mode cm) {
switch (cm) {
case Uncached:
return "Uncached";
case Writethrough:
return "Writethrough";
case Writeback:
return "Writeback";
case WritebackAsync:
return "WritebackAsync";
default:
return "Unknown";
}
}
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
static void compare(const void *tbuf, const void *controlbuf, off_t baseofs,
ssize_t size)
{
int i;
for (i = 0; i < size; i++) {
if (((const char*)tbuf)[i] != ((const char*)controlbuf)[i]) {
off_t ofs = baseofs + i;
FAIL() << "miscompare at offset "
<< std::hex
<< std::showbase
<< ofs
<< ". expected = "
<< std::setw(2)
<< (unsigned)((const uint8_t*)controlbuf)[i]
<< " got = "
<< (unsigned)((const uint8_t*)tbuf)[i];
}
}
}
typedef tuple<bool, uint32_t, cache_mode> IoParam;
class Io: public FuseTest, public WithParamInterface<IoParam> {
public:
int m_backing_fd, m_control_fd, m_test_fd;
off_t m_filesize;
bool m_direct_io;
Io(): m_backing_fd(-1), m_control_fd(-1), m_direct_io(false) {};
void SetUp()
{
m_filesize = 0;
m_backing_fd = open("backing_file", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (m_backing_fd < 0)
FAIL() << strerror(errno);
m_control_fd = open("control", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (m_control_fd < 0)
FAIL() << strerror(errno);
srandom(22'9'1982); // Seed with my birthday
if (get<0>(GetParam()))
m_init_flags |= FUSE_ASYNC_READ;
m_maxwrite = get<1>(GetParam());
switch (get<2>(GetParam())) {
case Uncached:
m_direct_io = true;
break;
case WritebackAsync:
m_async = true;
/* FALLTHROUGH */
case Writeback:
m_init_flags |= FUSE_WRITEBACK_CACHE;
/* FALLTHROUGH */
case Writethrough:
break;
default:
FAIL() << "Unknown cache mode";
}
FuseTest::SetUp();
if (IsSkipped())
return;
if (verbosity > 0) {
printf("Test Parameters: init_flags=%#x maxwrite=%#x "
"%sasync cache=%s\n",
m_init_flags, m_maxwrite, m_async? "" : "no",
cache_mode_to_s(get<2>(GetParam())));
}
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO : 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_WRITE &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
const char *buf = (const char*)in.body.bytes +
sizeof(struct fuse_write_in);
ssize_t isize = in.body.write.size;
off_t iofs = in.body.write.offset;
ASSERT_EQ(isize, pwrite(m_backing_fd, buf, isize, iofs))
<< strerror(errno);
SET_OUT_HEADER_LEN(out, write);
out.body.write.size = isize;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
ssize_t isize = in.body.write.size;
off_t iofs = in.body.write.offset;
void *buf = out.body.bytes;
ssize_t osize;
osize = pread(m_backing_fd, buf, isize, iofs);
ASSERT_LE(0, osize) << strerror(errno);
out.header.len = sizeof(struct fuse_out_header) + osize;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
(in.body.setattr.valid & FATTR_SIZE));
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
ASSERT_EQ(0, ftruncate(m_backing_fd, in.body.setattr.size))
<< strerror(errno);
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = S_IFREG | 0755;
out.body.attr.attr.size = in.body.setattr.size;
out.body.attr.attr_valid = UINT64_MAX;
})));
/* Any test that close()s will send FUSE_FLUSH and FUSE_RELEASE */
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FLUSH &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnErrno(0)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASE &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnErrno(0)));
m_test_fd = open(FULLPATH, O_RDWR );
EXPECT_LE(0, m_test_fd) << strerror(errno);
}
void TearDown()
{
if (m_test_fd >= 0)
close(m_test_fd);
if (m_backing_fd >= 0)
close(m_backing_fd);
if (m_control_fd >= 0)
close(m_control_fd);
FuseTest::TearDown();
leak(m_test_fd);
}
void do_closeopen()
{
ASSERT_EQ(0, close(m_test_fd)) << strerror(errno);
m_test_fd = open("backing_file", O_RDWR);
ASSERT_LE(0, m_test_fd) << strerror(errno);
ASSERT_EQ(0, close(m_control_fd)) << strerror(errno);
m_control_fd = open("control", O_RDWR);
ASSERT_LE(0, m_control_fd) << strerror(errno);
}
void do_ftruncate(off_t offs)
{
ASSERT_EQ(0, ftruncate(m_test_fd, offs)) << strerror(errno);
ASSERT_EQ(0, ftruncate(m_control_fd, offs)) << strerror(errno);
m_filesize = offs;
}
void do_mapread(ssize_t size, off_t offs)
{
void *control_buf, *p;
off_t pg_offset, page_mask;
size_t map_size;
page_mask = getpagesize() - 1;
pg_offset = offs & page_mask;
map_size = pg_offset + size;
p = mmap(NULL, map_size, PROT_READ, MAP_FILE | MAP_SHARED, m_test_fd,
offs - pg_offset);
ASSERT_NE(p, MAP_FAILED) << strerror(errno);
control_buf = malloc(size);
ASSERT_NE(nullptr, control_buf) << strerror(errno);
ASSERT_EQ(size, pread(m_control_fd, control_buf, size, offs))
<< strerror(errno);
compare((void*)((char*)p + pg_offset), control_buf, offs, size);
ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno);
free(control_buf);
}
void do_read(ssize_t size, off_t offs)
{
void *test_buf, *control_buf;
ssize_t r;
test_buf = malloc(size);
ASSERT_NE(nullptr, test_buf) << strerror(errno);
control_buf = malloc(size);
ASSERT_NE(nullptr, control_buf) << strerror(errno);
errno = 0;
r = pread(m_test_fd, test_buf, size, offs);
ASSERT_NE(-1, r) << strerror(errno);
ASSERT_EQ(size, r) << "unexpected short read";
r = pread(m_control_fd, control_buf, size, offs);
ASSERT_NE(-1, r) << strerror(errno);
ASSERT_EQ(size, r) << "unexpected short read";
compare(test_buf, control_buf, offs, size);
free(control_buf);
free(test_buf);
}
void do_mapwrite(ssize_t size, off_t offs)
{
char *buf;
void *p;
off_t pg_offset, page_mask;
size_t map_size;
long i;
page_mask = getpagesize() - 1;
pg_offset = offs & page_mask;
map_size = pg_offset + size;
buf = (char*)malloc(size);
ASSERT_NE(nullptr, buf) << strerror(errno);
for (i=0; i < size; i++)
buf[i] = random();
if (offs + size > m_filesize) {
/*
* Must manually extend. vm_mmap_vnode will not implicitly
* extend a vnode
*/
do_ftruncate(offs + size);
}
p = mmap(NULL, map_size, PROT_READ | PROT_WRITE,
MAP_FILE | MAP_SHARED, m_test_fd, offs - pg_offset);
ASSERT_NE(p, MAP_FAILED) << strerror(errno);
bcopy(buf, (char*)p + pg_offset, size);
ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs))
<< strerror(errno);
free(buf);
ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno);
}
void do_write(ssize_t size, off_t offs)
{
char *buf;
long i;
buf = (char*)malloc(size);
ASSERT_NE(nullptr, buf) << strerror(errno);
for (i=0; i < size; i++)
buf[i] = random();
ASSERT_EQ(size, pwrite(m_test_fd, buf, size, offs ))
<< strerror(errno);
ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs))
<< strerror(errno);
m_filesize = std::max(m_filesize, offs + size);
free(buf);
}
};
class IoCacheable: public Io {
public:
virtual void SetUp() {
Io::SetUp();
}
};
/*
* Extend a file with dirty data in the last page of the last block.
*
* fsx -WR -P /tmp -S8 -N3 fsx.bin
*/
TEST_P(Io, extend_from_dirty_page)
{
off_t wofs = 0x21a0;
ssize_t wsize = 0xf0a8;
off_t rofs = 0xb284;
ssize_t rsize = 0x9b22;
off_t truncsize = 0x28702;
do_write(wsize, wofs);
do_ftruncate(truncsize);
do_read(rsize, rofs);
}
/*
* mapwrite into a newly extended part of a file.
*
* fsx -c 100 -i 100 -l 524288 -o 131072 -N5 -P /tmp -S19 fsx.bin
*/
TEST_P(IoCacheable, extend_by_mapwrite)
{
do_mapwrite(0x849e, 0x29a3a); /* [0x29a3a, 0x31ed7] */
do_mapwrite(0x3994, 0x3c7d8); /* [0x3c7d8, 0x4016b] */
do_read(0xf556, 0x30c16); /* [0x30c16, 0x4016b] */
}
/*
* When writing the last page of a file, it must be written synchronously.
* Otherwise the cached page can become invalid by a subsequent extend
* operation.
*
* fsx -WR -P /tmp -S642 -N3 fsx.bin
*/
TEST_P(Io, last_page)
{
do_write(0xcc77, 0x1134f); /* [0x1134f, 0x1dfc5] */
do_write(0xdfa7, 0x2096a); /* [0x2096a, 0x2e910] */
do_read(0xb5b7, 0x1a3aa); /* [0x1a3aa, 0x25960] */
}
/*
* Read a hole using mmap
*
* fsx -c 100 -i 100 -l 524288 -o 131072 -N11 -P /tmp -S14 fsx.bin
*/
TEST_P(IoCacheable, mapread_hole)
{
do_write(0x123b7, 0xf205); /* [0xf205, 0x215bb] */
do_mapread(0xeeea, 0x2f4c); /* [0x2f4c, 0x11e35] */
}
/*
* Read a hole from a block that contains some cached data.
*
* fsx -WR -P /tmp -S55 fsx.bin
*/
TEST_P(Io, read_hole_from_cached_block)
{
off_t wofs = 0x160c5;
ssize_t wsize = 0xa996;
off_t rofs = 0x472e;
ssize_t rsize = 0xd8d5;
do_write(wsize, wofs);
do_read(rsize, rofs);
}
/*
* Truncating a file into a dirty buffer should not causing anything untoward
* to happen when that buffer is eventually flushed.
*
* fsx -WR -P /tmp -S839 -d -N6 fsx.bin
*/
TEST_P(Io, truncate_into_dirty_buffer)
{
off_t wofs0 = 0x3bad7;
ssize_t wsize0 = 0x4529;
off_t wofs1 = 0xc30d;
ssize_t wsize1 = 0x5f77;
off_t truncsize0 = 0x10916;
off_t rofs = 0xdf17;
ssize_t rsize = 0x29ff;
off_t truncsize1 = 0x152b4;
do_write(wsize0, wofs0);
do_write(wsize1, wofs1);
do_ftruncate(truncsize0);
do_read(rsize, rofs);
do_ftruncate(truncsize1);
close(m_test_fd);
}
/*
* Truncating a file into a dirty buffer should not causing anything untoward
* to happen when that buffer is eventually flushed, even when the buffer's
* dirty_off is > 0.
*
* Based on this command with a few steps removed:
* fsx -WR -P /tmp -S677 -d -N8 fsx.bin
*/
TEST_P(Io, truncate_into_dirty_buffer2)
{
off_t truncsize0 = 0x344f3;
off_t wofs = 0x2790c;
ssize_t wsize = 0xd86a;
off_t truncsize1 = 0x2de38;
off_t rofs2 = 0x1fd7a;
ssize_t rsize2 = 0xc594;
off_t truncsize2 = 0x31e71;
/* Sets the file size to something larger than the next write */
do_ftruncate(truncsize0);
/*
* Creates a dirty buffer. The part in lbn 2 doesn't flush
* synchronously.
*/
do_write(wsize, wofs);
/* Truncates part of the dirty buffer created in step 2 */
do_ftruncate(truncsize1);
/* XXX ?I don't know why this is necessary? */
do_read(rsize2, rofs2);
/* Truncates the dirty buffer */
do_ftruncate(truncsize2);
close(m_test_fd);
}
/*
* Regression test for a bug introduced in r348931
*
* Sequence of operations:
* 1) The first write reads lbn so it can modify it
* 2) The first write flushes lbn 3 immediately because it's the end of file
* 3) The first write then flushes lbn 4 because it's the end of the file
* 4) The second write modifies the cached versions of lbn 3 and 4
* 5) The third write's getblkx invalidates lbn 4's B_CACHE because it's
* extending the buffer. Then it flushes lbn 4 because B_DELWRI was set but
* B_CACHE was clear.
* 6) fuse_write_biobackend erroneously called vfs_bio_clrbuf, putting the
* buffer into a weird write-only state. All read operations would return
* 0. Writes were apparently still processed, because the buffer's contents
* were correct when examined in a core dump.
* 7) The third write reads lbn 4 because cache is clear
* 9) uiomove dutifully copies new data into the buffer
* 10) The buffer's dirty is flushed to lbn 4
* 11) The read returns all zeros because of step 6.
*
* Based on:
* fsx -WR -l 524388 -o 131072 -P /tmp -S6456 -q fsx.bin
*/
TEST_P(Io, resize_a_valid_buffer_while_extending)
{
do_write(0x14530, 0x36ee6); /* [0x36ee6, 0x4b415] */
do_write(0x1507c, 0x33256); /* [0x33256, 0x482d1] */
do_write(0x175c, 0x4c03d); /* [0x4c03d, 0x4d798] */
do_read(0xe277, 0x3599c); /* [0x3599c, 0x43c12] */
close(m_test_fd);
}
INSTANTIATE_TEST_CASE_P(Io, Io,
Combine(Bool(), /* async read */
Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
Values(Uncached, Writethrough, Writeback, WritebackAsync)
)
);
INSTANTIATE_TEST_CASE_P(Io, IoCacheable,
Combine(Bool(), /* async read */
Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
Values(Writethrough, Writeback, WritebackAsync)
)
);

233
tests/sys/fs/fusefs/link.cc Normal file
View File

@ -0,0 +1,233 @@
/*-
* 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.
*/
extern "C" {
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Link: public FuseTest {
public:
void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes
+ sizeof(struct fuse_link_in);
return (in.header.opcode == FUSE_LINK &&
in.body.link.oldnodeid == ino &&
(0 == strcmp(name, relpath)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.mode = mode;
out.body.entry.attr.nlink = nlink;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
}
};
class Link_7_8: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
FuseTest::SetUp();
}
void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes
+ sizeof(struct fuse_link_in);
return (in.header.opcode == FUSE_LINK &&
in.body.link.oldnodeid == ino &&
(0 == strcmp(name, relpath)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.nodeid = ino;
out.body.entry.attr.mode = mode;
out.body.entry.attr.nlink = nlink;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, 0, 1);
}
};
/*
* A successful link should clear the parent directory's attribute cache,
* because the fuse daemon should update its mtime and ctime
*/
TEST_F(Link, clear_attr_cache)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const uint64_t ino = 42;
mode_t mode = S_IFREG | 0644;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = FUSE_ROOT_ID;
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.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.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
expect_link(ino, RELPATH, mode, 2);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
EXPECT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
}
TEST_F(Link, emlink)
{
const char FULLPATH[] = "mountpoint/lnk";
const char RELPATH[] = "lnk";
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
uint64_t dst_ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_lookup(RELDST, dst_ino);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes
+ sizeof(struct fuse_link_in);
return (in.header.opcode == FUSE_LINK &&
in.body.link.oldnodeid == dst_ino &&
(0 == strcmp(name, RELPATH)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EMLINK)));
EXPECT_EQ(-1, link(FULLDST, FULLPATH));
EXPECT_EQ(EMLINK, errno);
}
TEST_F(Link, ok)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const uint64_t ino = 42;
mode_t mode = S_IFREG | 0644;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.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.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
expect_link(ino, RELPATH, mode, 2);
ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
// Check that the original file's nlink count has increased.
ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno);
EXPECT_EQ(2ul, sb.st_nlink);
}
TEST_F(Link_7_8, ok)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const uint64_t ino = 42;
mode_t mode = S_IFREG | 0644;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
expect_link(ino, RELPATH, mode, 2);
ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
// Check that the original file's nlink count has increased.
ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno);
EXPECT_EQ(2ul, sb.st_nlink);
}

View File

@ -0,0 +1,478 @@
/*-
* 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.
*/
extern "C" {
#include <sys/file.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
/* This flag value should probably be defined in fuse_kernel.h */
#define OFFSET_MAX 0x7fffffffffffffffLL
using namespace testing;
/* For testing filesystems without posix locking support */
class Fallback: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
}
};
/* For testing filesystems with posix locking support */
class Locks: public Fallback {
virtual void SetUp() {
m_init_flags = FUSE_POSIX_LOCKS;
Fallback::SetUp();
}
};
class Fcntl: public Locks {
public:
void expect_setlk(uint64_t ino, pid_t pid, uint64_t start, uint64_t end,
uint32_t type, int err)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETLK &&
in.header.nodeid == ino &&
in.body.setlk.fh == FH &&
in.body.setlkw.owner == (uint32_t)pid &&
in.body.setlkw.lk.start == start &&
in.body.setlkw.lk.end == end &&
in.body.setlkw.lk.type == type &&
in.body.setlkw.lk.pid == (uint64_t)pid);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(err)));
}
};
class Flock: public Locks {
public:
void expect_setlk(uint64_t ino, uint32_t type, int err)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETLK &&
in.header.nodeid == ino &&
in.body.setlk.fh == FH &&
/*
* The owner should be set to the address of
* the vnode. That's hard to verify.
*/
/* in.body.setlk.owner == ??? && */
in.body.setlk.lk.type == type);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(err)));
}
};
class FlockFallback: public Fallback {};
class GetlkFallback: public Fallback {};
class Getlk: public Fcntl {};
class SetlkFallback: public Fallback {};
class Setlk: public Fcntl {};
class SetlkwFallback: public Fallback {};
class Setlkw: public Fcntl {};
/*
* If the fuse filesystem does not support flock locks, then the kernel should
* fall back to local locks.
*/
TEST_F(FlockFallback, local)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
leak(fd);
}
/*
* Even if the fuse file system supports POSIX locks, we must implement flock
* locks locally until protocol 7.17. Protocol 7.9 added partial buggy support
* but we won't implement that.
*/
TEST_F(Flock, local)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
leak(fd);
}
/* Set a new flock lock with FUSE_SETLK */
/* TODO: enable after upgrading to protocol 7.17 */
TEST_F(Flock, DISABLED_set)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, F_WRLCK, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
leak(fd);
}
/* Fail to set a flock lock in non-blocking mode */
/* TODO: enable after upgrading to protocol 7.17 */
TEST_F(Flock, DISABLED_eagain)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, F_WRLCK, EAGAIN);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_NE(0, flock(fd, LOCK_EX | LOCK_NB));
ASSERT_EQ(EAGAIN, errno);
leak(fd);
}
/*
* If the fuse filesystem does not support posix file locks, then the kernel
* should fall back to local locks.
*/
TEST_F(GetlkFallback, local)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = getpid();
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
leak(fd);
}
/*
* If the filesystem has no locks that fit the description, the filesystem
* should return F_UNLCK
*/
TEST_F(Getlk, no_locks)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETLK &&
in.header.nodeid == ino &&
in.body.getlk.fh == FH &&
in.body.getlk.owner == (uint32_t)pid &&
in.body.getlk.lk.start == 10 &&
in.body.getlk.lk.end == 1009 &&
in.body.getlk.lk.type == F_RDLCK &&
in.body.getlk.lk.pid == (uint64_t)pid);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
SET_OUT_HEADER_LEN(out, getlk);
out.body.getlk.lk = in.body.getlk.lk;
out.body.getlk.lk.type = F_UNLCK;
})));
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
ASSERT_EQ(F_UNLCK, fl.l_type);
leak(fd);
}
/* A different pid does have a lock */
TEST_F(Getlk, lock_exists)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
pid_t pid2 = 1235;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETLK &&
in.header.nodeid == ino &&
in.body.getlk.fh == FH &&
in.body.getlk.owner == (uint32_t)pid &&
in.body.getlk.lk.start == 10 &&
in.body.getlk.lk.end == 1009 &&
in.body.getlk.lk.type == F_RDLCK &&
in.body.getlk.lk.pid == (uint64_t)pid);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, getlk);
out.body.getlk.lk.start = 100;
out.body.getlk.lk.end = 199;
out.body.getlk.lk.type = F_WRLCK;
out.body.getlk.lk.pid = (uint32_t)pid2;;
})));
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
EXPECT_EQ(100, fl.l_start);
EXPECT_EQ(100, fl.l_len);
EXPECT_EQ(pid2, fl.l_pid);
EXPECT_EQ(F_WRLCK, fl.l_type);
EXPECT_EQ(SEEK_SET, fl.l_whence);
EXPECT_EQ(0, fl.l_sysid);
leak(fd);
}
/*
* If the fuse filesystem does not support posix file locks, then the kernel
* should fall back to local locks.
*/
TEST_F(SetlkFallback, local)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = getpid();
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
leak(fd);
}
/* Set a new lock with FUSE_SETLK */
TEST_F(Setlk, set)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, pid, 10, 1009, F_RDLCK, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
leak(fd);
}
/* l_len = 0 is a flag value that means to lock until EOF */
TEST_F(Setlk, set_eof)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, pid, 10, OFFSET_MAX, F_RDLCK, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 0;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
leak(fd);
}
/* Fail to set a new lock with FUSE_SETLK due to a conflict */
TEST_F(Setlk, eagain)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, pid, 10, 1009, F_RDLCK, EAGAIN);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_EQ(-1, fcntl(fd, F_SETLK, &fl));
ASSERT_EQ(EAGAIN, errno);
leak(fd);
}
/*
* If the fuse filesystem does not support posix file locks, then the kernel
* should fall back to local locks.
*/
TEST_F(SetlkwFallback, local)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = getpid();
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
leak(fd);
}
/*
* Set a new lock with FUSE_SETLK. If the lock is not available, then the
* command should block. But to the kernel, that's the same as just being
* slow, so we don't need a separate test method
*/
TEST_F(Setlkw, set)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
struct flock fl;
int fd;
pid_t pid = 1234;
expect_lookup(RELPATH, ino);
expect_open(ino, 0, 1);
expect_setlk(ino, pid, 10, 1009, F_RDLCK, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 10;
fl.l_len = 1000;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
leak(fd);
}

View File

@ -0,0 +1,381 @@
/*-
* 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.
*/
extern "C" {
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Lookup: public FuseTest {};
class Lookup_7_8: public Lookup {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
Lookup::SetUp();
}
};
/*
* If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs
* should use the cached attributes, rather than query the daemon
*/
TEST_F(Lookup, attr_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const uint64_t generation = 13;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.ino = ino; // Must match nodeid
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.attr.size = 1;
out.body.entry.attr.blocks = 2;
out.body.entry.attr.atime = 3;
out.body.entry.attr.mtime = 4;
out.body.entry.attr.ctime = 5;
out.body.entry.attr.atimensec = 6;
out.body.entry.attr.mtimensec = 7;
out.body.entry.attr.ctimensec = 8;
out.body.entry.attr.nlink = 9;
out.body.entry.attr.uid = 10;
out.body.entry.attr.gid = 11;
out.body.entry.attr.rdev = 12;
out.body.entry.generation = generation;
})));
/* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(1, sb.st_size);
EXPECT_EQ(2, sb.st_blocks);
EXPECT_EQ(3, sb.st_atim.tv_sec);
EXPECT_EQ(6, sb.st_atim.tv_nsec);
EXPECT_EQ(4, sb.st_mtim.tv_sec);
EXPECT_EQ(7, sb.st_mtim.tv_nsec);
EXPECT_EQ(5, sb.st_ctim.tv_sec);
EXPECT_EQ(8, sb.st_ctim.tv_nsec);
EXPECT_EQ(9ull, sb.st_nlink);
EXPECT_EQ(10ul, sb.st_uid);
EXPECT_EQ(11ul, sb.st_gid);
EXPECT_EQ(12ul, sb.st_rdev);
EXPECT_EQ(ino, sb.st_ino);
EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
// fuse(4) does not _yet_ support inode generations
//EXPECT_EQ(generation, sb.st_gen);
//st_birthtim and st_flags are not supported by protocol 7.8. They're
//only supported as OS-specific extensions to OSX.
//EXPECT_EQ(, sb.st_birthtim);
//EXPECT_EQ(, sb.st_flags);
//FUSE can't set st_blksize until protocol 7.9
}
/*
* If lookup returns a finite but non-zero cache timeout, then we should discard
* the cached attributes and requery the daemon.
*/
TEST_F(Lookup, attr_cache_timeout)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino; // Must match nodeid
out.body.entry.attr.mode = S_IFREG | 0644;
})));
/* access(2) will issue a VOP_LOOKUP and fill the attr cache */
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
/* Next access(2) will use the cached attributes */
nap();
/* The cache has timed out; VOP_GETATTR should query the daemon*/
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
}
TEST_F(Lookup, dot)
{
const char FULLPATH[] = "mountpoint/some_dir/.";
const char RELDIRPATH[] = "some_dir";
uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, 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;
})));
/*
* access(2) is one of the few syscalls that will not (always) follow
* up a successful VOP_LOOKUP with another VOP.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
TEST_F(Lookup, dotdot)
{
const char FULLPATH[] = "mountpoint/some_dir/..";
const char RELDIRPATH[] = "some_dir";
EXPECT_LOOKUP(FUSE_ROOT_ID, 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 = 14;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
/*
* access(2) is one of the few syscalls that will not (always) follow
* up a successful VOP_LOOKUP with another VOP.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
TEST_F(Lookup, enoent)
{
const char FULLPATH[] = "mountpoint/does_not_exist";
const char RELPATH[] = "does_not_exist";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_NE(0, access(FULLPATH, F_OK));
EXPECT_EQ(ENOENT, errno);
}
TEST_F(Lookup, enotdir)
{
const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt";
const char RELPATH[] = "not_a_dir";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = 42;
})));
ASSERT_EQ(-1, access(FULLPATH, F_OK));
ASSERT_EQ(ENOTDIR, errno);
}
/*
* If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs
* should use the cached inode rather than requery the daemon
*/
TEST_F(Lookup, entry_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = 14;
})));
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
/* The second access(2) should use the cache */
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
/*
* If the daemon returns an error of 0 and an inode of 0, that's a flag for
* "ENOENT and cache it" with the given entry_timeout
*/
TEST_F(Lookup, entry_cache_negative)
{
struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist")
.Times(1)
.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)));
EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
EXPECT_EQ(ENOENT, errno);
EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
EXPECT_EQ(ENOENT, errno);
}
/* Negative entry caches should timeout, too */
TEST_F(Lookup, entry_cache_negative_timeout)
{
const char *RELPATH = "does_not_exist";
const char *FULLPATH = "mountpoint/does_not_exist";
struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2};
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(2)
.WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid)));
EXPECT_NE(0, access(FULLPATH, F_OK));
EXPECT_EQ(ENOENT, errno);
nap();
/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
EXPECT_NE(0, access(FULLPATH, F_OK));
EXPECT_EQ(ENOENT, errno);
}
/*
* If lookup returns a finite but non-zero entry cache timeout, then we should
* discard the cached inode and requery the daemon
*/
TEST_F(Lookup, entry_cache_timeout)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.entry_valid_nsec = NAP_NS / 2;
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = 14;
})));
/* access(2) will issue a VOP_LOOKUP and fill the entry cache */
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
/* Next access(2) will use the cached entry */
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
nap();
/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
TEST_F(Lookup, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = 14;
})));
/*
* access(2) is one of the few syscalls that will not (always) follow
* up a successful VOP_LOOKUP with another VOP.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
// Lookup in a subdirectory of the fuse mount
TEST_F(Lookup, subdir)
{
const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
const char DIRPATH[] = "some_dir";
const char RELPATH[] = "some_file.txt";
uint64_t dir_ino = 2;
uint64_t file_ino = 3;
EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
.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 = dir_ino;
})));
EXPECT_LOOKUP(dir_ino, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = file_ino;
})));
/*
* access(2) is one of the few syscalls that will not (always) follow
* up a successful VOP_LOOKUP with another VOP.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
/*
* The server returns two different vtypes for the same nodeid. This is a bad
* server! But we shouldn't crash.
*/
TEST_F(Lookup, vtype_conflict)
{
const char FIRSTFULLPATH[] = "mountpoint/foo";
const char SECONDFULLPATH[] = "mountpoint/bar";
const char FIRSTRELPATH[] = "foo";
const char SECONDRELPATH[] = "bar";
uint64_t ino = 42;
expect_lookup(FIRSTRELPATH, ino, S_IFREG | 0644, 0, 1, UINT64_MAX);
expect_lookup(SECONDRELPATH, ino, S_IFDIR | 0755, 0, 1, UINT64_MAX);
ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno);
ASSERT_EQ(-1, access(SECONDFULLPATH, F_OK));
ASSERT_EQ(EAGAIN, errno);
}
TEST_F(Lookup_7_8, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = 14;
})));
/*
* access(2) is one of the few syscalls that will not (always) follow
* up a successful VOP_LOOKUP with another VOP.
*/
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}

View File

@ -0,0 +1,222 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Mkdir: public FuseTest {};
class Mkdir_7_8: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
FuseTest::SetUp();
}
};
/*
* EMLINK is possible on filesystems that limit the number of hard links to a
* single file, like early versions of BtrFS
*/
TEST_F(Mkdir, emlink)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
mode_t mode = 0755;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mkdir_in);
return (in.header.opcode == FUSE_MKDIR &&
in.body.mkdir.mode == (S_IFDIR | mode) &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EMLINK)));
ASSERT_NE(1, mkdir(FULLPATH, mode));
ASSERT_EQ(EMLINK, errno);
}
/*
* Creating a new directory after FUSE_LOOKUP returned a negative cache entry
*/
TEST_F(Mkdir, entry_cache_negative)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = 0755;
uint64_t ino = 42;
/*
* Set entry_valid = 0 because this test isn't concerned with whether
* or not we actually cache negative entries, only with whether we
* interpret negative cache responses correctly.
*/
struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
/* mkdir will first do a LOOKUP, adding a negative cache entry */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(ReturnNegativeCache(&entry_valid));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_open_in);
return (in.header.opcode == FUSE_MKDIR &&
in.body.mkdir.mode == (S_IFDIR | mode) &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
}
/*
* Creating a new directory should purge any negative namecache entries
*/
TEST_F(Mkdir, entry_cache_negative_purge)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
mode_t mode = 0755;
uint64_t ino = 42;
struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
/* mkdir will first do a LOOKUP, adding a negative cache entry */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(1)
.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
.RetiresOnSaturation();
/* Then the MKDIR should purge the negative cache entry */
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_open_in);
return (in.header.opcode == FUSE_MKDIR &&
in.body.mkdir.mode == (S_IFDIR | mode) &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | mode;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
/* Finally, a subsequent lookup should query the daemon */
expect_lookup(RELPATH, ino, S_IFDIR | mode, 0, 1);
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
TEST_F(Mkdir, ok)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
mode_t mode = 0755;
uint64_t ino = 42;
mode_t mask;
mask = umask(0);
(void)umask(mask);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mkdir_in);
return (in.header.opcode == FUSE_MKDIR &&
in.body.mkdir.mode == (S_IFDIR | mode) &&
in.body.mkdir.umask == mask &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.create.entry.attr.mode = S_IFDIR | mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
}
TEST_F(Mkdir_7_8, ok)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
mode_t mode = 0755;
uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mkdir_in);
return (in.header.opcode == FUSE_MKDIR &&
in.body.mkdir.mode == (S_IFDIR | mode) &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.create.entry.attr.mode = S_IFDIR | mode;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
}

View File

@ -0,0 +1,238 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
#ifndef VNOVAL
#define VNOVAL (-1) /* Defined in sys/vnode.h */
#endif
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
class Mknod: public FuseTest {
mode_t m_oldmask;
const static mode_t c_umask = 022;
public:
virtual void SetUp() {
m_oldmask = umask(c_umask);
if (geteuid() != 0) {
GTEST_SKIP() << "Only root may use most mknod(2) variations";
}
FuseTest::SetUp();
}
virtual void TearDown() {
FuseTest::TearDown();
(void)umask(m_oldmask);
}
/* Test an OK creation of a file with the given mode and device number */
void expect_mknod(mode_t mode, dev_t dev) {
uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mknod_in);
return (in.header.opcode == FUSE_MKNOD &&
in.body.mknod.mode == mode &&
in.body.mknod.rdev == (uint32_t)dev &&
in.body.mknod.umask == c_umask &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).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.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.rdev = dev;
})));
}
};
class Mknod_7_11: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 11;
if (geteuid() != 0) {
GTEST_SKIP() << "Only root may use most mknod(2) variations";
}
FuseTest::SetUp();
}
void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
{
FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1);
}
/* Test an OK creation of a file with the given mode and device number */
void expect_mknod(mode_t mode, dev_t dev) {
uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
FUSE_COMPAT_MKNOD_IN_SIZE;
return (in.header.opcode == FUSE_MKNOD &&
in.body.mknod.mode == mode &&
in.body.mknod.rdev == (uint32_t)dev &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).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.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.rdev = dev;
})));
}
};
/*
* mknod(2) should be able to create block devices on a FUSE filesystem. Even
* though FreeBSD doesn't use block devices, this is useful when copying media
* from or preparing media for other operating systems.
*/
TEST_F(Mknod, blk)
{
mode_t mode = S_IFBLK | 0755;
dev_t rdev = 0xfe00; /* /dev/vda's device number on Linux */
expect_mknod(mode, rdev);
EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
}
TEST_F(Mknod, chr)
{
mode_t mode = S_IFCHR | 0755;
dev_t rdev = 54; /* /dev/fuse's device number */
expect_mknod(mode, rdev);
EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
}
/*
* The daemon is responsible for checking file permissions (unless the
* default_permissions mount option was used)
*/
TEST_F(Mknod, eperm)
{
mode_t mode = S_IFIFO | 0755;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_mknod_in);
return (in.header.opcode == FUSE_MKNOD &&
in.body.mknod.mode == mode &&
(0 == strcmp(RELPATH, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EPERM)));
EXPECT_NE(0, mkfifo(FULLPATH, mode));
EXPECT_EQ(EPERM, errno);
}
TEST_F(Mknod, fifo)
{
mode_t mode = S_IFIFO | 0755;
dev_t rdev = VNOVAL; /* Fifos don't have device numbers */
expect_mknod(mode, rdev);
EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
}
/*
* Create a unix-domain socket.
*
* This test case doesn't actually need root privileges.
*/
TEST_F(Mknod, socket)
{
mode_t mode = S_IFSOCK | 0755;
struct sockaddr_un sa;
int fd;
dev_t rdev = -1; /* Really it's a don't care */
expect_mknod(mode, rdev);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, fd) << strerror(errno);
sa.sun_family = AF_UNIX;
strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)))
<< strerror(errno);
}
/*
* fusefs(5) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a
* feature, not a bug
*/
TEST_F(Mknod, DISABLED_whiteout)
{
mode_t mode = S_IFWHT | 0755;
dev_t rdev = VNOVAL; /* whiteouts don't have device numbers */
expect_mknod(mode, rdev);
EXPECT_EQ(0, mknod(FULLPATH, mode, 0)) << strerror(errno);
}
/* A server built at protocol version 7.11 or earlier can still use mknod */
TEST_F(Mknod_7_11, fifo)
{
mode_t mode = S_IFIFO | 0755;
dev_t rdev = VNOVAL;
expect_mknod(mode, rdev);
EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
}

View File

@ -0,0 +1,733 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/user.h>
#include <fcntl.h>
#include <libutil.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include "mntopts.h" // for build_iovec
}
#include <cinttypes>
#include <gtest/gtest.h>
#include "mockfs.hh"
using namespace testing;
int verbosity = 0;
const char* opcode2opname(uint32_t opcode)
{
const int NUM_OPS = 39;
const char* table[NUM_OPS] = {
"Unknown (opcode 0)",
"LOOKUP",
"FORGET",
"GETATTR",
"SETATTR",
"READLINK",
"SYMLINK",
"Unknown (opcode 7)",
"MKNOD",
"MKDIR",
"UNLINK",
"RMDIR",
"RENAME",
"LINK",
"OPEN",
"READ",
"WRITE",
"STATFS",
"RELEASE",
"Unknown (opcode 19)",
"FSYNC",
"SETXATTR",
"GETXATTR",
"LISTXATTR",
"REMOVEXATTR",
"FLUSH",
"INIT",
"OPENDIR",
"READDIR",
"RELEASEDIR",
"FSYNCDIR",
"GETLK",
"SETLK",
"SETLKW",
"ACCESS",
"CREATE",
"INTERRUPT",
"BMAP",
"DESTROY"
};
if (opcode >= NUM_OPS)
return ("Unknown (opcode > max)");
else
return (table[opcode]);
}
ProcessMockerT
ReturnErrno(int error)
{
return([=](auto in, auto &out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.unique = in.header.unique;
out0->header.error = -error;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
});
}
/* Helper function used for returning negative cache entries for LOOKUP */
ProcessMockerT
ReturnNegativeCache(const struct timespec *entry_valid)
{
return([=](auto in, auto &out) {
/* nodeid means ENOENT and cache it */
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->body.entry.nodeid = 0;
out0->header.unique = in.header.unique;
out0->header.error = 0;
out0->body.entry.entry_valid = entry_valid->tv_sec;
out0->body.entry.entry_valid_nsec = entry_valid->tv_nsec;
SET_OUT_HEADER_LEN(*out0, entry);
out.push_back(std::move(out0));
});
}
ProcessMockerT
ReturnImmediate(std::function<void(const mockfs_buf_in& in,
struct mockfs_buf_out &out)> f)
{
return([=](auto& in, auto &out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.unique = in.header.unique;
f(in, *out0);
out.push_back(std::move(out0));
});
}
void sigint_handler(int __unused sig) {
// Don't do anything except interrupt the daemon's read(2) call
}
void MockFS::debug_request(const mockfs_buf_in &in)
{
printf("%-11s ino=%2" PRIu64, opcode2opname(in.header.opcode),
in.header.nodeid);
if (verbosity > 1) {
printf(" uid=%5u gid=%5u pid=%5u unique=%" PRIu64 " len=%u",
in.header.uid, in.header.gid, in.header.pid,
in.header.unique, in.header.len);
}
switch (in.header.opcode) {
const char *name, *value;
case FUSE_ACCESS:
printf(" mask=%#x", in.body.access.mask);
break;
case FUSE_BMAP:
printf(" block=%" PRIx64 " blocksize=%#x",
in.body.bmap.block, in.body.bmap.blocksize);
break;
case FUSE_CREATE:
if (m_kernel_minor_version >= 12)
name = (const char*)in.body.bytes +
sizeof(fuse_create_in);
else
name = (const char*)in.body.bytes +
sizeof(fuse_open_in);
printf(" flags=%#x name=%s",
in.body.open.flags, name);
break;
case FUSE_FLUSH:
printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64,
in.body.flush.fh,
in.body.flush.lock_owner);
break;
case FUSE_FORGET:
printf(" nlookup=%" PRIu64, in.body.forget.nlookup);
break;
case FUSE_FSYNC:
printf(" flags=%#x", in.body.fsync.fsync_flags);
break;
case FUSE_FSYNCDIR:
printf(" flags=%#x", in.body.fsyncdir.fsync_flags);
break;
case FUSE_INTERRUPT:
printf(" unique=%" PRIu64, in.body.interrupt.unique);
break;
case FUSE_LINK:
printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid);
break;
case FUSE_LOOKUP:
printf(" %s", in.body.lookup);
break;
case FUSE_MKDIR:
name = (const char*)in.body.bytes +
sizeof(fuse_mkdir_in);
printf(" name=%s mode=%#o umask=%#o", name,
in.body.mkdir.mode, in.body.mkdir.umask);
break;
case FUSE_MKNOD:
if (m_kernel_minor_version >= 12)
name = (const char*)in.body.bytes +
sizeof(fuse_mknod_in);
else
name = (const char*)in.body.bytes +
FUSE_COMPAT_MKNOD_IN_SIZE;
printf(" mode=%#o rdev=%x umask=%#o name=%s",
in.body.mknod.mode, in.body.mknod.rdev,
in.body.mknod.umask, name);
break;
case FUSE_OPEN:
printf(" flags=%#x", in.body.open.flags);
break;
case FUSE_OPENDIR:
printf(" flags=%#x", in.body.opendir.flags);
break;
case FUSE_READ:
printf(" offset=%" PRIu64 " size=%u",
in.body.read.offset,
in.body.read.size);
if (verbosity > 1)
printf(" flags=%#x", in.body.read.flags);
break;
case FUSE_READDIR:
printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u",
in.body.readdir.fh, in.body.readdir.offset,
in.body.readdir.size);
break;
case FUSE_RELEASE:
printf(" fh=%#" PRIx64 " flags=%#x lock_owner=%" PRIu64,
in.body.release.fh,
in.body.release.flags,
in.body.release.lock_owner);
break;
case FUSE_SETATTR:
if (verbosity <= 1) {
printf(" valid=%#x", in.body.setattr.valid);
break;
}
if (in.body.setattr.valid & FATTR_MODE)
printf(" mode=%#o", in.body.setattr.mode);
if (in.body.setattr.valid & FATTR_UID)
printf(" uid=%u", in.body.setattr.uid);
if (in.body.setattr.valid & FATTR_GID)
printf(" gid=%u", in.body.setattr.gid);
if (in.body.setattr.valid & FATTR_SIZE)
printf(" size=%" PRIu64, in.body.setattr.size);
if (in.body.setattr.valid & FATTR_ATIME)
printf(" atime=%" PRIu64 ".%u",
in.body.setattr.atime,
in.body.setattr.atimensec);
if (in.body.setattr.valid & FATTR_MTIME)
printf(" mtime=%" PRIu64 ".%u",
in.body.setattr.mtime,
in.body.setattr.mtimensec);
if (in.body.setattr.valid & FATTR_FH)
printf(" fh=%" PRIu64 "", in.body.setattr.fh);
break;
case FUSE_SETLK:
printf(" fh=%#" PRIx64 " owner=%" PRIu64
" type=%u pid=%u",
in.body.setlk.fh, in.body.setlk.owner,
in.body.setlk.lk.type,
in.body.setlk.lk.pid);
if (verbosity >= 2) {
printf(" range=[%" PRIu64 "-%" PRIu64 "]",
in.body.setlk.lk.start,
in.body.setlk.lk.end);
}
break;
case FUSE_SETXATTR:
/*
* In theory neither the xattr name and value need be
* ASCII, but in this test suite they always are.
*/
name = (const char*)in.body.bytes +
sizeof(fuse_setxattr_in);
value = name + strlen(name) + 1;
printf(" %s=%s", name, value);
break;
case FUSE_WRITE:
printf(" fh=%#" PRIx64 " offset=%" PRIu64
" size=%u write_flags=%u",
in.body.write.fh,
in.body.write.offset, in.body.write.size,
in.body.write.write_flags);
if (verbosity > 1)
printf(" flags=%#x", in.body.write.flags);
break;
default:
break;
}
printf("\n");
}
/*
* Debug a FUSE response.
*
* This is mostly useful for asynchronous notifications, which don't correspond
* to any request
*/
void MockFS::debug_response(const mockfs_buf_out &out) {
const char *name;
if (verbosity == 0)
return;
switch (out.header.error) {
case FUSE_NOTIFY_INVAL_ENTRY:
name = (const char*)out.body.bytes +
sizeof(fuse_notify_inval_entry_out);
printf("<- INVAL_ENTRY parent=%" PRIu64 " %s\n",
out.body.inval_entry.parent, name);
break;
case FUSE_NOTIFY_INVAL_INODE:
printf("<- INVAL_INODE ino=%" PRIu64 " off=%" PRIi64
" len=%" PRIi64 "\n",
out.body.inval_inode.ino,
out.body.inval_inode.off,
out.body.inval_inode.len);
break;
case FUSE_NOTIFY_STORE:
printf("<- STORE ino=%" PRIu64 " off=%" PRIu64
" size=%" PRIu32 "\n",
out.body.store.nodeid,
out.body.store.offset,
out.body.store.size);
break;
default:
break;
}
}
MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags,
uint32_t kernel_minor_version, uint32_t max_write, bool async,
bool noclusterr, unsigned time_gran, bool nointr)
{
struct sigaction sa;
struct iovec *iov = NULL;
int iovlen = 0;
char fdstr[15];
const bool trueval = true;
m_daemon_id = NULL;
m_kernel_minor_version = kernel_minor_version;
m_maxreadahead = max_readahead;
m_maxwrite = max_write;
m_nready = -1;
m_pm = pm;
m_time_gran = time_gran;
m_quit = false;
if (m_pm == KQ)
m_kq = kqueue();
else
m_kq = -1;
/*
* Kyua sets pwd to a testcase-unique tempdir; no need to use
* mkdtemp
*/
/*
* googletest doesn't allow ASSERT_ in constructors, so we must throw
* instead.
*/
if (mkdir("mountpoint" , 0755) && errno != EEXIST)
throw(std::system_error(errno, std::system_category(),
"Couldn't make mountpoint directory"));
switch (m_pm) {
case BLOCKING:
m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
break;
default:
m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR | O_NONBLOCK);
break;
}
if (m_fuse_fd < 0)
throw(std::system_error(errno, std::system_category(),
"Couldn't open /dev/fuse"));
m_pid = getpid();
m_child_pid = -1;
build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1);
build_iovec(&iov, &iovlen, "fspath",
__DECONST(void *, "mountpoint"), -1);
build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
sprintf(fdstr, "%d", m_fuse_fd);
build_iovec(&iov, &iovlen, "fd", fdstr, -1);
if (allow_other) {
build_iovec(&iov, &iovlen, "allow_other",
__DECONST(void*, &trueval), sizeof(bool));
}
if (default_permissions) {
build_iovec(&iov, &iovlen, "default_permissions",
__DECONST(void*, &trueval), sizeof(bool));
}
if (push_symlinks_in) {
build_iovec(&iov, &iovlen, "push_symlinks_in",
__DECONST(void*, &trueval), sizeof(bool));
}
if (ro) {
build_iovec(&iov, &iovlen, "ro",
__DECONST(void*, &trueval), sizeof(bool));
}
if (async) {
build_iovec(&iov, &iovlen, "async", __DECONST(void*, &trueval),
sizeof(bool));
}
if (noclusterr) {
build_iovec(&iov, &iovlen, "noclusterr",
__DECONST(void*, &trueval), sizeof(bool));
}
if (nointr) {
build_iovec(&iov, &iovlen, "nointr",
__DECONST(void*, &trueval), sizeof(bool));
} else {
build_iovec(&iov, &iovlen, "intr",
__DECONST(void*, &trueval), sizeof(bool));
}
if (nmount(iov, iovlen, 0))
throw(std::system_error(errno, std::system_category(),
"Couldn't mount filesystem"));
// Setup default handler
ON_CALL(*this, process(_, _))
.WillByDefault(Invoke(this, &MockFS::process_default));
init(flags);
bzero(&sa, sizeof(sa));
sa.sa_handler = sigint_handler;
sa.sa_flags = 0; /* Don't set SA_RESTART! */
if (0 != sigaction(SIGUSR1, &sa, NULL))
throw(std::system_error(errno, std::system_category(),
"Couldn't handle SIGUSR1"));
if (pthread_create(&m_daemon_id, NULL, service, (void*)this))
throw(std::system_error(errno, std::system_category(),
"Couldn't Couldn't start fuse thread"));
}
MockFS::~MockFS() {
kill_daemon();
if (m_daemon_id != NULL) {
pthread_join(m_daemon_id, NULL);
m_daemon_id = NULL;
}
::unmount("mountpoint", MNT_FORCE);
rmdir("mountpoint");
if (m_kq >= 0)
close(m_kq);
}
void MockFS::init(uint32_t flags) {
std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in);
std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
read_request(*in);
ASSERT_EQ(FUSE_INIT, in->header.opcode);
out->header.unique = in->header.unique;
out->header.error = 0;
out->body.init.major = FUSE_KERNEL_VERSION;
out->body.init.minor = m_kernel_minor_version;;
out->body.init.flags = in->body.init.flags & flags;
out->body.init.max_write = m_maxwrite;
out->body.init.max_readahead = m_maxreadahead;
if (m_kernel_minor_version < 23) {
SET_OUT_HEADER_LEN(*out, init_7_22);
} else {
out->body.init.time_gran = m_time_gran;
SET_OUT_HEADER_LEN(*out, init);
}
write(m_fuse_fd, out.get(), out->header.len);
}
void MockFS::kill_daemon() {
m_quit = true;
if (m_daemon_id != NULL)
pthread_kill(m_daemon_id, SIGUSR1);
// Closing the /dev/fuse file descriptor first allows unmount to
// succeed even if the daemon doesn't correctly respond to commands
// during the unmount sequence.
close(m_fuse_fd);
m_fuse_fd = -1;
}
void MockFS::loop() {
std::vector<std::unique_ptr<mockfs_buf_out>> out;
std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in);
ASSERT_TRUE(in != NULL);
while (!m_quit) {
bzero(in.get(), sizeof(*in));
read_request(*in);
if (m_quit)
break;
if (verbosity > 0)
debug_request(*in);
if (pid_ok((pid_t)in->header.pid)) {
process(*in, out);
} else {
/*
* Reject any requests from unknown processes. Because
* we actually do mount a filesystem, plenty of
* unrelated system daemons may try to access it.
*/
if (verbosity > 1)
printf("\tREJECTED (wrong pid %d)\n",
in->header.pid);
process_default(*in, out);
}
for (auto &it: out)
write_response(*it);
out.clear();
}
}
int MockFS::notify_inval_entry(ino_t parent, const char *name, size_t namelen)
{
std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
out->header.unique = 0; /* 0 means asynchronous notification */
out->header.error = FUSE_NOTIFY_INVAL_ENTRY;
out->body.inval_entry.parent = parent;
out->body.inval_entry.namelen = namelen;
strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry),
name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry));
out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) +
namelen;
debug_response(*out);
write_response(*out);
return 0;
}
int MockFS::notify_inval_inode(ino_t ino, off_t off, ssize_t len)
{
std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
out->header.unique = 0; /* 0 means asynchronous notification */
out->header.error = FUSE_NOTIFY_INVAL_INODE;
out->body.inval_inode.ino = ino;
out->body.inval_inode.off = off;
out->body.inval_inode.len = len;
out->header.len = sizeof(out->header) + sizeof(out->body.inval_inode);
debug_response(*out);
write_response(*out);
return 0;
}
int MockFS::notify_store(ino_t ino, off_t off, const void* data, ssize_t size)
{
std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
out->header.unique = 0; /* 0 means asynchronous notification */
out->header.error = FUSE_NOTIFY_STORE;
out->body.store.nodeid = ino;
out->body.store.offset = off;
out->body.store.size = size;
bcopy(data, (char*)&out->body.bytes + sizeof(out->body.store), size);
out->header.len = sizeof(out->header) + sizeof(out->body.store) + size;
debug_response(*out);
write_response(*out);
return 0;
}
bool MockFS::pid_ok(pid_t pid) {
if (pid == m_pid) {
return (true);
} else if (pid == m_child_pid) {
return (true);
} else {
struct kinfo_proc *ki;
bool ok = false;
ki = kinfo_getproc(pid);
if (ki == NULL)
return (false);
/*
* Allow access by the aio daemon processes so that our tests
* can use aio functions
*/
if (0 == strncmp("aiod", ki->ki_comm, 4))
ok = true;
free(ki);
return (ok);
}
}
void MockFS::process_default(const mockfs_buf_in& in,
std::vector<std::unique_ptr<mockfs_buf_out>> &out)
{
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
out0->header.unique = in.header.unique;
out0->header.error = -EOPNOTSUPP;
out0->header.len = sizeof(out0->header);
out.push_back(std::move(out0));
}
void MockFS::read_request(mockfs_buf_in &in) {
ssize_t res;
int nready = 0;
fd_set readfds;
pollfd fds[1];
struct kevent changes[1];
struct kevent events[1];
struct timespec timeout_ts;
struct timeval timeout_tv;
const int timeout_ms = 999;
int timeout_int, nfds;
switch (m_pm) {
case BLOCKING:
break;
case KQ:
timeout_ts.tv_sec = 0;
timeout_ts.tv_nsec = timeout_ms * 1'000'000;
while (nready == 0) {
EV_SET(&changes[0], m_fuse_fd, EVFILT_READ, EV_ADD, 0,
0, 0);
nready = kevent(m_kq, &changes[0], 1, &events[0], 1,
&timeout_ts);
if (m_quit)
return;
}
ASSERT_LE(0, nready) << strerror(errno);
ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd);
if (events[0].flags & EV_ERROR)
FAIL() << strerror(events[0].data);
else if (events[0].flags & EV_EOF)
FAIL() << strerror(events[0].fflags);
m_nready = events[0].data;
break;
case POLL:
timeout_int = timeout_ms;
fds[0].fd = m_fuse_fd;
fds[0].events = POLLIN;
while (nready == 0) {
nready = poll(fds, 1, timeout_int);
if (m_quit)
return;
}
ASSERT_LE(0, nready) << strerror(errno);
ASSERT_TRUE(fds[0].revents & POLLIN);
break;
case SELECT:
timeout_tv.tv_sec = 0;
timeout_tv.tv_usec = timeout_ms * 1'000;
nfds = m_fuse_fd + 1;
while (nready == 0) {
FD_ZERO(&readfds);
FD_SET(m_fuse_fd, &readfds);
nready = select(nfds, &readfds, NULL, NULL,
&timeout_tv);
if (m_quit)
return;
}
ASSERT_LE(0, nready) << strerror(errno);
ASSERT_TRUE(FD_ISSET(m_fuse_fd, &readfds));
break;
default:
FAIL() << "not yet implemented";
}
res = read(m_fuse_fd, &in, sizeof(in));
if (res < 0 && !m_quit) {
FAIL() << "read: " << strerror(errno);
m_quit = true;
}
ASSERT_TRUE(res >= static_cast<ssize_t>(sizeof(in.header)) || m_quit);
}
void MockFS::write_response(const mockfs_buf_out &out) {
fd_set writefds;
pollfd fds[1];
int nready, nfds;
ssize_t r;
switch (m_pm) {
case BLOCKING:
case KQ: /* EVFILT_WRITE is not supported */
break;
case POLL:
fds[0].fd = m_fuse_fd;
fds[0].events = POLLOUT;
nready = poll(fds, 1, INFTIM);
ASSERT_LE(0, nready) << strerror(errno);
ASSERT_EQ(1, nready) << "NULL timeout expired?";
ASSERT_TRUE(fds[0].revents & POLLOUT);
break;
case SELECT:
FD_ZERO(&writefds);
FD_SET(m_fuse_fd, &writefds);
nfds = m_fuse_fd + 1;
nready = select(nfds, NULL, &writefds, NULL, NULL);
ASSERT_LE(0, nready) << strerror(errno);
ASSERT_EQ(1, nready) << "NULL timeout expired?";
ASSERT_TRUE(FD_ISSET(m_fuse_fd, &writefds));
break;
default:
FAIL() << "not yet implemented";
}
r = write(m_fuse_fd, &out, out.header.len);
ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno);
}
void* MockFS::service(void *pthr_data) {
MockFS *mock_fs = (MockFS*)pthr_data;
mock_fs->loop();
return (NULL);
}
void MockFS::unmount() {
::unmount("mountpoint", 0);
}

View File

@ -0,0 +1,394 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <pthread.h>
#include "fuse_kernel.h"
}
#include <gmock/gmock.h>
#define TIME_T_MAX (std::numeric_limits<time_t>::max())
/*
* A pseudo-fuse errno used indicate that a fuse operation should have no
* response, at least not immediately
*/
#define FUSE_NORESPONSE 9999
#define SET_OUT_HEADER_LEN(out, variant) { \
(out).header.len = (sizeof((out).header) + \
sizeof((out).body.variant)); \
}
/*
* Create an expectation on FUSE_LOOKUP and return it so the caller can set
* actions.
*
* This must be a macro instead of a method because EXPECT_CALL returns a type
* with a deleted constructor.
*/
#define EXPECT_LOOKUP(parent, path) \
EXPECT_CALL(*m_mock, process( \
ResultOf([=](auto in) { \
return (in.header.opcode == FUSE_LOOKUP && \
in.header.nodeid == (parent) && \
strcmp(in.body.lookup, (path)) == 0); \
}, Eq(true)), \
_) \
)
extern int verbosity;
/* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */
struct fuse_create_out {
struct fuse_entry_out entry;
struct fuse_open_out open;
};
/* Protocol 7.8 version of struct fuse_attr */
struct fuse_attr_7_8
{
uint64_t ino;
uint64_t size;
uint64_t blocks;
uint64_t atime;
uint64_t mtime;
uint64_t ctime;
uint32_t atimensec;
uint32_t mtimensec;
uint32_t ctimensec;
uint32_t mode;
uint32_t nlink;
uint32_t uid;
uint32_t gid;
uint32_t rdev;
};
/* Protocol 7.8 version of struct fuse_attr_out */
struct fuse_attr_out_7_8
{
uint64_t attr_valid;
uint32_t attr_valid_nsec;
uint32_t dummy;
struct fuse_attr_7_8 attr;
};
/* Protocol 7.8 version of struct fuse_entry_out */
struct fuse_entry_out_7_8 {
uint64_t nodeid; /* Inode ID */
uint64_t generation; /* Inode generation: nodeid:gen must
be unique for the fs's lifetime */
uint64_t entry_valid; /* Cache timeout for the name */
uint64_t attr_valid; /* Cache timeout for the attributes */
uint32_t entry_valid_nsec;
uint32_t attr_valid_nsec;
struct fuse_attr_7_8 attr;
};
/* Output struct for FUSE_CREATE for protocol 7.8 servers */
struct fuse_create_out_7_8 {
struct fuse_entry_out_7_8 entry;
struct fuse_open_out open;
};
/* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */
struct fuse_init_out_7_22 {
uint32_t major;
uint32_t minor;
uint32_t max_readahead;
uint32_t flags;
uint16_t max_background;
uint16_t congestion_threshold;
uint32_t max_write;
};
union fuse_payloads_in {
fuse_access_in access;
fuse_bmap_in bmap;
/* value is from fuse_kern_chan.c in fusefs-libs */
uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)];
fuse_create_in create;
fuse_flush_in flush;
fuse_fsync_in fsync;
fuse_fsync_in fsyncdir;
fuse_forget_in forget;
fuse_interrupt_in interrupt;
fuse_lk_in getlk;
fuse_getxattr_in getxattr;
fuse_init_in init;
fuse_link_in link;
fuse_listxattr_in listxattr;
char lookup[0];
fuse_mkdir_in mkdir;
fuse_mknod_in mknod;
fuse_open_in open;
fuse_open_in opendir;
fuse_read_in read;
fuse_read_in readdir;
fuse_release_in release;
fuse_release_in releasedir;
fuse_rename_in rename;
char rmdir[0];
fuse_setattr_in setattr;
fuse_setxattr_in setxattr;
fuse_lk_in setlk;
fuse_lk_in setlkw;
char unlink[0];
fuse_write_in write;
};
struct mockfs_buf_in {
fuse_in_header header;
union fuse_payloads_in body;
};
union fuse_payloads_out {
fuse_attr_out attr;
fuse_attr_out_7_8 attr_7_8;
fuse_bmap_out bmap;
fuse_create_out create;
fuse_create_out_7_8 create_7_8;
/*
* The protocol places no limits on the size of bytes. Choose
* a size big enough for anything we'll test.
*/
uint8_t bytes[0x20000];
fuse_entry_out entry;
fuse_entry_out_7_8 entry_7_8;
fuse_lk_out getlk;
fuse_getxattr_out getxattr;
fuse_init_out init;
fuse_init_out_7_22 init_7_22;
/* The inval_entry structure should be followed by the entry's name */
fuse_notify_inval_entry_out inval_entry;
fuse_notify_inval_inode_out inval_inode;
/* The store structure should be followed by the data to store */
fuse_notify_store_out store;
fuse_listxattr_out listxattr;
fuse_open_out open;
fuse_statfs_out statfs;
/*
* The protocol places no limits on the length of the string. This is
* merely convenient for testing.
*/
char str[80];
fuse_write_out write;
};
struct mockfs_buf_out {
fuse_out_header header;
union fuse_payloads_out body;
/* Default constructor: zero everything */
mockfs_buf_out() {
memset(this, 0, sizeof(*this));
}
};
/* A function that can be invoked in place of MockFS::process */
typedef std::function<void (const mockfs_buf_in& in,
std::vector<std::unique_ptr<mockfs_buf_out>> &out)>
ProcessMockerT;
/*
* Helper function used for setting an error expectation for any fuse operation.
* The operation will return the supplied error
*/
ProcessMockerT ReturnErrno(int error);
/* Helper function used for returning negative cache entries for LOOKUP */
ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid);
/* Helper function used for returning a single immediate response */
ProcessMockerT ReturnImmediate(
std::function<void(const mockfs_buf_in& in,
struct mockfs_buf_out &out)> f);
/* How the daemon should check /dev/fuse for readiness */
enum poll_method {
BLOCKING,
SELECT,
POLL,
KQ
};
/*
* Fake FUSE filesystem
*
* "Mounts" a filesystem to a temporary directory and services requests
* according to the programmed expectations.
*
* Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api.
*/
class MockFS {
/*
* thread id of the fuse daemon thread
*
* It must run in a separate thread so it doesn't deadlock with the
* client test code.
*/
pthread_t m_daemon_id;
/* file descriptor of /dev/fuse control device */
int m_fuse_fd;
/* The minor version of the kernel API that this mock daemon targets */
uint32_t m_kernel_minor_version;
int m_kq;
/* The max_readahead file system option */
uint32_t m_maxreadahead;
/* pid of the test process */
pid_t m_pid;
/* Method the daemon should use for I/O to and from /dev/fuse */
enum poll_method m_pm;
/* Timestamp granularity in nanoseconds */
unsigned m_time_gran;
void debug_request(const mockfs_buf_in&);
void debug_response(const mockfs_buf_out&);
/* Initialize a session after mounting */
void init(uint32_t flags);
/* Is pid from a process that might be involved in the test? */
bool pid_ok(pid_t pid);
/* Default request handler */
void process_default(const mockfs_buf_in&,
std::vector<std::unique_ptr<mockfs_buf_out>>&);
/* Entry point for the daemon thread */
static void* service(void*);
/* Read, but do not process, a single request from the kernel */
void read_request(mockfs_buf_in& in);
/* Write a single response back to the kernel */
void write_response(const mockfs_buf_out &out);
public:
/* pid of child process, for two-process test cases */
pid_t m_child_pid;
/* Maximum size of a FUSE_WRITE write */
uint32_t m_maxwrite;
/*
* Number of events that were available from /dev/fuse after the last
* kevent call. Only valid when m_pm = KQ.
*/
int m_nready;
/* Tell the daemon to shut down ASAP */
bool m_quit;
/* Create a new mockfs and mount it to a tempdir */
MockFS(int max_readahead, bool allow_other,
bool default_permissions, bool push_symlinks_in, bool ro,
enum poll_method pm, uint32_t flags,
uint32_t kernel_minor_version, uint32_t max_write, bool async,
bool no_clusterr, unsigned time_gran, bool nointr);
virtual ~MockFS();
/* Kill the filesystem daemon without unmounting the filesystem */
void kill_daemon();
/* Process FUSE requests endlessly */
void loop();
/*
* Send an asynchronous notification to invalidate a directory entry.
* Similar to libfuse's fuse_lowlevel_notify_inval_entry
*
* This method will block until the client has responded, so it should
* generally be run in a separate thread from request processing.
*
* @param parent Parent directory's inode number
* @param name name of dirent to invalidate
* @param namelen size of name, including the NUL
*/
int notify_inval_entry(ino_t parent, const char *name, size_t namelen);
/*
* Send an asynchronous notification to invalidate an inode's cached
* data and/or attributes. Similar to libfuse's
* fuse_lowlevel_notify_inval_inode.
*
* This method will block until the client has responded, so it should
* generally be run in a separate thread from request processing.
*
* @param ino File's inode number
* @param off offset at which to begin invalidation. A
* negative offset means to invalidate attributes
* only.
* @param len Size of region of data to invalidate. 0 means
* to invalidate all cached data.
*/
int notify_inval_inode(ino_t ino, off_t off, ssize_t len);
/*
* Send an asynchronous notification to store data directly into an
* inode's cache. Similar to libfuse's fuse_lowlevel_notify_store.
*
* This method will block until the client has responded, so it should
* generally be run in a separate thread from request processing.
*
* @param ino File's inode number
* @param off Offset at which to store data
* @param data Pointer to the data to cache
* @param len Size of data
*/
int notify_store(ino_t ino, off_t off, const void* data, ssize_t size);
/*
* Request handler
*
* This method is expected to provide the responses to each FUSE
* operation. For an immediate response, push one buffer into out.
* For a delayed response, push nothing. For an immediate response
* plus a delayed response to an earlier operation, push two bufs.
* Test cases must define each response using Googlemock expectations
*/
MOCK_METHOD2(process, void(const mockfs_buf_in&,
std::vector<std::unique_ptr<mockfs_buf_out>>&));
/* Gracefully unmount */
void unmount();
};

View File

@ -0,0 +1,152 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/uio.h>
#include "mntopts.h" // for build_iovec
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class UpdateOk: public FuseTest, public WithParamInterface<const char*> {};
class UpdateErr: public FuseTest, public WithParamInterface<const char*> {};
int mntflag_from_string(const char *s)
{
if (0 == strcmp("MNT_RDONLY", s))
return MNT_RDONLY;
else if (0 == strcmp("MNT_NOEXEC", s))
return MNT_NOEXEC;
else if (0 == strcmp("MNT_NOSUID", s))
return MNT_NOSUID;
else if (0 == strcmp("MNT_NOATIME", s))
return MNT_NOATIME;
else if (0 == strcmp("MNT_SUIDDIR", s))
return MNT_SUIDDIR;
else if (0 == strcmp("MNT_USER", s))
return MNT_USER;
else
return 0;
}
/* Some mount options can be changed by mount -u */
TEST_P(UpdateOk, update)
{
struct statfs statbuf;
struct iovec *iov = NULL;
int iovlen = 0;
int flag;
int newflags = MNT_UPDATE | MNT_SYNCHRONOUS;
flag = mntflag_from_string(GetParam());
if (flag == MNT_NOSUID && 0 != geteuid())
GTEST_SKIP() << "Only root may clear MNT_NOSUID";
if (flag == MNT_SUIDDIR && 0 != geteuid())
GTEST_SKIP() << "Only root may set MNT_SUIDDIR";
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
/*
* All of the fields except f_flags are don't care, and f_flags is set by
* the VFS
*/
SET_OUT_HEADER_LEN(out, statfs);
})));
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag;
build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
EXPECT_FALSE((newflags ^ statbuf.f_flags) & flag);
}
/* Some mount options cannnot be changed by mount -u */
TEST_P(UpdateErr, update)
{
struct statfs statbuf;
struct iovec *iov = NULL;
int iovlen = 0;
int flag;
int newflags = MNT_UPDATE | MNT_SYNCHRONOUS;
flag = mntflag_from_string(GetParam());
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
/*
* All of the fields except f_flags are don't care, and f_flags is set by
* the VFS
*/
SET_OUT_HEADER_LEN(out, statfs);
})));
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag;
build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
/*
* Don't check nmount's return value, because vfs_domount may "fix" the
* options for us. The important thing is to check the final value of
* statbuf.f_flags below.
*/
(void)nmount(iov, iovlen, newflags);
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
EXPECT_TRUE((newflags ^ statbuf.f_flags) & flag);
}
INSTANTIATE_TEST_CASE_P(Mount, UpdateOk,
::testing::Values("MNT_RDONLY", "MNT_NOEXEC", "MNT_NOSUID", "MNT_NOATIME",
"MNT_SUIDDIR")
);
INSTANTIATE_TEST_CASE_P(Mount, UpdateErr,
::testing::Values( "MNT_USER");
);

344
tests/sys/fs/fusefs/nfs.cc Normal file
View File

@ -0,0 +1,344 @@
/*-
* 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(FUSE_ROOT_ID, 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(FUSE_ROOT_ID, 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 */
TEST_F(Fhstat, 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;
EXPECT_LOOKUP(FUSE_ROOT_ID, 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.ino = ino;
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(ino, sb.st_ino);
}
/* File handle entries should expire from the cache, too */
TEST_F(Fhstat, cache_expired)
{
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;
EXPECT_LOOKUP(FUSE_ROOT_ID, 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.ino = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid_nsec = NAP_NS / 2;
})));
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.ino = ino;
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(ino, sb.st_ino);
nap();
/* Cache should be expired; fuse should issue a FUSE_LOOKUP */
ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
EXPECT_EQ(ino, sb.st_ino);
}
/*
* 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(FUSE_ROOT_ID, 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(FUSE_ROOT_ID, 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(FUSE_ROOT_ID, 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(FUSE_ROOT_ID, 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);
leak(fd);
}

View File

@ -0,0 +1,545 @@
/*-
* 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.
*/
extern "C" {
#include <sys/types.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <pthread.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/*
* FUSE asynchonous notification
*
* FUSE servers can send unprompted notification messages for things like cache
* invalidation. This file tests our client's handling of those messages.
*/
class Notify: public FuseTest {
public:
/* Ignore an optional FUSE_FSYNC */
void maybe_expect_fsync(uint64_t ino)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FSYNC &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
}
void expect_lookup(uint64_t parent, const char *relpath, uint64_t ino,
off_t size, Sequence &seq)
{
EXPECT_LOOKUP(parent, relpath)
.InSequence(seq)
.WillOnce(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.ino = ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr.size = size;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
}
};
class NotifyWriteback: public Notify {
public:
virtual void SetUp() {
m_init_flags |= FUSE_WRITEBACK_CACHE;
m_async = true;
Notify::SetUp();
if (IsSkipped())
return;
}
void expect_write(uint64_t ino, uint64_t offset, uint64_t size,
const void *contents)
{
FuseTest::expect_write(ino, offset, size, size, 0, 0, contents);
}
};
struct inval_entry_args {
MockFS *mock;
ino_t parent;
const char *name;
size_t namelen;
};
static void* inval_entry(void* arg) {
const struct inval_entry_args *iea = (struct inval_entry_args*)arg;
ssize_t r;
r = iea->mock->notify_inval_entry(iea->parent, iea->name, iea->namelen);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
struct inval_inode_args {
MockFS *mock;
ino_t ino;
off_t off;
ssize_t len;
};
struct store_args {
MockFS *mock;
ino_t nodeid;
off_t offset;
ssize_t size;
const void* data;
};
static void* inval_inode(void* arg) {
const struct inval_inode_args *iia = (struct inval_inode_args*)arg;
ssize_t r;
r = iia->mock->notify_inval_inode(iia->ino, iia->off, iia->len);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
static void* store(void* arg) {
const struct store_args *sa = (struct store_args*)arg;
ssize_t r;
r = sa->mock->notify_store(sa->nodeid, sa->offset, sa->data, sa->size);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
/* Invalidate a nonexistent entry */
TEST_F(Notify, inval_entry_nonexistent)
{
const static char *name = "foo";
struct inval_entry_args iea;
void *thr0_value;
pthread_t th0;
iea.mock = m_mock;
iea.parent = FUSE_ROOT_ID;
iea.name = name;
iea.namelen = strlen(name);
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
<< strerror(errno);
pthread_join(th0, &thr0_value);
/* It's not an error for an entry to not be cached */
EXPECT_EQ(0, (intptr_t)thr0_value);
}
/* Invalidate a cached entry */
TEST_F(Notify, inval_entry)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
struct inval_entry_args iea;
struct stat sb;
void *thr0_value;
uint64_t ino0 = 42;
uint64_t ino1 = 43;
Sequence seq;
pthread_t th0;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino0, 0, seq);
expect_lookup(FUSE_ROOT_ID, RELPATH, ino1, 0, seq);
/* Fill the entry cache */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(ino0, sb.st_ino);
/* Now invalidate the entry */
iea.mock = m_mock;
iea.parent = FUSE_ROOT_ID;
iea.name = RELPATH;
iea.namelen = strlen(RELPATH);
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* The second lookup should return the alternate ino */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(ino1, sb.st_ino);
}
/*
* Invalidate a cached entry beneath the root, which uses a slightly different
* code path.
*/
TEST_F(Notify, inval_entry_below_root)
{
const static char FULLPATH[] = "mountpoint/some_dir/foo";
const static char DNAME[] = "some_dir";
const static char FNAME[] = "foo";
struct inval_entry_args iea;
struct stat sb;
void *thr0_value;
uint64_t dir_ino = 41;
uint64_t ino0 = 42;
uint64_t ino1 = 43;
Sequence seq;
pthread_t th0;
EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
.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 = dir_ino;
out.body.entry.attr.nlink = 2;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
expect_lookup(dir_ino, FNAME, ino0, 0, seq);
expect_lookup(dir_ino, FNAME, ino1, 0, seq);
/* Fill the entry cache */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(ino0, sb.st_ino);
/* Now invalidate the entry */
iea.mock = m_mock;
iea.parent = dir_ino;
iea.name = FNAME;
iea.namelen = strlen(FNAME);
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* The second lookup should return the alternate ino */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(ino1, sb.st_ino);
}
/* Invalidating an entry invalidates the parent directory's attributes */
TEST_F(Notify, inval_entry_invalidates_parent_attrs)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
struct inval_entry_args iea;
struct stat sb;
void *thr0_value;
uint64_t ino = 42;
Sequence seq;
pthread_t th0;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
/* Fill the attr and entry cache */
ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
/* Now invalidate the entry */
iea.mock = m_mock;
iea.parent = FUSE_ROOT_ID;
iea.name = RELPATH;
iea.namelen = strlen(RELPATH);
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* /'s attribute cache should be cleared */
ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
}
TEST_F(Notify, inval_inode_nonexistent)
{
struct inval_inode_args iia;
ino_t ino = 42;
void *thr0_value;
pthread_t th0;
iia.mock = m_mock;
iia.ino = ino;
iia.off = 0;
iia.len = 0;
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
<< strerror(errno);
pthread_join(th0, &thr0_value);
/* It's not an error for an inode to not be cached */
EXPECT_EQ(0, (intptr_t)thr0_value);
}
TEST_F(Notify, inval_inode_with_clean_cache)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
const char CONTENTS0[] = "abcdefgh";
const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
struct inval_inode_args iia;
struct stat sb;
ino_t ino = 42;
void *thr0_value;
Sequence seq;
uid_t uid = 12345;
pthread_t th0;
ssize_t size0 = sizeof(CONTENTS0);
ssize_t size1 = sizeof(CONTENTS1);
char buf[80];
int fd;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size0, seq);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr_valid = UINT64_MAX;
out.body.attr.attr.size = size1;
out.body.attr.attr.uid = uid;
})));
expect_read(ino, 0, size0, size0, CONTENTS0);
expect_read(ino, 0, size1, size1, CONTENTS1);
/* Fill the data cache */
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(size0, read(fd, buf, size0)) << strerror(errno);
EXPECT_EQ(0, memcmp(buf, CONTENTS0, size0));
/* Evict the data cache */
iia.mock = m_mock;
iia.ino = ino;
iia.off = 0;
iia.len = 0;
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* cache attributes were been purged; this will trigger a new GETATTR */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(uid, sb.st_uid);
EXPECT_EQ(size1, sb.st_size);
/* This read should not be serviced by cache */
ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
leak(fd);
}
/* FUSE_NOTIFY_STORE with a file that's not in the entry cache */
/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
TEST_F(Notify, DISABLED_store_nonexistent)
{
struct store_args sa;
ino_t ino = 42;
void *thr0_value;
pthread_t th0;
sa.mock = m_mock;
sa.nodeid = ino;
sa.offset = 0;
sa.size = 0;
ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
pthread_join(th0, &thr0_value);
/* It's not an error for a file to be unknown to the kernel */
EXPECT_EQ(0, (intptr_t)thr0_value);
}
/* Store data into for a file that does not yet have anything cached */
/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
TEST_F(Notify, DISABLED_store_with_blank_cache)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
struct store_args sa;
ino_t ino = 42;
void *thr0_value;
Sequence seq;
pthread_t th0;
ssize_t size1 = sizeof(CONTENTS1);
char buf[80];
int fd;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size1, seq);
expect_open(ino, 0, 1);
/* Fill the data cache */
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
/* Evict the data cache */
sa.mock = m_mock;
sa.nodeid = ino;
sa.offset = 0;
sa.size = size1;
sa.data = (const void*)CONTENTS1;
ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* This read should be serviced by cache */
ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
leak(fd);
}
TEST_F(NotifyWriteback, inval_inode_with_dirty_cache)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
const char CONTENTS[] = "abcdefgh";
struct inval_inode_args iia;
ino_t ino = 42;
void *thr0_value;
Sequence seq;
pthread_t th0;
ssize_t bufsize = sizeof(CONTENTS);
int fd;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
expect_open(ino, 0, 1);
/* Fill the data cache */
fd = open(FULLPATH, O_RDWR);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
expect_write(ino, 0, bufsize, CONTENTS);
/*
* The FUSE protocol does not require an fsync here, but FreeBSD's
* bufobj_invalbuf sends it anyway
*/
maybe_expect_fsync(ino);
/* Evict the data cache */
iia.mock = m_mock;
iia.ino = ino;
iia.off = 0;
iia.len = 0;
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
leak(fd);
}
TEST_F(NotifyWriteback, inval_inode_attrs_only)
{
const static char FULLPATH[] = "mountpoint/foo";
const static char RELPATH[] = "foo";
const char CONTENTS[] = "abcdefgh";
struct inval_inode_args iia;
struct stat sb;
uid_t uid = 12345;
ino_t ino = 42;
void *thr0_value;
Sequence seq;
pthread_t th0;
ssize_t bufsize = sizeof(CONTENTS);
int fd;
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_WRITE);
}, Eq(true)),
_)
).Times(0);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr_valid = UINT64_MAX;
out.body.attr.attr.size = bufsize;
out.body.attr.attr.uid = uid;
})));
/* Fill the data cache */
fd = open(FULLPATH, O_RDWR);
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
/* Evict the attributes, but not data cache */
iia.mock = m_mock;
iia.ino = ino;
iia.off = -1;
iia.len = 0;
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
<< strerror(errno);
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
/* cache attributes were been purged; this will trigger a new GETATTR */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(uid, sb.st_uid);
EXPECT_EQ(bufsize, sb.st_size);
leak(fd);
}

262
tests/sys/fs/fusefs/open.cc Normal file
View File

@ -0,0 +1,262 @@
/*-
* 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.
*/
extern "C" {
#include <sys/wait.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Open: public FuseTest {
public:
/* Test an OK open of a file with the given flags */
void test_ok(int os_flags, int fuse_flags) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.body.open.flags == (uint32_t)fuse_flags &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
fd = open(FULLPATH, os_flags);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
};
/*
* fusefs(5) does not support I/O on device nodes (neither does UFS). But it
* shouldn't crash
*/
TEST_F(Open, chr)
{
const char FULLPATH[] = "mountpoint/zero";
const char RELPATH[] = "zero";
uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFCHR | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.rdev = 44; /* /dev/zero's rdev */
})));
ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
EXPECT_EQ(EOPNOTSUPP, errno);
}
/*
* The fuse daemon fails the request with enoent. This usually indicates a
* race condition: some other FUSE client removed the file in between when the
* kernel checked for it with lookup and tried to open it
*/
TEST_F(Open, enoent)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_NE(0, open(FULLPATH, O_RDONLY));
EXPECT_EQ(ENOENT, errno);
}
/*
* The daemon is responsible for checking file permissions (unless the
* default_permissions mount option was used)
*/
TEST_F(Open, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EPERM)));
EXPECT_NE(0, open(FULLPATH, O_RDONLY));
EXPECT_EQ(EPERM, errno);
}
/*
* fusefs must issue multiple FUSE_OPEN operations if clients with different
* credentials open the same file, even if they use the same mode. This is
* necessary so that the daemon can validate each set of credentials.
*/
TEST_F(Open, multiple_creds)
{
const static char FULLPATH[] = "mountpoint/some_file.txt";
const static char RELPATH[] = "some_file.txt";
int fd1, status;
const static uint64_t ino = 42;
const static uint64_t fh0 = 100, fh1 = 200;
/* Fork a child to open the file with different credentials */
fork(false, &status, [&] {
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.pid == (uint32_t)getpid() &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(
ReturnImmediate([](auto in __unused, auto& out) {
out.body.open.fh = fh0;
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.pid != (uint32_t)getpid() &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(
ReturnImmediate([](auto in __unused, auto& out) {
out.body.open.fh = fh1;
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
expect_flush(ino, 2, ReturnErrno(0));
expect_release(ino, fh0);
expect_release(ino, fh1);
fd1 = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd1) << strerror(errno);
}, [] {
int fd0;
fd0 = open(FULLPATH, O_RDONLY);
if (fd0 < 0) {
perror("open");
return(1);
}
return 0;
}
);
ASSERT_EQ(0, WEXITSTATUS(status));
close(fd1);
}
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open, DISABLED_o_append)
{
test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND);
}
/* The kernel is supposed to filter out this flag */
TEST_F(Open, o_creat)
{
test_ok(O_WRONLY | O_CREAT, O_WRONLY);
}
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open, DISABLED_o_direct)
{
test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT);
}
/* The kernel is supposed to filter out this flag */
TEST_F(Open, o_excl)
{
test_ok(O_WRONLY | O_EXCL, O_WRONLY);
}
TEST_F(Open, o_exec)
{
test_ok(O_EXEC, O_EXEC);
}
/* The kernel is supposed to filter out this flag */
TEST_F(Open, o_noctty)
{
test_ok(O_WRONLY | O_NOCTTY, O_WRONLY);
}
TEST_F(Open, o_rdonly)
{
test_ok(O_RDONLY, O_RDONLY);
}
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open, DISABLED_o_trunc)
{
test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC);
}
TEST_F(Open, o_wronly)
{
test_ok(O_WRONLY, O_WRONLY);
}
TEST_F(Open, o_rdwr)
{
test_ok(O_RDWR, O_RDWR);
}

View File

@ -0,0 +1,155 @@
/*-
* 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.
*/
extern "C" {
#include <dirent.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Opendir: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
}
void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r)
{
/* opendir(3) calls fstatfs */
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, statfs);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPENDIR &&
in.header.nodeid == ino &&
in.body.opendir.flags == flags);
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
/*
* The fuse daemon fails the request with enoent. This usually indicates a
* race condition: some other FUSE client removed the file in between when the
* kernel checked for it with lookup and tried to open it
*/
TEST_F(Opendir, enoent)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
expect_lookup(RELPATH, ino);
expect_opendir(ino, O_RDONLY, ReturnErrno(ENOENT));
EXPECT_NE(0, open(FULLPATH, O_DIRECTORY));
EXPECT_EQ(ENOENT, errno);
}
/*
* The daemon is responsible for checking file permissions (unless the
* default_permissions mount option was used)
*/
TEST_F(Opendir, eperm)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
expect_lookup(RELPATH, ino);
expect_opendir(ino, O_RDONLY, ReturnErrno(EPERM));
EXPECT_NE(0, open(FULLPATH, O_DIRECTORY));
EXPECT_EQ(EPERM, errno);
}
TEST_F(Opendir, open)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
expect_lookup(RELPATH, ino);
expect_opendir(ino, O_RDONLY,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, open);
}));
EXPECT_LE(0, open(FULLPATH, O_DIRECTORY)) << strerror(errno);
}
/* Directories can be opened O_EXEC for stuff like fchdir(2) */
TEST_F(Opendir, open_exec)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino, O_EXEC,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, open);
}));
fd = open(FULLPATH, O_EXEC | O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
}
TEST_F(Opendir, opendir)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
expect_lookup(RELPATH, ino);
expect_opendir(ino, O_RDONLY,
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, open);
}));
errno = 0;
EXPECT_NE(nullptr, opendir(FULLPATH)) << strerror(errno);
}

916
tests/sys/fs/fusefs/read.cc Normal file
View File

@ -0,0 +1,916 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/uio.h>
#include <aio.h>
#include <fcntl.h>
#include <semaphore.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Read: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1);
}
};
class Read_7_8: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
FuseTest::SetUp();
}
void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
{
FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1);
}
};
class AioRead: public Read {
public:
virtual void SetUp() {
const char *node = "vfs.aio.enable_unsafe";
int val = 0;
size_t size = sizeof(val);
FuseTest::SetUp();
ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
<< strerror(errno);
if (!val)
GTEST_SKIP() <<
"vfs.aio.enable_unsafe must be set for this test";
}
};
class AsyncRead: public AioRead {
virtual void SetUp() {
m_init_flags = FUSE_ASYNC_READ;
AioRead::SetUp();
}
};
class ReadAhead: public Read,
public WithParamInterface<tuple<bool, int>>
{
virtual void SetUp() {
int val;
const char *node = "vfs.maxbcachebuf";
size_t size = sizeof(val);
ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
<< strerror(errno);
m_maxreadahead = val * get<1>(GetParam());
m_noclusterr = get<0>(GetParam());
Read::SetUp();
}
};
/* AIO reads need to set the header's pid field correctly */
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
TEST_F(AioRead, aio_read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
struct aiocb iocb, *piocb;
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
iocb.aio_nbytes = bufsize;
iocb.aio_fildes = fd;
iocb.aio_buf = buf;
iocb.aio_offset = 0;
iocb.aio_sigevent.sigev_notify = SIGEV_NONE;
ASSERT_EQ(0, aio_read(&iocb)) << strerror(errno);
ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
/*
* Without the FUSE_ASYNC_READ mount option, fuse(4) should ensure that there
* is at most one outstanding read operation per file handle
*/
TEST_F(AioRead, async_read_disabled)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
ssize_t bufsize = 50;
char buf0[bufsize], buf1[bufsize];
off_t off0 = 0;
off_t off1 = m_maxbcachebuf;
struct aiocb iocb0, iocb1;
volatile sig_atomic_t read_count = 0;
expect_lookup(RELPATH, ino, 131072);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == FH &&
in.body.read.offset == (uint64_t)off0);
}, Eq(true)),
_)
).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) {
read_count++;
/* Filesystem is slow to respond */
}));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == FH &&
in.body.read.offset == (uint64_t)off1);
}, Eq(true)),
_)
).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) {
read_count++;
/* Filesystem is slow to respond */
}));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
/*
* Submit two AIO read requests, and respond to neither. If the
* filesystem ever gets the second read request, then we failed to
* limit outstanding reads.
*/
iocb0.aio_nbytes = bufsize;
iocb0.aio_fildes = fd;
iocb0.aio_buf = buf0;
iocb0.aio_offset = off0;
iocb0.aio_sigevent.sigev_notify = SIGEV_NONE;
ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno);
iocb1.aio_nbytes = bufsize;
iocb1.aio_fildes = fd;
iocb1.aio_buf = buf1;
iocb1.aio_offset = off1;
iocb1.aio_sigevent.sigev_notify = SIGEV_NONE;
ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno);
/*
* Sleep for awhile to make sure the kernel has had a chance to issue
* the second read, even though the first has not yet returned
*/
nap();
EXPECT_EQ(read_count, 1);
m_mock->kill_daemon();
/* Wait for AIO activity to complete, but ignore errors */
(void)aio_waitcomplete(NULL, NULL);
leak(fd);
}
/*
* With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple
* simultaneous read requests on the same file handle.
*/
TEST_F(AsyncRead, async_read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
ssize_t bufsize = 50;
char buf0[bufsize], buf1[bufsize];
off_t off0 = 0;
off_t off1 = m_maxbcachebuf;
off_t fsize = 2 * m_maxbcachebuf;
struct aiocb iocb0, iocb1;
sem_t sem;
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
expect_lookup(RELPATH, ino, fsize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == FH &&
in.body.read.offset == (uint64_t)off0);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
sem_post(&sem);
/* Filesystem is slow to respond */
}));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == FH &&
in.body.read.offset == (uint64_t)off1);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
sem_post(&sem);
/* Filesystem is slow to respond */
}));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
/*
* Submit two AIO read requests, but respond to neither. Ensure that
* we received both.
*/
iocb0.aio_nbytes = bufsize;
iocb0.aio_fildes = fd;
iocb0.aio_buf = buf0;
iocb0.aio_offset = off0;
iocb0.aio_sigevent.sigev_notify = SIGEV_NONE;
ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno);
iocb1.aio_nbytes = bufsize;
iocb1.aio_fildes = fd;
iocb1.aio_buf = buf1;
iocb1.aio_offset = off1;
iocb1.aio_sigevent.sigev_notify = SIGEV_NONE;
ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno);
/* Wait until both reads have reached the daemon */
ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
m_mock->kill_daemon();
/* Wait for AIO activity to complete, but ignore errors */
(void)aio_waitcomplete(NULL, NULL);
leak(fd);
}
/* 0-length reads shouldn't cause any confusion */
TEST_F(Read, direct_io_read_nothing)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
uint64_t offset = 100;
char buf[80];
expect_lookup(RELPATH, ino, offset + 1000);
expect_open(ino, FOPEN_DIRECT_IO, 1);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno);
leak(fd);
}
/*
* With direct_io, reads should not fill the cache. They should go straight to
* the daemon
*/
TEST_F(Read, direct_io_pread)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
uint64_t offset = 100;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, offset + bufsize);
expect_open(ino, FOPEN_DIRECT_IO, 1);
expect_read(ino, offset, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
// With FOPEN_DIRECT_IO, the cache should be bypassed. The server will
// get a 2nd read request.
expect_read(ino, offset, bufsize, bufsize, CONTENTS);
ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
/*
* With direct_io, filesystems are allowed to return less data than is
* requested. fuse(4) should return a short read to userland.
*/
TEST_F(Read, direct_io_short_read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefghijklmnop";
uint64_t ino = 42;
int fd;
uint64_t offset = 100;
ssize_t bufsize = strlen(CONTENTS);
ssize_t halfbufsize = bufsize / 2;
char buf[bufsize];
expect_lookup(RELPATH, ino, offset + bufsize);
expect_open(ino, FOPEN_DIRECT_IO, 1);
expect_read(ino, offset, bufsize, halfbufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(halfbufsize, pread(fd, buf, bufsize, offset))
<< strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, halfbufsize));
leak(fd);
}
TEST_F(Read, eio)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EIO)));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(-1, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(EIO, errno);
leak(fd);
}
/*
* If the server returns a short read when direct io is not in use, that
* indicates EOF, because of a server-side truncation. We should invalidate
* all cached attributes. We may update the file size,
*/
TEST_F(Read, eof)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefghijklmnop";
uint64_t ino = 42;
int fd;
uint64_t offset = 100;
ssize_t bufsize = strlen(CONTENTS);
ssize_t partbufsize = 3 * bufsize / 4;
ssize_t r;
char buf[bufsize];
struct stat sb;
expect_lookup(RELPATH, ino, offset + bufsize);
expect_open(ino, 0, 1);
expect_read(ino, 0, offset + bufsize, offset + partbufsize, CONTENTS);
expect_getattr(ino, offset + partbufsize);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
r = pread(fd, buf, bufsize, offset);
ASSERT_LE(0, r) << strerror(errno);
EXPECT_EQ(partbufsize, r) << strerror(errno);
ASSERT_EQ(0, fstat(fd, &sb));
EXPECT_EQ((off_t)(offset + partbufsize), sb.st_size);
leak(fd);
}
/* Like Read.eof, but causes an entire buffer to be invalidated */
TEST_F(Read, eof_of_whole_buffer)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefghijklmnop";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
off_t old_filesize = m_maxbcachebuf * 2 + bufsize;
char buf[bufsize];
struct stat sb;
expect_lookup(RELPATH, ino, old_filesize);
expect_open(ino, 0, 1);
expect_read(ino, 2 * m_maxbcachebuf, bufsize, bufsize, CONTENTS);
expect_read(ino, m_maxbcachebuf, m_maxbcachebuf, 0, CONTENTS);
expect_getattr(ino, m_maxbcachebuf);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
/* Cache the third block */
ASSERT_EQ(bufsize, pread(fd, buf, bufsize, m_maxbcachebuf * 2))
<< strerror(errno);
/* Try to read the 2nd block, but it's past EOF */
ASSERT_EQ(0, pread(fd, buf, bufsize, m_maxbcachebuf))
<< strerror(errno);
ASSERT_EQ(0, fstat(fd, &sb));
EXPECT_EQ((off_t)(m_maxbcachebuf), sb.st_size);
leak(fd);
}
/*
* With the keep_cache option, the kernel may keep its read cache across
* multiple open(2)s.
*/
TEST_F(Read, keep_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd0, fd1;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2);
expect_open(ino, FOPEN_KEEP_CACHE, 2);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd0 = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd0) << strerror(errno);
ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
fd1 = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd1) << strerror(errno);
/*
* This read should be serviced by cache, even though it's on the other
* file descriptor
*/
ASSERT_EQ(bufsize, read(fd1, buf, bufsize)) << strerror(errno);
leak(fd0);
leak(fd1);
}
/*
* Without the keep_cache option, the kernel should drop its read caches on
* every open
*/
TEST_F(Read, keep_cache_disabled)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd0, fd1;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2);
expect_open(ino, 0, 2);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd0 = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd0) << strerror(errno);
ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
fd1 = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd1) << strerror(errno);
/*
* This read should not be serviced by cache, even though it's on the
* original file descriptor
*/
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
ASSERT_EQ(0, lseek(fd0, 0, SEEK_SET)) << strerror(errno);
ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
leak(fd0);
leak(fd1);
}
TEST_F(Read, mmap)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t len;
size_t bufsize = strlen(CONTENTS);
void *p;
len = getpagesize();
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == Read::FH &&
in.body.read.offset == 0 &&
in.body.read.size == bufsize);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(struct fuse_out_header) + bufsize;
memmove(out.body.bytes, CONTENTS, bufsize);
})));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
ASSERT_NE(MAP_FAILED, p) << strerror(errno);
ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize));
ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
leak(fd);
}
/*
* A read via mmap comes up short, indicating that the file was truncated
* server-side.
*/
TEST_F(Read, mmap_eof)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t len;
size_t bufsize = strlen(CONTENTS);
struct stat sb;
void *p;
len = getpagesize();
expect_lookup(RELPATH, ino, m_maxbcachebuf);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == Read::FH &&
in.body.read.offset == 0 &&
in.body.read.size == (uint32_t)m_maxbcachebuf);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(struct fuse_out_header) + bufsize;
memmove(out.body.bytes, CONTENTS, bufsize);
})));
expect_getattr(ino, bufsize);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
ASSERT_NE(MAP_FAILED, p) << strerror(errno);
/* The file size should be automatically truncated */
ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize));
ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
EXPECT_EQ((off_t)bufsize, sb.st_size);
ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
leak(fd);
}
/*
* Just as when FOPEN_DIRECT_IO is used, reads with O_DIRECT should bypass
* cache and to straight to the daemon
*/
TEST_F(Read, o_direct)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
// Fill the cache
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
// Reads with o_direct should bypass the cache
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno);
ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
TEST_F(Read, pread)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
/*
* Set offset to a maxbcachebuf boundary so we'll be sure what offset
* to read from. Without this, the read might start at a lower offset.
*/
uint64_t offset = m_maxbcachebuf;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, offset + bufsize);
expect_open(ino, 0, 1);
expect_read(ino, offset, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
TEST_F(Read, read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
TEST_F(Read_7_8, read)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
char buf[bufsize];
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
expect_read(ino, 0, bufsize, bufsize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
leak(fd);
}
/*
* If cacheing is enabled, the kernel should try to read an entire cache block
* at a time.
*/
TEST_F(Read, cache_block)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS0 = "abcdefghijklmnop";
uint64_t ino = 42;
int fd;
ssize_t bufsize = 8;
ssize_t filesize = m_maxbcachebuf * 2;
char *contents;
char buf[bufsize];
const char *contents1 = CONTENTS0 + bufsize;
contents = (char*)calloc(1, filesize);
ASSERT_NE(nullptr, contents);
memmove(contents, CONTENTS0, strlen(CONTENTS0));
expect_lookup(RELPATH, ino, filesize);
expect_open(ino, 0, 1);
expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf,
contents);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize));
/* A subsequent read should be serviced by cache */
ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, contents1, bufsize));
leak(fd);
}
/* Reading with sendfile should work (though it obviously won't be 0-copy) */
TEST_F(Read, sendfile)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
size_t bufsize = strlen(CONTENTS);
char buf[bufsize];
int sp[2];
off_t sbytes;
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == Read::FH &&
in.body.read.offset == 0 &&
in.body.read.size == bufsize);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(struct fuse_out_header) + bufsize;
memmove(out.body.bytes, CONTENTS, bufsize);
})));
ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp))
<< strerror(errno);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0))
<< strerror(errno);
ASSERT_EQ(static_cast<ssize_t>(bufsize), read(sp[0], buf, bufsize))
<< strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
close(sp[1]);
close(sp[0]);
leak(fd);
}
/* sendfile should fail gracefully if fuse declines the read */
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */
TEST_F(Read, sendfile_eio)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefgh";
uint64_t ino = 42;
int fd;
ssize_t bufsize = strlen(CONTENTS);
int sp[2];
off_t sbytes;
expect_lookup(RELPATH, ino, bufsize);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EIO)));
ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp))
<< strerror(errno);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_NE(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0));
close(sp[1]);
close(sp[0]);
leak(fd);
}
/*
* Sequential reads should use readahead. And if allowed, large reads should
* be clustered.
*/
TEST_P(ReadAhead, readahead) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd, maxcontig, clustersize;
ssize_t bufsize = 4 * m_maxbcachebuf;
ssize_t filesize = bufsize;
uint64_t len;
char *rbuf, *contents;
off_t offs;
contents = (char*)malloc(filesize);
ASSERT_NE(nullptr, contents);
memset(contents, 'X', filesize);
rbuf = (char*)calloc(1, bufsize);
expect_lookup(RELPATH, ino, filesize);
expect_open(ino, 0, 1);
maxcontig = m_noclusterr ? m_maxbcachebuf :
m_maxbcachebuf + m_maxreadahead;
clustersize = MIN(maxcontig, m_maxphys);
for (offs = 0; offs < bufsize; offs += clustersize) {
len = std::min((size_t)clustersize, (size_t)(filesize - offs));
expect_read(ino, offs, len, len, contents + offs);
}
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
/* Set the internal readahead counter to a "large" value */
ASSERT_EQ(0, fcntl(fd, F_READAHEAD, 1'000'000'000)) << strerror(errno);
ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(rbuf, contents, bufsize));
leak(fd);
}
INSTANTIATE_TEST_CASE_P(RA, ReadAhead,
Values(tuple<bool, int>(false, 0),
tuple<bool, int>(false, 1),
tuple<bool, int>(false, 2),
tuple<bool, int>(false, 3),
tuple<bool, int>(true, 0),
tuple<bool, int>(true, 1),
tuple<bool, int>(true, 2)));

View File

@ -0,0 +1,375 @@
/*-
* 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.
*/
extern "C" {
#include <dirent.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
using namespace std;
class Readdir: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
}
};
class Readdir_7_8: public Readdir {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
Readdir::SetUp();
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup_7_8(relpath, ino, S_IFDIR | 0755, 0, 1);
}
};
/* FUSE_READDIR returns nothing but "." and ".." */
TEST_F(Readdir, dots)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
struct dirent *de;
vector<struct dirent> ents(2);
vector<struct dirent> empty_ents(0);
const char dot[] = ".";
const char dotdot[] = "..";
expect_lookup(RELPATH, ino);
expect_opendir(ino);
ents[0].d_fileno = 2;
ents[0].d_off = 2000;
ents[0].d_namlen = sizeof(dotdot);
ents[0].d_type = DT_DIR;
strncpy(ents[0].d_name, dotdot, ents[0].d_namlen);
ents[1].d_fileno = 3;
ents[1].d_off = 3000;
ents[1].d_namlen = sizeof(dot);
ents[1].d_type = DT_DIR;
strncpy(ents[1].d_name, dot, ents[1].d_namlen);
expect_readdir(ino, 0, ents);
expect_readdir(ino, 3000, empty_ents);
errno = 0;
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
errno = 0;
de = readdir(dir);
ASSERT_NE(nullptr, de) << strerror(errno);
EXPECT_EQ(2ul, de->d_fileno);
/*
* fuse(4) doesn't actually set d_off, which is ok for now because
* nothing uses it.
*/
//EXPECT_EQ(2000, de->d_off);
EXPECT_EQ(DT_DIR, de->d_type);
EXPECT_EQ(sizeof(dotdot), de->d_namlen);
EXPECT_EQ(0, strcmp(dotdot, de->d_name));
errno = 0;
de = readdir(dir);
ASSERT_NE(nullptr, de) << strerror(errno);
EXPECT_EQ(3ul, de->d_fileno);
//EXPECT_EQ(3000, de->d_off);
EXPECT_EQ(DT_DIR, de->d_type);
EXPECT_EQ(sizeof(dot), de->d_namlen);
EXPECT_EQ(0, strcmp(dot, de->d_name));
ASSERT_EQ(nullptr, readdir(dir));
ASSERT_EQ(0, errno);
leakdir(dir);
}
TEST_F(Readdir, eio)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
struct dirent *de;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READDIR &&
in.header.nodeid == ino &&
in.body.readdir.offset == 0);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EIO)));
errno = 0;
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
errno = 0;
de = readdir(dir);
ASSERT_EQ(nullptr, de);
ASSERT_EQ(EIO, errno);
leakdir(dir);
}
/* getdirentries(2) can use a larger buffer size than readdir(3) */
TEST_F(Readdir, getdirentries)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
char buf[8192];
ssize_t r;
expect_lookup(RELPATH, ino);
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 == 8192);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.error = 0;
out.header.len = sizeof(out.header);
})));
fd = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd) << strerror(errno);
r = getdirentries(fd, buf, sizeof(buf), 0);
ASSERT_EQ(0, r) << strerror(errno);
leak(fd);
}
/*
* Nothing bad should happen if getdirentries is called on two file descriptors
* which were concurrently open, but one has already been closed.
* This is a regression test for a specific bug dating from r238402.
*/
TEST_F(Readdir, getdirentries_concurrent)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd0, fd1;
char buf[8192];
ssize_t r;
FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2);
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 == 8192);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.error = 0;
out.header.len = sizeof(out.header);
})));
fd0 = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd0) << strerror(errno);
fd1 = open(FULLPATH, O_DIRECTORY);
ASSERT_LE(0, fd1) << strerror(errno);
r = getdirentries(fd0, buf, sizeof(buf), 0);
ASSERT_EQ(0, r) << strerror(errno);
EXPECT_EQ(0, close(fd0)) << strerror(errno);
r = getdirentries(fd1, buf, sizeof(buf), 0);
ASSERT_EQ(0, r) << strerror(errno);
leak(fd0);
leak(fd1);
}
/*
* FUSE_READDIR returns nothing, not even "." and "..". This is legal, though
* the filesystem obviously won't be fully functional.
*/
TEST_F(Readdir, nodots)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READDIR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.error = 0;
out.header.len = sizeof(out.header);
})));
errno = 0;
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
errno = 0;
ASSERT_EQ(nullptr, readdir(dir));
ASSERT_EQ(0, errno);
leakdir(dir);
}
/* telldir(3) and seekdir(3) should work with fuse */
TEST_F(Readdir, seekdir)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
struct dirent *de;
/*
* use enough entries to be > 4096 bytes, so getdirentries must be
* called
* multiple times.
*/
vector<struct dirent> ents0(122), ents1(102), ents2(30);
long bookmark;
int i = 0;
for (auto& it: ents0) {
snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
it.d_fileno = 2 + i;
it.d_off = (2 + i) * 1000;
it.d_namlen = strlen(it.d_name);
it.d_type = DT_REG;
i++;
}
for (auto& it: ents1) {
snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
it.d_fileno = 2 + i;
it.d_off = (2 + i) * 1000;
it.d_namlen = strlen(it.d_name);
it.d_type = DT_REG;
i++;
}
for (auto& it: ents2) {
snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
it.d_fileno = 2 + i;
it.d_off = (2 + i) * 1000;
it.d_namlen = strlen(it.d_name);
it.d_type = DT_REG;
i++;
}
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_readdir(ino, 0, ents0);
expect_readdir(ino, 123000, ents1);
expect_readdir(ino, 225000, ents2);
errno = 0;
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
for (i=0; i < 128; i++) {
errno = 0;
de = readdir(dir);
ASSERT_NE(nullptr, de) << strerror(errno);
EXPECT_EQ(2 + (ino_t)i, de->d_fileno);
}
bookmark = telldir(dir);
for (; i < 232; i++) {
errno = 0;
de = readdir(dir);
ASSERT_NE(nullptr, de) << strerror(errno);
EXPECT_EQ(2 + (ino_t)i, de->d_fileno);
}
seekdir(dir, bookmark);
de = readdir(dir);
ASSERT_NE(nullptr, de) << strerror(errno);
EXPECT_EQ(130ul, de->d_fileno);
leakdir(dir);
}
TEST_F(Readdir_7_8, nodots)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READDIR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.error = 0;
out.header.len = sizeof(out.header);
})));
errno = 0;
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
errno = 0;
ASSERT_EQ(nullptr, readdir(dir));
ASSERT_EQ(0, errno);
leakdir(dir);
}

View File

@ -0,0 +1,123 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Readlink: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFLNK | 0777, 0, 1);
}
void expect_readlink(uint64_t ino, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READLINK &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
class PushSymlinksIn: public Readlink {
virtual void SetUp() {
m_push_symlinks_in = true;
Readlink::SetUp();
}
};
TEST_F(Readlink, eloop)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const uint64_t ino = 42;
char buf[80];
expect_lookup(RELPATH, ino);
expect_readlink(ino, ReturnErrno(ELOOP));
EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf)));
EXPECT_EQ(ELOOP, errno);
}
TEST_F(Readlink, ok)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "dst";
const uint64_t ino = 42;
char buf[80];
expect_lookup(RELPATH, ino);
expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) {
strlcpy(out.body.str, dst, sizeof(out.body.str));
out.header.len = sizeof(out.header) + strlen(dst) + 1;
}));
EXPECT_EQ(static_cast<ssize_t>(strlen(dst)) + 1,
readlink(FULLPATH, buf, sizeof(buf)));
EXPECT_STREQ(dst, buf);
}
TEST_F(PushSymlinksIn, readlink)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "/dst";
const uint64_t ino = 42;
char buf[MAXPATHLEN], wd[MAXPATHLEN], want[MAXPATHLEN];
int len;
expect_lookup(RELPATH, ino);
expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) {
strlcpy(out.body.str, dst, sizeof(out.body.str));
out.header.len = sizeof(out.header) + strlen(dst) + 1;
}));
ASSERT_NE(nullptr, getcwd(wd, sizeof(wd))) << strerror(errno);
len = snprintf(want, sizeof(want), "%s/mountpoint%s", wd, dst);
ASSERT_LE(0, len) << strerror(errno);
EXPECT_EQ(static_cast<ssize_t>(len) + 1,
readlink(FULLPATH, buf, sizeof(buf)));
EXPECT_STREQ(want, buf);
}

View File

@ -0,0 +1,224 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Release: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino, int times)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times);
}
void expect_release(uint64_t ino, uint64_t lock_owner,
uint32_t flags, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASE &&
in.header.nodeid == ino &&
in.body.release.lock_owner == lock_owner &&
in.body.release.fh == FH &&
in.body.release.flags == flags);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)))
.RetiresOnSaturation();
}
};
class ReleaseWithLocks: public Release {
virtual void SetUp() {
m_init_flags = FUSE_POSIX_LOCKS;
Release::SetUp();
}
};
/* If a file descriptor is duplicated, only the last close causes RELEASE */
TEST_F(Release, dup)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd, fd2;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, getpid(), O_RDONLY, 0);
fd = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd) << strerror(errno);
fd2 = dup(fd);
ASSERT_EQ(0, close(fd2)) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}
/*
* Some FUSE filesystem cache data internally and flush it on release. Such
* filesystems may generate errors during release. On Linux, these get
* returned by close(2). However, POSIX does not require close(2) to return
* this error. FreeBSD's fuse(4) should return EIO if it returns an error at
* all.
*/
/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */
TEST_F(Release, eio)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, getpid(), O_WRONLY, EIO);
fd = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno);
}
/*
* FUSE_RELEASE should contain the same flags used for FUSE_OPEN
*/
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Release, DISABLED_flags)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, getpid(), O_RDWR | O_APPEND, 0);
fd = open(FULLPATH, O_RDWR | O_APPEND);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}
/*
* fuse(4) will issue multiple FUSE_OPEN operations for the same file if it's
* opened with different modes. Each FUSE_OPEN should get its own
* FUSE_RELEASE.
*/
TEST_F(Release, multiple_opens)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd, fd2;
expect_lookup(RELPATH, ino, 2);
expect_open(ino, 0, 2);
expect_flush(ino, 2, ReturnErrno(0));
expect_release(ino, getpid(), O_RDONLY, 0);
fd = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd) << strerror(errno);
expect_release(ino, getpid(), O_WRONLY, 0);
fd2 = open(FULLPATH, O_WRONLY);
EXPECT_LE(0, fd2) << strerror(errno);
ASSERT_EQ(0, close(fd2)) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}
TEST_F(Release, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, getpid(), O_RDONLY, 0);
fd = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}
/* When closing a file with a POSIX file lock, release should release the lock*/
TEST_F(ReleaseWithLocks, unlock_on_close)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
struct flock fl;
pid_t pid = getpid();
expect_lookup(RELPATH, ino, 1);
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETLK &&
in.header.nodeid == ino &&
in.body.setlk.fh == FH);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
expect_flush(ino, 1, ReturnErrno(0));
expect_release(ino, static_cast<uint64_t>(pid), O_RDWR, 0);
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
fl.l_start = 0;
fl.l_len = 0;
fl.l_pid = pid;
fl.l_type = F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_sysid = 0;
ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}

View File

@ -0,0 +1,116 @@
/*-
* 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.
*/
extern "C" {
#include <dirent.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class ReleaseDir: public FuseTest {
public:
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
}
};
/* If a file descriptor is duplicated, only the last close causes RELEASE */
TEST_F(ReleaseDir, dup)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
DIR *dir, *dir2;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READDIR &&
in.header.nodeid == ino &&
in.body.readdir.offset == 0);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.error = 0;
out.header.len = sizeof(out.header);
})));
expect_releasedir(ino, ReturnErrno(0));
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
dir2 = fdopendir(dup(dirfd(dir)));
ASSERT_NE(nullptr, dir2) << strerror(errno);
ASSERT_EQ(0, closedir(dir)) << strerror(errno);
ASSERT_EQ(0, closedir(dir2)) << strerror(errno);
}
TEST_F(ReleaseDir, ok)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
DIR *dir;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_releasedir(ino, ReturnErrno(0));
dir = opendir(FULLPATH);
ASSERT_NE(nullptr, dir) << strerror(errno);
ASSERT_EQ(0, closedir(dir)) << strerror(errno);
}
/* Directories opened O_EXEC should be properly released, too */
TEST_F(ReleaseDir, o_exec)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
int fd;
expect_lookup(RELPATH, ino);
expect_opendir(ino);
expect_releasedir(ino, ReturnErrno(0));
fd = open(FULLPATH, O_EXEC | O_DIRECTORY);
EXPECT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, close(fd)) << strerror(errno);
}

View File

@ -0,0 +1,321 @@
/*-
* 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.
*/
extern "C" {
#include <stdlib.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Rename: public FuseTest {
public:
int tmpfd = -1;
char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
virtual void TearDown() {
if (tmpfd >= 0) {
close(tmpfd);
unlink(tmpfile);
}
FuseTest::TearDown();
}
void expect_getattr(uint64_t ino, mode_t mode)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(
ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = mode;
out.body.attr.attr_valid = UINT64_MAX;
})));
}
};
// EINVAL, dst is subdir of src
TEST_F(Rename, einval)
{
const char FULLDST[] = "mountpoint/src/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t src_ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
ASSERT_NE(0, rename(FULLSRC, FULLDST));
ASSERT_EQ(EINVAL, errno);
}
// source does not exist
TEST_F(Rename, enoent)
{
const char FULLDST[] = "mountpoint/dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
// FUSE hardcodes the mountpoint to inode 1
EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
ASSERT_NE(0, rename(FULLSRC, FULLDST));
ASSERT_EQ(ENOENT, errno);
}
/*
* Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst
*/
TEST_F(Rename, entry_cache_negative)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t dst_dir_ino = FUSE_ROOT_ID;
uint64_t ino = 42;
/*
* Set entry_valid = 0 because this test isn't concerned with whether
* or not we actually cache negative entries, only with whether we
* interpret negative cache responses correctly.
*/
struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
/* LOOKUP returns a negative cache entry for dst */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(ReturnNegativeCache(&entry_valid));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *src = (const char*)in.body.bytes +
sizeof(fuse_rename_in);
const char *dst = src + strlen(src) + 1;
return (in.header.opcode == FUSE_RENAME &&
in.body.rename.newdir == dst_dir_ino &&
(0 == strcmp(RELDST, dst)) &&
(0 == strcmp(RELSRC, src)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}
/*
* Renaming a file should purge any negative namecache entries for the dst
*/
TEST_F(Rename, entry_cache_negative_purge)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t dst_dir_ino = FUSE_ROOT_ID;
uint64_t ino = 42;
struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
/* LOOKUP returns a negative cache entry for dst */
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(ReturnNegativeCache(&entry_valid))
.RetiresOnSaturation();
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *src = (const char*)in.body.bytes +
sizeof(fuse_rename_in);
const char *dst = src + strlen(src) + 1;
return (in.header.opcode == FUSE_RENAME &&
in.body.rename.newdir == dst_dir_ino &&
(0 == strcmp(RELDST, dst)) &&
(0 == strcmp(RELSRC, src)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
/* Finally, a subsequent lookup should query the daemon */
expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1);
ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
}
TEST_F(Rename, exdev)
{
const char FULLB[] = "mountpoint/src";
const char RELB[] = "src";
// FUSE hardcodes the mountpoint to inode 1
uint64_t b_ino = 42;
tmpfd = mkstemp(tmpfile);
ASSERT_LE(0, tmpfd) << strerror(errno);
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
ASSERT_NE(0, rename(tmpfile, FULLB));
ASSERT_EQ(EXDEV, errno);
ASSERT_NE(0, rename(FULLB, tmpfile));
ASSERT_EQ(EXDEV, errno);
}
TEST_F(Rename, ok)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t dst_dir_ino = FUSE_ROOT_ID;
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *src = (const char*)in.body.bytes +
sizeof(fuse_rename_in);
const char *dst = src + strlen(src) + 1;
return (in.header.opcode == FUSE_RENAME &&
in.body.rename.newdir == dst_dir_ino &&
(0 == strcmp(RELDST, dst)) &&
(0 == strcmp(RELSRC, src)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}
/* When moving a file to a new directory, update its parent */
TEST_F(Rename, parent)
{
const char FULLDST[] = "mountpoint/dstdir/dst";
const char RELDSTDIR[] = "dstdir";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
const char FULLDSTPARENT[] = "mountpoint/dstdir/dst/..";
Sequence seq;
uint64_t dst_dir_ino = 43;
uint64_t ino = 42;
struct stat sb;
expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1);
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = dst_dir_ino;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.attr.ino = dst_dir_ino;
})));
EXPECT_LOOKUP(dst_dir_ino, RELDST)
.InSequence(seq)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *src = (const char*)in.body.bytes +
sizeof(fuse_rename_in);
const char *dst = src + strlen(src) + 1;
return (in.header.opcode == FUSE_RENAME &&
in.body.rename.newdir == dst_dir_ino &&
(0 == strcmp(RELDST, dst)) &&
(0 == strcmp(RELSRC, src)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
EXPECT_LOOKUP(dst_dir_ino, RELDST)
.InSequence(seq)
.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.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno);
ASSERT_EQ(dst_dir_ino, sb.st_ino);
}
// Rename overwrites an existing destination file
TEST_F(Rename, overwrite)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
// The inode of the already-existing destination file
uint64_t dst_ino = 2;
uint64_t dst_dir_ino = FUSE_ROOT_ID;
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *src = (const char*)in.body.bytes +
sizeof(fuse_rename_in);
const char *dst = src + strlen(src) + 1;
return (in.header.opcode == FUSE_RENAME &&
in.body.rename.newdir == dst_dir_ino &&
(0 == strcmp(RELDST, dst)) &&
(0 == strcmp(RELSRC, src)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}

View File

@ -0,0 +1,172 @@
/*-
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <semaphore.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Rmdir: public FuseTest {
public:
void expect_getattr(uint64_t ino, mode_t mode)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = mode;
out.body.attr.attr_valid = UINT64_MAX;
})));
}
void expect_lookup(const char *relpath, uint64_t ino, int times=1)
{
EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
.Times(times)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 2;
})));
}
void expect_rmdir(uint64_t parent, const char *relpath, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RMDIR &&
0 == strcmp(relpath, in.body.rmdir) &&
in.header.nodeid == parent);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
};
/*
* A successful rmdir should clear the parent directory's attribute cache,
* because the fuse daemon should update its mtime and ctime
*/
TEST_F(Rmdir, parent_attr_cache)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
struct stat sb;
sem_t sem;
uint64_t ino = 42;
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
expect_lookup(RELPATH, ino);
expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
expect_forget(ino, 1, &sem);
ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
sem_wait(&sem);
sem_destroy(&sem);
}
TEST_F(Rmdir, enotempty)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELPATH, ino);
expect_rmdir(FUSE_ROOT_ID, RELPATH, ENOTEMPTY);
ASSERT_NE(0, rmdir(FULLPATH));
ASSERT_EQ(ENOTEMPTY, errno);
}
/* Removing a directory should expire its entry cache */
TEST_F(Rmdir, entry_cache)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
sem_t sem;
uint64_t ino = 42;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH, ino, 2);
expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
expect_forget(ino, 1, &sem);
ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
sem_wait(&sem);
sem_destroy(&sem);
}
TEST_F(Rmdir, ok)
{
const char FULLPATH[] = "mountpoint/some_dir";
const char RELPATH[] = "some_dir";
sem_t sem;
uint64_t ino = 42;
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
expect_lookup(RELPATH, ino);
expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
expect_forget(ino, 1, &sem);
ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
sem_wait(&sem);
sem_destroy(&sem);
}

View File

@ -0,0 +1,774 @@
/*-
* 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.
*/
extern "C" {
#include <sys/stat.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Setattr : public FuseTest {};
class RofsSetattr: public Setattr {
public:
virtual void SetUp() {
m_ro = true;
Setattr::SetUp();
}
};
class Setattr_7_8: public Setattr {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
Setattr::SetUp();
}
};
/*
* If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
* should use the cached attributes, rather than query the daemon
*/
TEST_F(Setattr, attr_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
struct stat sb;
const mode_t newmode = 0644;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.entry_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
out.body.attr.attr_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_GETATTR);
}, Eq(true)),
_)
).Times(0);
/* Set an attribute with SETATTR */
ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
/* The stat(2) should use cached attributes */
ASSERT_EQ(0, stat(FULLPATH, &sb));
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
}
/* Change the mode of a file */
TEST_F(Setattr, chmod)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
uint32_t valid = FATTR_MODE;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
})));
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}
/*
* Chmod a multiply-linked file with cached attributes. Check that both files'
* attributes have changed.
*/
TEST_F(Setattr, chmod_multiply_linked)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char RELPATH0[] = "some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
struct stat sb;
const uint64_t ino = 42;
const mode_t oldmode = 0777;
const mode_t newmode = 0666;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 2;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 2;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
uint32_t valid = FATTR_MODE;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = S_IFREG | newmode;
out.body.attr.attr.nlink = 2;
out.body.attr.attr_valid = UINT64_MAX;
})));
/* For a lookup of the 2nd file to get it into the cache*/
ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno);
ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno);
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
}
/* Change the owner and group of a file */
TEST_F(Setattr, chown)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const gid_t oldgroup = 66;
const gid_t newgroup = 99;
const uid_t olduser = 33;
const uid_t newuser = 44;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.gid = oldgroup;
out.body.entry.attr.uid = olduser;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
uint32_t valid = FATTR_GID | FATTR_UID;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.uid == newuser &&
in.body.setattr.gid == newgroup);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.uid = newuser;
out.body.attr.attr.gid = newgroup;
})));
EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno);
}
/*
* FUSE daemons are allowed to check permissions however they like. If the
* daemon returns EPERM, even if the file permissions "should" grant access,
* then fuse(4) should return EPERM too.
*/
TEST_F(Setattr, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0777;
out.body.entry.nodeid = ino;
out.body.entry.attr.uid = in.header.uid;
out.body.entry.attr.gid = in.header.gid;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EPERM)));
EXPECT_NE(0, truncate(FULLPATH, 10));
EXPECT_EQ(EPERM, errno);
}
/* Change the mode of an open file, by its file descriptor */
TEST_F(Setattr, fchmod)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
uint32_t valid = FATTR_MODE;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
})));
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
leak(fd);
}
/* Change the size of an open file, by its file descriptor */
TEST_F(Setattr, ftruncate)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
uint64_t fh = 0xdeadbeef1a7ebabe;
const off_t oldsize = 99;
const off_t newsize = 12345;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0755;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.size = oldsize;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
out.body.open.fh = fh;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
uint32_t valid = FATTR_SIZE | FATTR_FH;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.fh == fh);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0755;
out.body.attr.attr.size = newsize;
})));
fd = open(FULLPATH, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno);
leak(fd);
}
/* Change the size of the file */
TEST_F(Setattr, truncate) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const uint64_t oldsize = 100'000'000;
const uint64_t newsize = 20'000'000;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
uint32_t valid = FATTR_SIZE;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.size == newsize);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.size = newsize;
})));
EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno);
}
/*
* Truncating a file should discard cached data past the truncation point.
* This is a regression test for bug 233783.
*
* There are two distinct failure modes. The first one is a failure to zero
* the portion of the file's final buffer past EOF. It can be reproduced by
* fsx -WR -P /tmp -S10 fsx.bin
*
* The second is a failure to drop buffers beyond that. It can be reproduced by
* fsx -WR -P /tmp -S18 -n fsx.bin
* Also reproducible in sh with:
* $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt
* $> cd /tmp/mnt/tmp
* $> dd if=/dev/random of=randfile bs=1k count=192
* $> truncate -s 1k randfile && truncate -s 192k randfile
* $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000
*/
TEST_F(Setattr, truncate_discards_cached_data) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
void *w0buf, *r0buf, *r1buf, *expected;
off_t w0_offset = 0;
size_t w0_size = 0x30000;
off_t r0_offset = 0;
off_t r0_size = w0_size;
size_t trunc0_size = 0x400;
size_t trunc1_size = w0_size;
off_t r1_offset = trunc0_size;
off_t r1_size = w0_size - trunc0_size;
size_t cur_size = 0;
const uint64_t ino = 42;
mode_t mode = S_IFREG | 0644;
int fd, r;
bool should_have_data = false;
w0buf = malloc(w0_size);
ASSERT_NE(nullptr, w0buf) << strerror(errno);
memset(w0buf, 'X', w0_size);
r0buf = malloc(r0_size);
ASSERT_NE(nullptr, r0buf) << strerror(errno);
r1buf = malloc(r1_size);
ASSERT_NE(nullptr, r1buf) << strerror(errno);
expected = malloc(r1_size);
ASSERT_NE(nullptr, expected) << strerror(errno);
memset(expected, 0, r1_size);
expect_lookup(RELPATH, ino, mode, 0, 1);
expect_open(ino, O_RDWR, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = mode;
out.body.attr.attr.size = cur_size;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_WRITE);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
SET_OUT_HEADER_LEN(out, write);
out.body.attr.attr.ino = ino;
out.body.write.size = in.body.write.size;
cur_size = std::max(static_cast<uint64_t>(cur_size),
in.body.write.size + in.body.write.offset);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
(in.body.setattr.valid & FATTR_SIZE));
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
auto trunc_size = in.body.setattr.size;
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = mode;
out.body.attr.attr.size = trunc_size;
cur_size = trunc_size;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
auto osize = std::min(
static_cast<uint64_t>(cur_size) - in.body.read.offset,
static_cast<uint64_t>(in.body.read.size));
out.header.len = sizeof(struct fuse_out_header) + osize;
if (should_have_data)
memset(out.body.bytes, 'X', osize);
else
bzero(out.body.bytes, osize);
})));
fd = open(FULLPATH, O_RDWR, 0644);
ASSERT_LE(0, fd) << strerror(errno);
/* Fill the file with Xs */
ASSERT_EQ(static_cast<ssize_t>(w0_size),
pwrite(fd, w0buf, w0_size, w0_offset));
should_have_data = true;
/* Fill the cache */
ASSERT_EQ(static_cast<ssize_t>(r0_size),
pread(fd, r0buf, r0_size, r0_offset));
/* 1st truncate should discard cached data */
EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno);
should_have_data = false;
/* 2nd truncate extends file into previously cached data */
EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno);
/* Read should return all zeros */
ASSERT_EQ(static_cast<ssize_t>(r1_size),
pread(fd, r1buf, r1_size, r1_offset));
r = memcmp(expected, r1buf, r1_size);
ASSERT_EQ(0, r);
free(expected);
free(r1buf);
free(r0buf);
free(w0buf);
}
/* Change a file's timestamps */
TEST_F(Setattr, utimensat) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const timespec oldtimes[2] = {
{.tv_sec = 1, .tv_nsec = 2},
{.tv_sec = 3, .tv_nsec = 4},
};
const timespec newtimes[2] = {
{.tv_sec = 5, .tv_nsec = 6},
{.tv_sec = 7, .tv_nsec = 8},
};
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.atime = oldtimes[0].tv_sec;
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
uint32_t valid = FATTR_ATIME | FATTR_MTIME;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
(time_t)in.body.setattr.atime ==
newtimes[0].tv_sec &&
in.body.setattr.atimensec ==
newtimes[0].tv_nsec &&
(time_t)in.body.setattr.mtime ==
newtimes[1].tv_sec &&
in.body.setattr.mtimensec ==
newtimes[1].tv_nsec);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.atime = newtimes[0].tv_sec;
out.body.attr.attr.atimensec = newtimes[0].tv_nsec;
out.body.attr.attr.mtime = newtimes[1].tv_sec;
out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
})));
EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
<< strerror(errno);
}
/* Change a file mtime but not its atime */
TEST_F(Setattr, utimensat_mtime_only) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const timespec oldtimes[2] = {
{.tv_sec = 1, .tv_nsec = 2},
{.tv_sec = 3, .tv_nsec = 4},
};
const timespec newtimes[2] = {
{.tv_sec = 5, .tv_nsec = UTIME_OMIT},
{.tv_sec = 7, .tv_nsec = 8},
};
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.atime = oldtimes[0].tv_sec;
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
uint32_t valid = FATTR_MTIME;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
(time_t)in.body.setattr.mtime ==
newtimes[1].tv_sec &&
in.body.setattr.mtimensec ==
newtimes[1].tv_nsec);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.atime = oldtimes[0].tv_sec;
out.body.attr.attr.atimensec = oldtimes[0].tv_nsec;
out.body.attr.attr.mtime = newtimes[1].tv_sec;
out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
})));
EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
<< strerror(errno);
}
/*
* Set a file's mtime and atime to now
*
* The design of FreeBSD's VFS does not allow fusefs to set just one of atime
* or mtime to UTIME_NOW; it's both or neither.
*/
TEST_F(Setattr, utimensat_utime_now) {
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const timespec oldtimes[2] = {
{.tv_sec = 1, .tv_nsec = 2},
{.tv_sec = 3, .tv_nsec = 4},
};
const timespec newtimes[2] = {
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
};
/* "now" is whatever the server says it is */
const timespec now[2] = {
{.tv_sec = 5, .tv_nsec = 7},
{.tv_sec = 6, .tv_nsec = 8},
};
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.atime = oldtimes[0].tv_sec;
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW |
FATTR_MTIME | FATTR_MTIME_NOW;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.atime = now[0].tv_sec;
out.body.attr.attr.atimensec = now[0].tv_nsec;
out.body.attr.attr.mtime = now[1].tv_sec;
out.body.attr.attr.mtimensec = now[1].tv_nsec;
out.body.attr.attr_valid = UINT64_MAX;
})));
ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
<< strerror(errno);
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec);
EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec);
EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec);
EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec);
}
/* On a read-only mount, no attributes may be changed */
TEST_F(RofsSetattr, erofs)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
})));
ASSERT_EQ(-1, chmod(FULLPATH, newmode));
ASSERT_EQ(EROFS, errno);
}
/* Change the mode of a file */
TEST_F(Setattr_7_8, chmod)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = S_IFREG | oldmode;
out.body.entry.nodeid = ino;
})));
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
uint32_t valid = FATTR_MODE;
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr_7_8);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
})));
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}

View File

@ -0,0 +1,171 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/mount.h>
#include <semaphore.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Statfs: public FuseTest {};
TEST_F(Statfs, eio)
{
struct statfs statbuf;
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(EIO)));
ASSERT_NE(0, statfs("mountpoint", &statbuf));
ASSERT_EQ(EIO, errno);
}
/*
* When the daemon is dead but the filesystem is still mounted, fuse(4) fakes
* the statfs(2) response, which is necessary for unmounting.
*/
TEST_F(Statfs, enotconn)
{
struct statfs statbuf;
char mp[PATH_MAX];
m_mock->kill_daemon();
ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
strlcat(mp, "/mountpoint", PATH_MAX);
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
EXPECT_EQ(getuid(), statbuf.f_owner);
EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
}
static void* statfs_th(void* arg) {
ssize_t r;
struct statfs *sb = (struct statfs*)arg;
r = statfs("mountpoint", sb);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
/*
* Like the enotconn test, but in this case the daemon dies after we send the
* FUSE_STATFS operation but before we get a response.
*/
TEST_F(Statfs, enotconn_while_blocked)
{
struct statfs statbuf;
void *thr0_value;
pthread_t th0;
char mp[PATH_MAX];
sem_t sem;
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
sem_post(&sem);
/* Just block until the daemon dies */
}));
ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
strlcat(mp, "/mountpoint", PATH_MAX);
ASSERT_EQ(0, pthread_create(&th0, NULL, statfs_th, (void*)&statbuf))
<< strerror(errno);
ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
m_mock->kill_daemon();
pthread_join(th0, &thr0_value);
ASSERT_EQ(0, (intptr_t)thr0_value);
EXPECT_EQ(getuid(), statbuf.f_owner);
EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
}
TEST_F(Statfs, ok)
{
struct statfs statbuf;
char mp[PATH_MAX];
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, statfs);
out.body.statfs.st.blocks = 1000;
out.body.statfs.st.bfree = 100;
out.body.statfs.st.bavail = 200;
out.body.statfs.st.files = 5;
out.body.statfs.st.ffree = 6;
out.body.statfs.st.namelen = 128;
out.body.statfs.st.frsize = 1024;
})));
ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
strlcat(mp, "/mountpoint", PATH_MAX);
ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
EXPECT_EQ(1024ul, statbuf.f_bsize);
/*
* fuse(4) ignores the filesystem's reported optimal transfer size, and
* chooses a size that works well with the rest of the system instead
*/
EXPECT_EQ(1000ul, statbuf.f_blocks);
EXPECT_EQ(100ul, statbuf.f_bfree);
EXPECT_EQ(200l, statbuf.f_bavail);
EXPECT_EQ(5ul, statbuf.f_files);
EXPECT_EQ(6l, statbuf.f_ffree);
EXPECT_EQ(128u, statbuf.f_namemax);
EXPECT_EQ(getuid(), statbuf.f_owner);
EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
}

View File

@ -0,0 +1,178 @@
/*-
* 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.
*/
extern "C" {
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Symlink: public FuseTest {
public:
void expect_symlink(uint64_t ino, const char *target, const char *relpath)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes;
const char *linkname = name + strlen(name) + 1;
return (in.header.opcode == FUSE_SYMLINK &&
(0 == strcmp(linkname, target)) &&
(0 == strcmp(name, relpath)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFLNK | 0777;
out.body.entry.nodeid = ino;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
})));
}
};
class Symlink_7_8: public FuseTest {
public:
virtual void SetUp() {
m_kernel_minor_version = 8;
FuseTest::SetUp();
}
void expect_symlink(uint64_t ino, const char *target, const char *relpath)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes;
const char *linkname = name + strlen(name) + 1;
return (in.header.opcode == FUSE_SYMLINK &&
(0 == strcmp(linkname, target)) &&
(0 == strcmp(name, relpath)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = S_IFLNK | 0777;
out.body.entry.nodeid = ino;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr_valid = UINT64_MAX;
})));
}
};
/*
* A successful symlink should clear the parent directory's attribute cache,
* because the fuse daemon should update its mtime and ctime
*/
TEST_F(Symlink, clear_attr_cache)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "dst";
const uint64_t ino = 42;
struct stat sb;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = FUSE_ROOT_ID;
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
expect_symlink(ino, dst, RELPATH);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
}
TEST_F(Symlink, enospc)
{
const char FULLPATH[] = "mountpoint/lnk";
const char RELPATH[] = "lnk";
const char dst[] = "dst";
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes;
const char *linkname = name + strlen(name) + 1;
return (in.header.opcode == FUSE_SYMLINK &&
(0 == strcmp(linkname, dst)) &&
(0 == strcmp(name, RELPATH)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(ENOSPC)));
EXPECT_EQ(-1, symlink(dst, FULLPATH));
EXPECT_EQ(ENOSPC, errno);
}
TEST_F(Symlink, ok)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "dst";
const uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_symlink(ino, dst, RELPATH);
EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
}
TEST_F(Symlink_7_8, ok)
{
const char FULLPATH[] = "mountpoint/src";
const char RELPATH[] = "src";
const char dst[] = "dst";
const uint64_t ino = 42;
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_symlink(ino, dst, RELPATH);
EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
}

View File

@ -0,0 +1,251 @@
/*-
* Copyright (c) 2019 The FreeBSD Foundation
* All rights reserved.
*
* 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.
*/
extern "C" {
#include <fcntl.h>
#include <semaphore.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class Unlink: public FuseTest {
public:
void expect_getattr(uint64_t ino, mode_t mode)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = mode;
out.body.attr.attr_valid = UINT64_MAX;
})));
}
void expect_lookup(const char *relpath, uint64_t ino, int times, int nlink=1)
{
EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
.Times(times)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = nlink;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.attr.size = 0;
})));
}
};
/*
* Unlinking a multiply linked file should update its ctime and nlink. This
* could be handled simply by invalidating the attributes, necessitating a new
* GETATTR, but we implement it in-kernel for efficiency's sake.
*/
TEST_F(Unlink, attr_cache)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char RELPATH0[] = "some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
uint64_t ino = 42;
struct stat sb_old, sb_new;
int fd1;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH0, ino, 1, 2);
expect_lookup(RELPATH1, ino, 1, 2);
expect_open(ino, 0, 1);
expect_unlink(1, RELPATH0, 0);
fd1 = open(FULLPATH1, O_RDONLY);
ASSERT_LE(0, fd1) << strerror(errno);
ASSERT_EQ(0, fstat(fd1, &sb_old)) << strerror(errno);
ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
ASSERT_EQ(0, fstat(fd1, &sb_new)) << strerror(errno);
EXPECT_NE(sb_old.st_ctime, sb_new.st_ctime);
EXPECT_EQ(1u, sb_new.st_nlink);
leak(fd1);
}
/*
* A successful unlink should clear the parent directory's attribute cache,
* because the fuse daemon should update its mtime and ctime
*/
TEST_F(Unlink, parent_attr_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
struct stat sb;
uint64_t ino = 42;
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == FUSE_ROOT_ID);
}, Eq(true)),
_)
).Times(2)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFDIR | 0755;
out.body.attr.attr_valid = UINT64_MAX;
})));
/* Use nlink=2 so we don't get a FUSE_FORGET */
expect_lookup(RELPATH, ino, 1, 2);
expect_unlink(1, RELPATH, 0);
ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
}
TEST_F(Unlink, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH, ino, 1);
expect_unlink(1, RELPATH, EPERM);
ASSERT_NE(0, unlink(FULLPATH));
ASSERT_EQ(EPERM, errno);
}
/*
* Unlinking a file should expire its entry cache, even if it's multiply linked
*/
TEST_F(Unlink, entry_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH, ino, 2, 2);
expect_unlink(1, RELPATH, 0);
ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
}
/*
* Unlink a multiply-linked file. There should be no FUSE_FORGET because the
* file is still linked.
*/
TEST_F(Unlink, multiply_linked)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char RELPATH0[] = "some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
uint64_t ino = 42;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH0, ino, 1, 2);
expect_unlink(1, RELPATH0, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FORGET &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(0);
expect_lookup(RELPATH1, ino, 1, 1);
ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
/*
* The final syscall simply ensures that no FUSE_FORGET was ever sent,
* by scheduling an arbitrary different operation after a FUSE_FORGET
* would've been sent.
*/
ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno);
}
TEST_F(Unlink, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
sem_t sem;
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH, ino, 1);
expect_unlink(1, RELPATH, 0);
expect_forget(ino, 1, &sem);
ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
sem_wait(&sem);
sem_destroy(&sem);
}
/* Unlink an open file */
TEST_F(Unlink, open_but_deleted)
{
const char FULLPATH0[] = "mountpoint/some_file.txt";
const char RELPATH0[] = "some_file.txt";
const char FULLPATH1[] = "mountpoint/other_file.txt";
const char RELPATH1[] = "other_file.txt";
uint64_t ino = 42;
int fd;
expect_getattr(1, S_IFDIR | 0755);
expect_lookup(RELPATH0, ino, 2);
expect_open(ino, 0, 1);
expect_unlink(1, RELPATH0, 0);
expect_lookup(RELPATH1, ino, 1, 1);
fd = open(FULLPATH0, O_RDWR);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
/*
* The final syscall simply ensures that no FUSE_FORGET was ever sent,
* by scheduling an arbitrary different operation after a FUSE_FORGET
* would've been sent.
*/
ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno);
leak(fd);
}

View File

@ -0,0 +1,593 @@
/*-
* 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.
*/
extern "C" {
#include <sys/param.h>
#include <sys/mman.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <dirent.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <semaphore.h>
#include <unistd.h>
}
#include <gtest/gtest.h>
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/*
* The default max_write is set to this formula in libfuse, though
* individual filesystems can lower it. The "- 4096" was added in
* commit 154ffe2, with the commit message "fix".
*/
const uint32_t libfuse_max_write = 32 * getpagesize() + 0x1000 - 4096;
/*
* Set the default max_write to a distinct value from MAXPHYS to catch bugs
* that confuse the two.
*/
const uint32_t default_max_write = MIN(libfuse_max_write, MAXPHYS / 2);
/* Check that fusefs(4) is accessible and the current user can mount(2) */
void check_environment()
{
const char *devnode = "/dev/fuse";
const char *usermount_node = "vfs.usermount";
int usermount_val = 0;
size_t usermount_size = sizeof(usermount_val);
if (eaccess(devnode, R_OK | W_OK)) {
if (errno == ENOENT) {
GTEST_SKIP() << devnode << " does not exist";
} else if (errno == EACCES) {
GTEST_SKIP() << devnode <<
" is not accessible by the current user";
} else {
GTEST_SKIP() << strerror(errno);
}
}
sysctlbyname(usermount_node, &usermount_val, &usermount_size,
NULL, 0);
if (geteuid() != 0 && !usermount_val)
GTEST_SKIP() << "current user is not allowed to mount";
}
class FuseEnv: public Environment {
virtual void SetUp() {
}
};
void FuseTest::SetUp() {
const char *maxbcachebuf_node = "vfs.maxbcachebuf";
const char *maxphys_node = "kern.maxphys";
int val = 0;
size_t size = sizeof(val);
/*
* XXX check_environment should be called from FuseEnv::SetUp, but
* can't due to https://github.com/google/googletest/issues/2189
*/
check_environment();
if (IsSkipped())
return;
ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &val, &size, NULL, 0))
<< strerror(errno);
m_maxbcachebuf = val;
ASSERT_EQ(0, sysctlbyname(maxphys_node, &val, &size, NULL, 0))
<< strerror(errno);
m_maxphys = val;
try {
m_mock = new MockFS(m_maxreadahead, m_allow_other,
m_default_permissions, m_push_symlinks_in, m_ro,
m_pm, m_init_flags, m_kernel_minor_version,
m_maxwrite, m_async, m_noclusterr, m_time_gran,
m_nointr);
/*
* FUSE_ACCESS is called almost universally. Expecting it in
* each test case would be super-annoying. Instead, set a
* default expectation for FUSE_ACCESS and return ENOSYS.
*
* Individual test cases can override this expectation since
* googlemock evaluates expectations in LIFO order.
*/
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_ACCESS);
}, Eq(true)),
_)
).Times(AnyNumber())
.WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
/*
* FUSE_BMAP is called for most test cases that read data. Set
* a default expectation and return ENOSYS.
*
* Individual test cases can override this expectation since
* googlemock evaluates expectations in LIFO order.
*/
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_BMAP);
}, Eq(true)),
_)
).Times(AnyNumber())
.WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
} catch (std::system_error err) {
FAIL() << err.what();
}
}
void
FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_ACCESS &&
in.header.nodeid == ino &&
in.body.access.mask == access_mode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void
FuseTest::expect_destroy(int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_DESTROY);
}, Eq(true)),
_)
).WillOnce(Invoke( ReturnImmediate([&](auto in, auto& out) {
m_mock->m_quit = true;
out.header.len = sizeof(out.header);
out.header.unique = in.header.unique;
out.header.error = -error;
})));
}
void
FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FLUSH &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(r));
}
void
FuseTest::expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_FORGET &&
in.header.nodeid == ino &&
in.body.forget.nlookup == nlookup);
}, Eq(true)),
_)
).WillOnce(Invoke([=](auto in __unused, auto &out __unused) {
if (sem != NULL)
sem_post(sem);
/* FUSE_FORGET has no response! */
}));
}
void FuseTest::expect_getattr(uint64_t ino, uint64_t size)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.size = size;
out.body.attr.attr_valid = UINT64_MAX;
})));
}
void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid)
{
EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
.Times(times)
.WillRepeatedly(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.attr.nlink = 1;
out.body.entry.attr_valid = attr_valid;
out.body.entry.attr.size = size;
out.body.entry.attr.uid = uid;
out.body.entry.attr.gid = gid;
})));
}
void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode,
uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid)
{
EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
.Times(times)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry_7_8);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
out.body.entry.attr.nlink = 1;
out.body.entry.attr_valid = attr_valid;
out.body.entry.attr.size = size;
out.body.entry.attr.uid = uid;
out.body.entry.attr.gid = gid;
})));
}
void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPEN &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
out.body.open.fh = FH;
out.body.open.open_flags = flags;
})));
}
void FuseTest::expect_opendir(uint64_t ino)
{
/* opendir(3) calls fstatfs */
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_STATFS);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(
ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, statfs);
})));
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_OPENDIR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(out.header);
SET_OUT_HEADER_LEN(out, open);
out.body.open.fh = FH;
})));
}
void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, const void *contents, int flags)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_READ &&
in.header.nodeid == ino &&
in.body.read.fh == FH &&
in.body.read.offset == offset &&
in.body.read.size == isize &&
flags == -1 ?
(in.body.read.flags == O_RDONLY ||
in.body.read.flags == O_RDWR)
: in.body.read.flags == (uint32_t)flags);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
out.header.len = sizeof(struct fuse_out_header) + osize;
memmove(out.body.bytes, contents, osize);
}))).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);
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*)
((intmax_t*)fde + entsize / sizeof(intmax_t));
i++;
}
out.header.len += sizeof(out.header);
})));
}
void FuseTest::expect_release(uint64_t ino, uint64_t fh)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASE &&
in.header.nodeid == ino &&
in.body.release.fh == fh);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
}
void FuseTest::expect_releasedir(uint64_t ino, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASEDIR &&
in.header.nodeid == ino &&
in.body.release.fh == FH);
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
void FuseTest::expect_unlink(uint64_t parent, const char *path, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_UNLINK &&
0 == strcmp(path, in.body.unlink) &&
in.header.nodeid == parent);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, uint32_t flags_set, uint32_t flags_unset,
const void *contents)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *buf = (const char*)in.body.bytes +
sizeof(struct fuse_write_in);
bool pid_ok;
uint32_t wf = in.body.write.write_flags;
if (wf & FUSE_WRITE_CACHE)
pid_ok = true;
else
pid_ok = (pid_t)in.header.pid == getpid();
return (in.header.opcode == FUSE_WRITE &&
in.header.nodeid == ino &&
in.body.write.fh == FH &&
in.body.write.offset == offset &&
in.body.write.size == isize &&
pid_ok &&
(wf & flags_set) == flags_set &&
(wf & flags_unset) == 0 &&
(in.body.write.flags == O_WRONLY ||
in.body.write.flags == O_RDWR) &&
0 == bcmp(buf, contents, isize));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, write);
out.body.write.size = osize;
})));
}
void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, const void *contents)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *buf = (const char*)in.body.bytes +
FUSE_COMPAT_WRITE_IN_SIZE;
bool pid_ok = (pid_t)in.header.pid == getpid();
return (in.header.opcode == FUSE_WRITE &&
in.header.nodeid == ino &&
in.body.write.fh == FH &&
in.body.write.offset == offset &&
in.body.write.size == isize &&
pid_ok &&
0 == bcmp(buf, contents, isize));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, write);
out.body.write.size = osize;
})));
}
void
get_unprivileged_id(uid_t *uid, gid_t *gid)
{
struct passwd *pw;
struct group *gr;
/*
* First try "tests", Kyua's default unprivileged user. XXX after
* GoogleTest gains a proper Kyua wrapper, get this with the Kyua API
*/
pw = getpwnam("tests");
if (pw == NULL) {
/* Fall back to "nobody" */
pw = getpwnam("nobody");
}
if (pw == NULL)
GTEST_SKIP() << "Test requires an unprivileged user";
/* Use group "nobody", which is Kyua's default unprivileged group */
gr = getgrnam("nobody");
if (gr == NULL)
GTEST_SKIP() << "Test requires an unprivileged group";
*uid = pw->pw_uid;
*gid = gr->gr_gid;
}
void
FuseTest::fork(bool drop_privs, int *child_status,
std::function<void()> parent_func,
std::function<int()> child_func)
{
sem_t *sem;
int mprot = PROT_READ | PROT_WRITE;
int mflags = MAP_ANON | MAP_SHARED;
pid_t child;
uid_t uid;
gid_t gid;
if (drop_privs) {
get_unprivileged_id(&uid, &gid);
if (IsSkipped())
return;
}
sem = (sem_t*)mmap(NULL, sizeof(*sem), mprot, mflags, -1, 0);
ASSERT_NE(MAP_FAILED, sem) << strerror(errno);
ASSERT_EQ(0, sem_init(sem, 1, 0)) << strerror(errno);
if ((child = ::fork()) == 0) {
/* In child */
int err = 0;
if (sem_wait(sem)) {
perror("sem_wait");
err = 1;
goto out;
}
if (drop_privs && 0 != setegid(gid)) {
perror("setegid");
err = 1;
goto out;
}
if (drop_privs && 0 != setreuid(-1, uid)) {
perror("setreuid");
err = 1;
goto out;
}
err = child_func();
out:
sem_destroy(sem);
_exit(err);
} else if (child > 0) {
/*
* In parent. Cleanup must happen here, because it's still
* privileged.
*/
m_mock->m_child_pid = child;
ASSERT_NO_FATAL_FAILURE(parent_func());
/* Signal the child process to go */
ASSERT_EQ(0, sem_post(sem)) << strerror(errno);
ASSERT_LE(0, wait(child_status)) << strerror(errno);
} else {
FAIL() << strerror(errno);
}
munmap(sem, sizeof(*sem));
return;
}
static void usage(char* progname) {
fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname);
exit(2);
}
int main(int argc, char **argv) {
int ch;
FuseEnv *fuse_env = new FuseEnv;
InitGoogleTest(&argc, argv);
AddGlobalTestEnvironment(fuse_env);
while ((ch = getopt(argc, argv, "v")) != -1) {
switch (ch) {
case 'v':
verbosity++;
break;
default:
usage(argv[0]);
break;
}
}
return (RUN_ALL_TESTS());
}

View File

@ -0,0 +1,236 @@
/*-
* 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.
*/
struct _sem;
typedef struct _sem sem_t;
struct _dirdesc;
typedef struct _dirdesc DIR;
/* Nanoseconds to sleep, for tests that must */
#define NAP_NS (100'000'000)
void get_unprivileged_id(uid_t *uid, gid_t *gid);
inline void nap()
{
usleep(NAP_NS / 1000);
}
extern const uint32_t libfuse_max_write;
extern const uint32_t default_max_write;
class FuseTest : public ::testing::Test {
protected:
uint32_t m_maxreadahead;
uint32_t m_maxwrite;
uint32_t m_init_flags;
bool m_allow_other;
bool m_default_permissions;
uint32_t m_kernel_minor_version;
enum poll_method m_pm;
bool m_push_symlinks_in;
bool m_ro;
bool m_async;
bool m_noclusterr;
bool m_nointr;
unsigned m_time_gran;
MockFS *m_mock = NULL;
const static uint64_t FH = 0xdeadbeef1a7ebabe;
public:
int m_maxbcachebuf;
int m_maxphys;
FuseTest():
m_maxreadahead(0),
m_maxwrite(default_max_write),
m_init_flags(0),
m_allow_other(false),
m_default_permissions(false),
m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION),
m_pm(BLOCKING),
m_push_symlinks_in(false),
m_ro(false),
m_async(false),
m_noclusterr(false),
m_nointr(false),
m_time_gran(1)
{}
virtual void SetUp();
virtual void TearDown() {
if (m_mock)
delete m_mock;
}
/*
* Create an expectation that FUSE_ACCESS will be called once for the
* given inode with the given access_mode, returning the given errno
*/
void expect_access(uint64_t ino, mode_t access_mode, int error);
/* Expect FUSE_DESTROY and shutdown the daemon */
void expect_destroy(int error);
/*
* Create an expectation that FUSE_FLUSH will be called times times for
* the given inode
*/
void expect_flush(uint64_t ino, int times, ProcessMockerT r);
/*
* Create an expectation that FUSE_FORGET will be called for the given
* inode. There will be no response. If sem is provided, it will be
* posted after the operation is received by the daemon.
*/
void expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem = NULL);
/*
* Create an expectation that FUSE_GETATTR will be called for the given
* inode any number of times. It will respond with a few basic
* attributes, like the given size and the mode S_IFREG | 0644
*/
void expect_getattr(uint64_t ino, uint64_t size);
/*
* Create an expectation that FUSE_LOOKUP will be called for the given
* path exactly times times and cache validity period. It will respond
* with inode ino, mode mode, filesize size.
*/
void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
uint64_t size, int times, uint64_t attr_valid = UINT64_MAX,
uid_t uid = 0, gid_t gid = 0);
/* The protocol 7.8 version of expect_lookup */
void expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode,
uint64_t size, int times, uint64_t attr_valid = UINT64_MAX,
uid_t uid = 0, gid_t gid = 0);
/*
* Create an expectation that FUSE_OPEN will be called for the given
* inode exactly times times. It will return with open_flags flags and
* file handle FH.
*/
void expect_open(uint64_t ino, uint32_t flags, int times);
/*
* Create an expectation that FUSE_OPENDIR will be called exactly once
* for inode ino.
*/
void expect_opendir(uint64_t ino);
/*
* Create an expectation that FUSE_READ will be called exactly once for
* the given inode, at offset offset and with size isize. It will
* return the first osize bytes from contents
*
* Protocol 7.8 tests can use this same expectation method because
* nothing currently validates the size of the fuse_read_in struct.
*/
void expect_read(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, const void *contents, int flags = -1);
/*
* 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
*/
void expect_release(uint64_t ino, uint64_t fh);
/*
* Create an expectation that FUSE_RELEASEDIR will be called exactly
* once for the given inode
*/
void expect_releasedir(uint64_t ino, ProcessMockerT r);
/*
* Create an expectation that FUSE_UNLINK will be called exactly once
* for the given path, returning an errno
*/
void expect_unlink(uint64_t parent, const char *path, int error);
/*
* Create an expectation that FUSE_WRITE will be called exactly once
* for the given inode, at offset offset, with size isize and buffer
* contents. Any flags present in flags_set must be set, and any
* present in flags_unset must not be set. Other flags are don't care.
* It will return osize.
*/
void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, uint32_t flags_set, uint32_t flags_unset,
const void *contents);
/* Protocol 7.8 version of expect_write */
void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize,
uint64_t osize, const void *contents);
/*
* Helper that runs code in a child process.
*
* First, parent_func runs in the parent process.
* Then, child_func runs in the child process, dropping privileges if
* desired.
* Finally, fusetest_fork returns.
*
* # Returns
*
* fusetest_fork may SKIP the test, which the caller should detect with
* the IsSkipped() method. If not, then the child's exit status will
* be returned in status.
*/
void fork(bool drop_privs, int *status,
std::function<void()> parent_func,
std::function<int()> child_func);
/*
* Deliberately leak a file descriptor.
*
* Closing a file descriptor on fusefs would cause the server to
* receive FUSE_CLOSE and possibly FUSE_INACTIVE. Handling those
* operations would needlessly complicate most tests. So most tests
* deliberately leak the file descriptors instead. This method serves
* to document the leakage, and provide a single point of suppression
* for static analyzers.
*/
static void leak(int fd __unused) {}
/*
* Deliberately leak a DIR* pointer
*
* See comments for FuseTest::leak
*/
static void leakdir(DIR* dirp __unused) {}
};

1309
tests/sys/fs/fusefs/write.cc Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,641 @@
/*-
* 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.
*/
/* Tests for all things relating to extended attributes and FUSE */
extern "C" {
#include <sys/types.h>
#include <sys/extattr.h>
#include <string.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
class Xattr: public FuseTest {
public:
void expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *a = (const char*)in.body.bytes +
sizeof(fuse_getxattr_in);
return (in.header.opcode == FUSE_GETXATTR &&
in.header.nodeid == ino &&
0 == strcmp(attr, a));
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_LISTXATTR &&
in.header.nodeid == ino &&
in.body.listxattr.size == size);
}, Eq(true)),
_)
).WillOnce(Invoke(r))
.RetiresOnSaturation();
}
void expect_removexattr(uint64_t ino, const char *attr, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *a = (const char*)in.body.bytes;
return (in.header.opcode == FUSE_REMOVEXATTR &&
in.header.nodeid == ino &&
0 == strcmp(attr, a));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void expect_setxattr(uint64_t ino, const char *attr, const char *value,
ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *a = (const char*)in.body.bytes +
sizeof(fuse_setxattr_in);
const char *v = a + strlen(a) + 1;
return (in.header.opcode == FUSE_SETXATTR &&
in.header.nodeid == ino &&
0 == strcmp(attr, a) &&
0 == strcmp(value, v));
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
class Getxattr: public Xattr {};
class Listxattr: public Xattr {};
class Removexattr: public Xattr {};
class Setxattr: public Xattr {};
class RofsXattr: public Xattr {
public:
virtual void SetUp() {
m_ro = true;
Xattr::SetUp();
}
};
/*
* If the extended attribute does not exist on this file, the daemon should
* return ENOATTR (ENODATA on Linux, but it's up to the daemon to choose the
* correct errror code)
*/
TEST_F(Getxattr, enoattr)
{
char data[80];
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR));
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(-1, r);
ASSERT_EQ(ENOATTR, errno);
}
/*
* If the filesystem returns ENOSYS, then it will be treated as a permanent
* failure and all future VOP_GETEXTATTR calls will fail with EOPNOTSUPP
* without querying the filesystem daemon
*/
TEST_F(Getxattr, enosys)
{
char data[80];
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
expect_getxattr(ino, "user.foo", ReturnErrno(ENOSYS));
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(-1, r);
EXPECT_EQ(EOPNOTSUPP, errno);
/* Subsequent attempts should not query the filesystem at all */
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(-1, r);
EXPECT_EQ(EOPNOTSUPP, errno);
}
/*
* On FreeBSD, if the user passes an insufficiently large buffer then the
* filesystem is supposed to copy as much of the attribute's value as will fit.
*
* On Linux, however, the filesystem is supposed to return ERANGE.
*
* libfuse specifies the Linux behavior. However, that's probably an error.
* It would probably be correct for the filesystem to use platform-dependent
* behavior.
*
* This test case covers a filesystem that uses the Linux behavior
*/
TEST_F(Getxattr, erange)
{
char data[10];
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_getxattr(ino, "user.foo", ReturnErrno(ERANGE));
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(-1, r);
ASSERT_EQ(ERANGE, errno);
}
/*
* If the user passes a 0-length buffer, then the daemon should just return the
* size of the attribute
*/
TEST_F(Getxattr, size_only)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_getxattr(ino, "user.foo",
ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, getxattr);
out.body.getxattr.size = 99;
})
);
ASSERT_EQ(99, extattr_get_file(FULLPATH, ns, "foo", NULL, 0))
<< strerror(errno);;
}
/*
* Successfully get an attribute from the system namespace
*/
TEST_F(Getxattr, system)
{
uint64_t ino = 42;
char data[80];
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_SYSTEM;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_getxattr(ino, "system.foo",
ReturnImmediate([&](auto in __unused, auto& out) {
memcpy((void*)out.body.bytes, value, value_len);
out.header.len = sizeof(out.header) + value_len;
})
);
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(value_len, r) << strerror(errno);
EXPECT_STREQ(value, data);
}
/*
* Successfully get an attribute from the user namespace
*/
TEST_F(Getxattr, user)
{
uint64_t ino = 42;
char data[80];
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_getxattr(ino, "user.foo",
ReturnImmediate([&](auto in __unused, auto& out) {
memcpy((void*)out.body.bytes, value, value_len);
out.header.len = sizeof(out.header) + value_len;
})
);
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(value_len, r) << strerror(errno);
EXPECT_STREQ(value, data);
}
/*
* If the filesystem returns ENOSYS, then it will be treated as a permanent
* failure and all future VOP_LISTEXTATTR calls will fail with EOPNOTSUPP
* without querying the filesystem daemon
*/
TEST_F(Listxattr, enosys)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
expect_listxattr(ino, 0, ReturnErrno(ENOSYS));
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
EXPECT_EQ(EOPNOTSUPP, errno);
/* Subsequent attempts should not query the filesystem at all */
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
EXPECT_EQ(EOPNOTSUPP, errno);
}
/*
* Listing extended attributes failed because they aren't configured on this
* filesystem
*/
TEST_F(Listxattr, enotsup)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0, ReturnErrno(ENOTSUP));
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
ASSERT_EQ(ENOTSUP, errno);
}
/*
* On FreeBSD, if the user passes an insufficiently large buffer then the
* filesystem is supposed to copy as much of the attribute's value as will fit.
*
* On Linux, however, the filesystem is supposed to return ERANGE.
*
* libfuse specifies the Linux behavior. However, that's probably an error.
* It would probably be correct for the filesystem to use platform-dependent
* behavior.
*
* This test case covers a filesystem that uses the Linux behavior
*/
TEST_F(Listxattr, erange)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0, ReturnErrno(ERANGE));
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
ASSERT_EQ(ERANGE, errno);
}
/*
* Get the size of the list that it would take to list no extended attributes
*/
TEST_F(Listxattr, size_only_empty)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
out.body.listxattr.size = 0;
SET_OUT_HEADER_LEN(out, listxattr);
}));
ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
<< strerror(errno);
}
/*
* Get the size of the list that it would take to list some extended
* attributes. Due to the format differences between a FreeBSD and a
* Linux/FUSE extended attribute list, fuse(4) will actually allocate a buffer
* and get the whole list, then convert it, just to figure out its size.
*/
TEST_F(Listxattr, size_only_nonempty)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
out.body.listxattr.size = 45;
SET_OUT_HEADER_LEN(out, listxattr);
}));
// TODO: fix the expected size after fixing the size calculation bug in
// fuse_vnop_listextattr. It should be exactly 45.
expect_listxattr(ino, 53,
ReturnImmediate([](auto in __unused, auto& out) {
const char l[] = "user.foo";
strlcpy((char*)out.body.bytes, l,
sizeof(out.body.bytes));
out.header.len = sizeof(fuse_out_header) + sizeof(l);
})
);
ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0))
<< strerror(errno);
}
TEST_F(Listxattr, size_only_really_big)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
out.body.listxattr.size = 16000;
SET_OUT_HEADER_LEN(out, listxattr);
}));
// TODO: fix the expected size after fixing the size calculation bug in
// fuse_vnop_listextattr. It should be exactly 16000.
expect_listxattr(ino, 16008,
ReturnImmediate([](auto in __unused, auto& out) {
const char l[16] = "user.foobarbang";
for (int i=0; i < 1000; i++) {
memcpy(&out.body.bytes[16 * i], l, 16);
}
out.header.len = sizeof(fuse_out_header) + 16000;
})
);
ASSERT_EQ(11000, extattr_list_file(FULLPATH, ns, NULL, 0))
<< strerror(errno);
}
/*
* List all of the user attributes of a file which has both user and system
* attributes
*/
TEST_F(Listxattr, user)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
char data[80];
char expected[9] = {3, 'f', 'o', 'o', 4, 'b', 'a', 'n', 'g'};
char attrs[28] = "user.foo\0system.x\0user.bang";
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0,
ReturnImmediate([&](auto in __unused, auto& out) {
out.body.listxattr.size = sizeof(attrs);
SET_OUT_HEADER_LEN(out, listxattr);
})
);
// TODO: fix the expected size after fixing the size calculation bug in
// fuse_vnop_listextattr.
expect_listxattr(ino, sizeof(attrs) + 8,
ReturnImmediate([&](auto in __unused, auto& out) {
memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
}));
ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
extattr_list_file(FULLPATH, ns, data, sizeof(data)))
<< strerror(errno);
ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
}
/*
* List all of the system attributes of a file which has both user and system
* attributes
*/
TEST_F(Listxattr, system)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_SYSTEM;
char data[80];
char expected[2] = {1, 'x'};
char attrs[28] = "user.foo\0system.x\0user.bang";
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_listxattr(ino, 0,
ReturnImmediate([&](auto in __unused, auto& out) {
out.body.listxattr.size = sizeof(attrs);
SET_OUT_HEADER_LEN(out, listxattr);
})
);
// TODO: fix the expected size after fixing the size calculation bug in
// fuse_vnop_listextattr.
expect_listxattr(ino, sizeof(attrs) + 8,
ReturnImmediate([&](auto in __unused, auto& out) {
memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
}));
ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
extattr_list_file(FULLPATH, ns, data, sizeof(data)))
<< strerror(errno);
ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
}
/* Fail to remove a nonexistent attribute */
TEST_F(Removexattr, enoattr)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_removexattr(ino, "user.foo", ENOATTR);
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
ASSERT_EQ(ENOATTR, errno);
}
/*
* If the filesystem returns ENOSYS, then it will be treated as a permanent
* failure and all future VOP_DELETEEXTATTR calls will fail with EOPNOTSUPP
* without querying the filesystem daemon
*/
TEST_F(Removexattr, enosys)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
expect_removexattr(ino, "user.foo", ENOSYS);
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
EXPECT_EQ(EOPNOTSUPP, errno);
/* Subsequent attempts should not query the filesystem at all */
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
EXPECT_EQ(EOPNOTSUPP, errno);
}
/* Successfully remove a user xattr */
TEST_F(Removexattr, user)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_removexattr(ino, "user.foo", 0);
ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
<< strerror(errno);
}
/* Successfully remove a system xattr */
TEST_F(Removexattr, system)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_SYSTEM;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_removexattr(ino, "system.foo", 0);
ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
<< strerror(errno);
}
/*
* If the filesystem returns ENOSYS, then it will be treated as a permanent
* failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP
* without querying the filesystem daemon
*/
TEST_F(Setxattr, enosys)
{
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOSYS));
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(-1, r);
EXPECT_EQ(EOPNOTSUPP, errno);
/* Subsequent attempts should not query the filesystem at all */
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(-1, r);
EXPECT_EQ(EOPNOTSUPP, errno);
}
/*
* SETXATTR will return ENOTSUP if the namespace is invalid or the filesystem
* as currently configured doesn't support extended attributes.
*/
TEST_F(Setxattr, enotsup)
{
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOTSUP));
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(-1, r);
EXPECT_EQ(ENOTSUP, errno);
}
/*
* Successfully set a user attribute.
*/
TEST_F(Setxattr, user)
{
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_setxattr(ino, "user.foo", value, ReturnErrno(0));
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(value_len, r) << strerror(errno);
}
/*
* Successfully set a system attribute.
*/
TEST_F(Setxattr, system)
{
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_SYSTEM;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_setxattr(ino, "system.foo", value, ReturnErrno(0));
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(value_len, r) << strerror(errno);
}
TEST_F(RofsXattr, deleteextattr_erofs)
{
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
ASSERT_EQ(EROFS, errno);
}
TEST_F(RofsXattr, setextattr_erofs)
{
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
value_len);
ASSERT_EQ(-1, r);
EXPECT_EQ(EROFS, errno);
}