From nobody Sat Jan 01 04:05:44 2022 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id F1A6B1924184; Sat, 1 Jan 2022 04:05:44 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4JQpMX52nLz3wKX; Sat, 1 Jan 2022 04:05:44 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 8CBA217CFA; Sat, 1 Jan 2022 04:05:44 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.16.1/8.16.1) with ESMTP id 20145iYw035737; Sat, 1 Jan 2022 04:05:44 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.16.1/8.16.1/Submit) id 20145iKc035736; Sat, 1 Jan 2022 04:05:44 GMT (envelope-from git) Date: Sat, 1 Jan 2022 04:05:44 GMT Message-Id: <202201010405.20145iKc035736@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Alan Somers Subject: git: 398c88c7582a - main - fusefs: implement VOP_ALLOCATE List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: Sender: owner-dev-commits-src-all@freebsd.org X-BeenThere: dev-commits-src-all@freebsd.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: asomers X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 398c88c7582a195cbfeb689ceff1400cc717673f Auto-Submitted: auto-generated ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1641009944; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=4gL5+/rvTDurMO9T3WclsfrUvjDU2XCeFpssBuRGz8M=; b=ZY++XcTeYkDjx4JOrvEw/cM4S+VpKztqQ5FQXhqF3BdVqphMjHvsPz8rwFRg8deVnhD3GS brijfO97iJ/nVz7/gvG6IOx1YaVkq2X+NkXpS2XVaKCULOvZumJGlwRDz15luX/Kn5jVrS wEhXB7fbY+mUcpw2T5+Dd3b4Hbuz5ZyoHnPjqrIF/1NUej2PYOzG2mUlCHPEePpuMHuds4 n8OjCno62/tRLzHozADPLEt7Sp3u0LqijJYHOqjREKw990N4BOcM0snMJv2s8atHd5u/BO HFSyS9qb+DAaEjwOOqgTk66Dfkqi+e6CYiGl+cDzUFB6i7RW1SAJDAPho2pMug== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1641009944; a=rsa-sha256; cv=none; b=hMQYmdBWbyEanM7pwzjyqu9rqHaeED96Im1p6Nu1oyooP6UwXo8ta+CJQ3VpgIsigP+bWl WvUAWKCie3+gHM3Xe23gvBW0/nRCBB7Mh5fkBKXKu7LvF1fbEO+0PB23FsmQlBxarXxnTs mosDQdEIZG7jA/5uucqpb8O2CqqmV4z2GsedNe6TtwiEE9lvkz8mzAVATiQRieJGo9hoD8 R91WwjwqKFPGR55+MDLyrKEYDdrzMOem5p8Mw5aBqWeq55KCGKd77q47DY8KYVz3hzaece x6Uoe/91vjACBzG+A1VoEtZtnV1UbizrrmaGHSH1dR4krpfbbHIPzhD+MJXv5Q== ARC-Authentication-Results: i=1; mx1.freebsd.org; none X-ThisMailContainsUnwantedMimeParts: N The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=398c88c7582a195cbfeb689ceff1400cc717673f commit 398c88c7582a195cbfeb689ceff1400cc717673f Author: Alan Somers AuthorDate: 2022-01-01 04:04:12 +0000 Commit: Alan Somers CommitDate: 2022-01-01 04:05:28 +0000 fusefs: implement VOP_ALLOCATE Now posix_fallocate will be correctly forwarded to fuse file system servers, for those that support it. MFC after: 2 weeks Reviewed by: pfg Differential Revision: https://reviews.freebsd.org/D33389 --- sys/fs/fuse/fuse_internal.c | 4 + sys/fs/fuse/fuse_ipc.c | 4 + sys/fs/fuse/fuse_vnops.c | 93 ++++++++- tests/sys/fs/fusefs/Makefile | 1 + tests/sys/fs/fusefs/default_permissions.cc | 89 ++++++++- tests/sys/fs/fusefs/fallocate.cc | 297 +++++++++++++++++++++++++++++ tests/sys/fs/fusefs/last_local_modify.cc | 93 ++++++--- tests/sys/fs/fusefs/mockfs.cc | 13 +- tests/sys/fs/fusefs/mockfs.hh | 1 + tests/sys/fs/fusefs/utils.cc | 17 ++ tests/sys/fs/fusefs/utils.hh | 8 + 11 files changed, 593 insertions(+), 27 deletions(-) diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c index a4a8a3f6fce7..0b0c24827c1e 100644 --- a/sys/fs/fuse/fuse_internal.c +++ b/sys/fs/fuse/fuse_internal.c @@ -1074,6 +1074,10 @@ fuse_internal_init_callback(struct fuse_ticket *tick, struct uio *uio) fsess_set_notimpl(data->mp, FUSE_DESTROY); } + if (!fuse_libabi_geq(data, 7, 19)) { + fsess_set_notimpl(data->mp, FUSE_FALLOCATE); + } + if (fuse_libabi_geq(data, 7, 23) && fiio->time_gran >= 1 && fiio->time_gran <= 1000000000) data->time_gran = fiio->time_gran; diff --git a/sys/fs/fuse/fuse_ipc.c b/sys/fs/fuse/fuse_ipc.c index 0042de602739..7a6c0e3da76b 100644 --- a/sys/fs/fuse/fuse_ipc.c +++ b/sys/fs/fuse/fuse_ipc.c @@ -845,6 +845,10 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) err = (blen == 0) ? 0 : EINVAL; break; + case FUSE_FALLOCATE: + err = (blen == 0) ? 0 : EINVAL; + break; + case FUSE_LSEEK: err = (blen == sizeof(struct fuse_lseek_out)) ? 0 : EINVAL; break; diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c index 640f9d2f8243..31398596bc17 100644 --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -127,6 +127,7 @@ SDT_PROBE_DEFINE2(fusefs, , vnops, trace, "int", "char*"); /* vnode ops */ static vop_access_t fuse_vnop_access; static vop_advlock_t fuse_vnop_advlock; +static vop_allocate_t fuse_vnop_allocate; static vop_bmap_t fuse_vnop_bmap; static vop_close_t fuse_fifo_close; static vop_close_t fuse_vnop_close; @@ -180,7 +181,7 @@ struct vop_vector fuse_fifoops = { VFS_VOP_VECTOR_REGISTER(fuse_fifoops); struct vop_vector fuse_vnops = { - .vop_allocate = VOP_EINVAL, + .vop_allocate = fuse_vnop_allocate, .vop_default = &default_vnodeops, .vop_access = fuse_vnop_access, .vop_advlock = fuse_vnop_advlock, @@ -550,6 +551,96 @@ out: return err; } +static int +fuse_vnop_allocate(struct vop_allocate_args *ap) +{ + struct vnode *vp = ap->a_vp; + off_t *len = ap->a_len; + off_t *offset = ap->a_offset; + struct ucred *cred = ap->a_cred; + struct fuse_filehandle *fufh; + struct mount *mp = vnode_mount(vp); + struct fuse_dispatcher fdi; + struct fuse_fallocate_in *ffi; + struct uio io; + pid_t pid = curthread->td_proc->p_pid; + struct fuse_vnode_data *fvdat = VTOFUD(vp); + off_t filesize; + int err; + + if (fuse_isdeadfs(vp)) + return (ENXIO); + + switch (vp->v_type) { + case VFIFO: + return (ESPIPE); + case VLNK: + case VREG: + if (vfs_isrdonly(mp)) + return (EROFS); + break; + default: + return (ENODEV); + } + + if (vfs_isrdonly(mp)) + return (EROFS); + + if (fsess_not_impl(mp, FUSE_FALLOCATE)) + return (EINVAL); + + io.uio_offset = *offset; + io.uio_resid = *len; + err = vn_rlimit_fsize(vp, &io, curthread); + if (err) + return (err); + + err = fuse_filehandle_getrw(vp, FWRITE, &fufh, cred, pid); + if (err) + return (err); + + fuse_vnode_update(vp, FN_MTIMECHANGE | FN_CTIMECHANGE); + + err = fuse_vnode_size(vp, &filesize, cred, curthread); + if (err) + return (err); + fuse_inval_buf_range(vp, filesize, *offset, *offset + *len); + + fdisp_init(&fdi, sizeof(*ffi)); + fdisp_make_vp(&fdi, FUSE_FALLOCATE, vp, curthread, cred); + ffi = fdi.indata; + ffi->fh = fufh->fh_id; + ffi->offset = *offset; + ffi->length = *len; + ffi->mode = 0; + err = fdisp_wait_answ(&fdi); + + if (err == ENOSYS) { + fsess_set_notimpl(mp, FUSE_FALLOCATE); + err = EINVAL; + } else if (err == EOPNOTSUPP) { + /* + * The file system server does not support FUSE_FALLOCATE with + * the supplied mode. That's effectively the same thing as + * ENOSYS since we only ever issue mode=0. + * TODO: revise this section once we support fspacectl. + */ + fsess_set_notimpl(mp, FUSE_FALLOCATE); + err = EINVAL; + } else if (!err) { + *offset += *len; + *len = 0; + fuse_vnode_undirty_cached_timestamps(vp, false); + fuse_internal_clear_suid_on_write(vp, cred, curthread); + if (*offset > fvdat->cached_attrs.va_size) { + fuse_vnode_setsize(vp, *offset, false); + getnanouptime(&fvdat->last_local_modify); + } + } + + return (err); +} + /* { struct vnode *a_vp; daddr_t a_bn; diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index e508d3edbdea..04b68681ec9a 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -19,6 +19,7 @@ GTESTS+= default_permissions GTESTS+= default_permissions_privileged GTESTS+= destroy GTESTS+= dev_fuse_poll +GTESTS+= fallocate GTESTS+= fifo GTESTS+= flush GTESTS+= forget diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc index 6401f926bb49..0739ad48f1e2 100644 --- a/tests/sys/fs/fusefs/default_permissions.cc +++ b/tests/sys/fs/fusefs/default_permissions.cc @@ -163,6 +163,7 @@ class Chgrp: public DefaultPermissions {}; class CopyFileRange: public DefaultPermissions {}; class Lookup: public DefaultPermissions {}; class Open: public DefaultPermissions {}; +class PosixFallocate: public DefaultPermissions {}; class Setattr: public DefaultPermissions {}; class Unlink: public DefaultPermissions {}; class Utimensat: public DefaultPermissions {}; @@ -498,7 +499,7 @@ TEST_F(Chgrp, ok) } /* A write by a non-owner should clear a file's SGID bit */ -TEST_F(CopyFileRange, clear_guid) +TEST_F(CopyFileRange, clear_sgid) { const char FULLPATH_IN[] = "mountpoint/in.txt"; const char RELPATH_IN[] = "in.txt"; @@ -877,6 +878,92 @@ TEST_F(Open, ok) leak(fd); } +/* A write by a non-owner should clear a file's SGID bit */ +TEST_F(PosixFallocate, clear_sgid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 02777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, 0, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* A write by a non-owner should clear a file's SUID bit */ +TEST_F(PosixFallocate, clear_suid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 04777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, 0, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* + * posix_fallcoate() of a file without writable permissions should succeed as + * long as the file descriptor is writable. This is important when combined + * with O_CREAT + */ +TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, ino); + expect_fallocate(ino, off, len, 0, 0); + + fd = open(FULLPATH, O_CREAT | O_RDWR, 0); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + leak(fd); +} + TEST_F(Rename, eacces_on_srcdir) { const char FULLDST[] = "mountpoint/d/dst"; diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc new file mode 100644 index 000000000000..386a3ac746ea --- /dev/null +++ b/tests/sys/fs/fusefs/fallocate.cc @@ -0,0 +1,297 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2021 Alan Somers + * + * 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. + * + * $FreeBSD$ + */ + +extern "C" { +#include +#include +#include +#include + +#include +#include +#include + +#include "mntopts.h" // for build_iovec +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Fallocate: public FuseTest{}; + +class PosixFallocate: public Fallocate { +public: +static sig_atomic_t s_sigxfsz; + +void SetUp() { + s_sigxfsz = 0; + FuseTest::SetUp(); +} + +void TearDown() { + struct sigaction sa; + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGXFSZ, &sa, NULL); + + Fallocate::TearDown(); +} + +}; + +sig_atomic_t PosixFallocate::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { + PosixFallocate::s_sigxfsz = 1; +} + +class PosixFallocate_7_18: public PosixFallocate { +public: +virtual void SetUp() { + m_kernel_minor_version = 18; + PosixFallocate::SetUp(); +} +}; + + +/* + * If the server returns ENOSYS, it indicates that the server does not support + * FUSE_FALLOCATE. This and future calls should return EINVAL. + */ +TEST_F(PosixFallocate, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, ENOSYS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + /* Subsequent calls shouldn't query the daemon*/ + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +/* + * EOPNOTSUPP means either "the file system does not support fallocate" or "the + * file system does not support fallocate with the supplied mode". fusefs + * should conservatively assume the latter, and not issue any more fallocate + * operations with the same mode. + */ +TEST_F(PosixFallocate, eopnotsupp) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, EOPNOTSUPP); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + /* Subsequent calls shouldn't query the daemon*/ + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +/* EIO is not a permanent error, and may be retried */ +TEST_F(PosixFallocate, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, EIO); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EIO, posix_fallocate(fd, offset, length)); + + expect_fallocate(ino, offset, length, 0, 0); + + EXPECT_EQ(0, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +TEST_F(PosixFallocate, erofs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct statfs statbuf; + struct iovec *iov = NULL; + int iovlen = 0; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + int newflags; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + 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); + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Remount read-only */ + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY; + 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); + + EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +TEST_F(PosixFallocate, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb0, sb1; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + 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.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, offset, length)); + /* + * Despite the originally cached file size of zero, stat should now + * return either the new size or requery the daemon. + */ + EXPECT_EQ(0, stat(FULLPATH, &sb1)); + EXPECT_EQ(length, (uint64_t)sb1.st_size); + + /* mtime and ctime should be updated */ + EXPECT_EQ(sb0.st_atime, sb1.st_atime); + EXPECT_NE(sb0.st_mtime, sb1.st_mtime); + EXPECT_NE(sb0.st_ctime, sb1.st_ctime); + + leak(fd); +} + +/* fusefs should respect RLIMIT_FSIZE */ +TEST_F(PosixFallocate, rlimit_fsize) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct rlimit rl; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1'000'000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + + rl.rlim_cur = length / 2; + rl.rlim_max = 10 * length; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length)); + EXPECT_EQ(1, s_sigxfsz); + + leak(fd); +} + +/* With older servers, no FUSE_FALLOCATE should be attempted */ +TEST_F(PosixFallocate_7_18, einval) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc index a203a02e922b..9826296c80c3 100644 --- a/tests/sys/fs/fusefs/last_local_modify.cc +++ b/tests/sys/fs/fusefs/last_local_modify.cc @@ -62,9 +62,10 @@ using namespace testing; */ enum Mutator { + VOP_ALLOCATE, + VOP_COPY_FILE_RANGE, VOP_SETATTR, VOP_WRITE, - VOP_COPY_FILE_RANGE }; /* @@ -73,22 +74,26 @@ enum Mutator { * --gtest_list_tests */ enum Mutator writer_from_str(const char* s) { - if (0 == strcmp("VOP_SETATTR", s)) + if (0 == strcmp("VOP_ALLOCATE", s)) + return VOP_ALLOCATE; + else if (0 == strcmp("VOP_COPY_FILE_RANGE", s)) + return VOP_COPY_FILE_RANGE; + else if (0 == strcmp("VOP_SETATTR", s)) return VOP_SETATTR; - else if (0 == strcmp("VOP_WRITE", s)) - return VOP_WRITE; else - return VOP_COPY_FILE_RANGE; + return VOP_WRITE; } uint32_t fuse_op_from_mutator(enum Mutator mutator) { switch(mutator) { + case VOP_ALLOCATE: + return(FUSE_FALLOCATE); + case VOP_COPY_FILE_RANGE: + return(FUSE_COPY_FILE_RANGE); case VOP_SETATTR: return(FUSE_SETATTR); case VOP_WRITE: return(FUSE_WRITE); - case VOP_COPY_FILE_RANGE: - return(FUSE_COPY_FILE_RANGE); } } @@ -101,6 +106,25 @@ virtual void SetUp() { } }; +static void* allocate_th(void* arg) { + int fd; + ssize_t r; + sem_t *sem = (sem_t*) arg; + + if (sem) + sem_wait(sem); + + fd = open("mountpoint/some_file.txt", O_RDWR); + if (fd < 0) + return (void*)(intptr_t)errno; + + r = posix_fallocate(fd, 0, 15); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + static void* copy_file_range_th(void* arg) { ssize_t r; int fd; @@ -261,6 +285,14 @@ TEST_P(LastLocalModify, lookup) /* Then, respond to the mutator request */ out1->header.unique = mutator_unique; switch(mutator) { + case VOP_ALLOCATE: + out1->header.error = 0; + out1->header.len = sizeof(out1->header); + break; + case VOP_COPY_FILE_RANGE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = mutator_size; + break; case VOP_SETATTR: SET_OUT_HEADER_LEN(*out1, attr); out1->body.attr.attr.ino = ino; @@ -272,16 +304,20 @@ TEST_P(LastLocalModify, lookup) SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = mutator_size; break; - case VOP_COPY_FILE_RANGE: - SET_OUT_HEADER_LEN(*out1, write); - out1->body.write.size = mutator_size; - break; } out.push_back(std::move(out1)); })); /* Start the mutator thread */ switch(mutator) { + case VOP_ALLOCATE: + ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, + NULL)) << strerror(errno); + break; + case VOP_COPY_FILE_RANGE: + ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, + NULL)) << strerror(errno); + break; case VOP_SETATTR: ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL)) << strerror(errno); @@ -290,10 +326,6 @@ TEST_P(LastLocalModify, lookup) ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL)) << strerror(errno); break; - case VOP_COPY_FILE_RANGE: - ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, - NULL)) << strerror(errno); - break; } @@ -415,6 +447,14 @@ TEST_P(LastLocalModify, vfs_vget) /* Then, respond to the mutator request */ out1->header.unique = in.header.unique; switch(mutator) { + case VOP_ALLOCATE: + out1->header.error = 0; + out1->header.len = sizeof(out1->header); + break; + case VOP_COPY_FILE_RANGE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = in.body.copy_file_range.len; + break; case VOP_SETATTR: SET_OUT_HEADER_LEN(*out1, attr); out1->body.attr.attr.ino = ino; @@ -426,10 +466,6 @@ TEST_P(LastLocalModify, vfs_vget) SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = in.body.write.size; break; - case VOP_COPY_FILE_RANGE: - SET_OUT_HEADER_LEN(*out1, write); - out1->body.write.size = in.body.copy_file_range.len; - break; } out.push_back(std::move(out1)); })); @@ -439,6 +475,14 @@ TEST_P(LastLocalModify, vfs_vget) /* Start the mutator thread */ switch(mutator) { + case VOP_ALLOCATE: + ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, + (void*)&sem)) << strerror(errno); + break; + case VOP_COPY_FILE_RANGE: + ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, + (void*)&sem)) << strerror(errno); + break; case VOP_SETATTR: ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, (void*)&sem)) << strerror(errno); @@ -447,10 +491,6 @@ TEST_P(LastLocalModify, vfs_vget) ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem)) << strerror(errno); break; - case VOP_COPY_FILE_RANGE: - ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, - (void*)&sem)) << strerror(errno); - break; } /* Lookup again, which will race with setattr */ @@ -465,5 +505,10 @@ TEST_P(LastLocalModify, vfs_vget) INSTANTIATE_TEST_CASE_P(LLM, LastLocalModify, - Values("VOP_SETATTR", "VOP_WRITE", "VOP_COPY_FILE_RANGE") + Values( + "VOP_ALLOCATE", + "VOP_COPY_FILE_RANGE", + "VOP_SETATTR", + "VOP_WRITE" + ) ); diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index a943d3d6972c..231f46b18aba 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -207,6 +207,14 @@ void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen) printf(" flags=%#x name=%s", in.body.open.flags, name); break; + case FUSE_FALLOCATE: + printf(" fh=%#" PRIx64 " offset=%" PRIu64 + " length=%" PRIx64 " mode=%#x", + in.body.fallocate.fh, + in.body.fallocate.offset, + in.body.fallocate.length, + in.body.fallocate.mode); + break; case FUSE_FLUSH: printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64, in.body.flush.fh, @@ -684,6 +692,10 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { EXPECT_EQ(inlen, fih + sizeof(in.body.interrupt)); EXPECT_EQ((size_t)buflen, inlen); break; + case FUSE_FALLOCATE: + EXPECT_EQ(inlen, fih + sizeof(in.body.fallocate)); + EXPECT_EQ((size_t)buflen, inlen); + break; case FUSE_BMAP: EXPECT_EQ(inlen, fih + sizeof(in.body.bmap)); EXPECT_EQ((size_t)buflen, inlen); @@ -699,7 +711,6 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { break; case FUSE_NOTIFY_REPLY: case FUSE_BATCH_FORGET: - case FUSE_FALLOCATE: case FUSE_IOCTL: case FUSE_POLL: case FUSE_READDIRPLUS: diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index dd6d259ca5af..e35f2efb8dae 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -159,6 +159,7 @@ union fuse_payloads_in { ]; fuse_copy_file_range_in copy_file_range; fuse_create_in create; + fuse_fallocate_in fallocate; fuse_flush_in flush; fuse_fsync_in fsync; fuse_fsync_in fsyncdir; diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index fb2109e1e9c4..508c3af2828f 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -225,6 +225,23 @@ FuseTest::expect_destroy(int error) }))); } +void +FuseTest::expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, + uint32_t mode, int error, int times) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FALLOCATE && + in.header.nodeid == ino && + in.body.fallocate.offset == offset && + in.body.fallocate.length == length && + in.body.fallocate.mode == mode); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnErrno(error))); +} + void FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r) { diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 610d2126fa52..1cd7bad97998 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -114,6 +114,14 @@ class FuseTest : public ::testing::Test { /* Expect FUSE_DESTROY and shutdown the daemon */ void expect_destroy(int error); + /* + * Create an expectation that FUSE_FALLOCATE will be called with the + * given inode, offset, length, and mode, exactly times times and + * returning error + */ + void expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, + uint32_t mode, int error, int times=1); + /* * Create an expectation that FUSE_FLUSH will be called times times for * the given inode