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:
commit
0b4275accb
@ -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
|
||||
|
12
UPDATING
12
UPDATING
@ -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
|
||||
|
@ -731,6 +731,8 @@
|
||||
file
|
||||
..
|
||||
fs
|
||||
fusefs
|
||||
..
|
||||
tmpfs
|
||||
..
|
||||
..
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
@ -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
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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)))
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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_ */
|
||||
|
@ -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_ */
|
@ -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
@ -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,
|
||||
|
@ -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
|
||||
|
81
tests/sys/fs/fusefs/Makefile
Normal file
81
tests/sys/fs/fusefs/Makefile
Normal 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>
|
119
tests/sys/fs/fusefs/access.cc
Normal file
119
tests/sys/fs/fusefs/access.cc
Normal 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);
|
||||
}
|
303
tests/sys/fs/fusefs/allow_other.cc
Normal file
303
tests/sys/fs/fusefs/allow_other.cc
Normal 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
159
tests/sys/fs/fusefs/bmap.cc
Normal 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);
|
||||
}
|
449
tests/sys/fs/fusefs/create.cc
Normal file
449
tests/sys/fs/fusefs/create.cc
Normal 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);
|
||||
}
|
1305
tests/sys/fs/fusefs/default_permissions.cc
Normal file
1305
tests/sys/fs/fusefs/default_permissions.cc
Normal file
File diff suppressed because it is too large
Load Diff
124
tests/sys/fs/fusefs/default_permissions_privileged.cc
Normal file
124
tests/sys/fs/fusefs/default_permissions_privileged.cc
Normal 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);
|
||||
}
|
||||
|
||||
|
158
tests/sys/fs/fusefs/destroy.cc
Normal file
158
tests/sys/fs/fusefs/destroy.cc
Normal 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();
|
||||
}
|
227
tests/sys/fs/fusefs/dev_fuse_poll.cc
Normal file
227
tests/sys/fs/fusefs/dev_fuse_poll.cc
Normal 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
207
tests/sys/fs/fusefs/fifo.cc
Normal 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);
|
||||
}
|
232
tests/sys/fs/fusefs/flush.cc
Normal file
232
tests/sys/fs/fusefs/flush.cc
Normal 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);
|
||||
}
|
154
tests/sys/fs/fusefs/forget.cc
Normal file
154
tests/sys/fs/fusefs/forget.cc
Normal 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);
|
||||
}
|
249
tests/sys/fs/fusefs/fsync.cc
Normal file
249
tests/sys/fs/fusefs/fsync.cc
Normal 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);
|
||||
}
|
186
tests/sys/fs/fusefs/fsyncdir.cc
Normal file
186
tests/sys/fs/fusefs/fsyncdir.cc
Normal 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);
|
||||
}
|
300
tests/sys/fs/fusefs/getattr.cc
Normal file
300
tests/sys/fs/fusefs/getattr.cc
Normal 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.
|
||||
}
|
790
tests/sys/fs/fusefs/interrupt.cc
Normal file
790
tests/sys/fs/fusefs/interrupt.cc
Normal 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
543
tests/sys/fs/fusefs/io.cc
Normal 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
233
tests/sys/fs/fusefs/link.cc
Normal 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);
|
||||
}
|
478
tests/sys/fs/fusefs/locks.cc
Normal file
478
tests/sys/fs/fusefs/locks.cc
Normal 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);
|
||||
}
|
381
tests/sys/fs/fusefs/lookup.cc
Normal file
381
tests/sys/fs/fusefs/lookup.cc
Normal 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);
|
||||
}
|
||||
|
||||
|
222
tests/sys/fs/fusefs/mkdir.cc
Normal file
222
tests/sys/fs/fusefs/mkdir.cc
Normal 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);
|
||||
}
|
238
tests/sys/fs/fusefs/mknod.cc
Normal file
238
tests/sys/fs/fusefs/mknod.cc
Normal 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);
|
||||
}
|
733
tests/sys/fs/fusefs/mockfs.cc
Normal file
733
tests/sys/fs/fusefs/mockfs.cc
Normal 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);
|
||||
}
|
394
tests/sys/fs/fusefs/mockfs.hh
Normal file
394
tests/sys/fs/fusefs/mockfs.hh
Normal 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();
|
||||
};
|
152
tests/sys/fs/fusefs/mount.cc
Normal file
152
tests/sys/fs/fusefs/mount.cc
Normal 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
344
tests/sys/fs/fusefs/nfs.cc
Normal 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);
|
||||
}
|
545
tests/sys/fs/fusefs/notify.cc
Normal file
545
tests/sys/fs/fusefs/notify.cc
Normal 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
262
tests/sys/fs/fusefs/open.cc
Normal 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);
|
||||
}
|
||||
|
155
tests/sys/fs/fusefs/opendir.cc
Normal file
155
tests/sys/fs/fusefs/opendir.cc
Normal 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
916
tests/sys/fs/fusefs/read.cc
Normal 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)));
|
375
tests/sys/fs/fusefs/readdir.cc
Normal file
375
tests/sys/fs/fusefs/readdir.cc
Normal 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);
|
||||
}
|
123
tests/sys/fs/fusefs/readlink.cc
Normal file
123
tests/sys/fs/fusefs/readlink.cc
Normal 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);
|
||||
}
|
224
tests/sys/fs/fusefs/release.cc
Normal file
224
tests/sys/fs/fusefs/release.cc
Normal 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);
|
||||
}
|
116
tests/sys/fs/fusefs/releasedir.cc
Normal file
116
tests/sys/fs/fusefs/releasedir.cc
Normal 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);
|
||||
}
|
321
tests/sys/fs/fusefs/rename.cc
Normal file
321
tests/sys/fs/fusefs/rename.cc
Normal 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);
|
||||
}
|
172
tests/sys/fs/fusefs/rmdir.cc
Normal file
172
tests/sys/fs/fusefs/rmdir.cc
Normal 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);
|
||||
}
|
774
tests/sys/fs/fusefs/setattr.cc
Normal file
774
tests/sys/fs/fusefs/setattr.cc
Normal 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);
|
||||
}
|
171
tests/sys/fs/fusefs/statfs.cc
Normal file
171
tests/sys/fs/fusefs/statfs.cc
Normal 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));
|
||||
}
|
178
tests/sys/fs/fusefs/symlink.cc
Normal file
178
tests/sys/fs/fusefs/symlink.cc
Normal 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);
|
||||
}
|
251
tests/sys/fs/fusefs/unlink.cc
Normal file
251
tests/sys/fs/fusefs/unlink.cc
Normal 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);
|
||||
}
|
593
tests/sys/fs/fusefs/utils.cc
Normal file
593
tests/sys/fs/fusefs/utils.cc
Normal 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());
|
||||
}
|
236
tests/sys/fs/fusefs/utils.hh
Normal file
236
tests/sys/fs/fusefs/utils.hh
Normal 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
1309
tests/sys/fs/fusefs/write.cc
Normal file
File diff suppressed because it is too large
Load Diff
641
tests/sys/fs/fusefs/xattr.cc
Normal file
641
tests/sys/fs/fusefs/xattr.cc
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user