diff options
| -rw-r--r-- | sys/fs/fuse/fuse_io.c | 49 | ||||
| -rw-r--r-- | sys/fs/fuse/fuse_node.h | 2 | ||||
| -rw-r--r-- | sys/fs/fuse/fuse_vnops.c | 27 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/Makefile | 2 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/ext2-misc.sh | 55 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/misc.cc | 304 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/mockfs.cc | 2 | ||||
| -rw-r--r-- | tests/sys/fs/fusefs/utils.cc | 3 |
8 files changed, 388 insertions, 56 deletions
diff --git a/sys/fs/fuse/fuse_io.c b/sys/fs/fuse/fuse_io.c index 9f864e48effc..c4fbb263557f 100644 --- a/sys/fs/fuse/fuse_io.c +++ b/sys/fs/fuse/fuse_io.c @@ -928,58 +928,13 @@ fuse_io_flushbuf(struct vnode *vp, int waitfor, struct thread *td) return (vn_fsync_buf(vp, waitfor)); } -/* - * Flush and invalidate all dirty buffers. If another process is already - * doing the flush, just wait for completion. - */ +/* Flush and invalidate all dirty buffers. */ int fuse_io_invalbuf(struct vnode *vp, struct thread *td) { - struct fuse_vnode_data *fvdat = VTOFUD(vp); - int error = 0; - if (VN_IS_DOOMED(vp)) return 0; - ASSERT_VOP_ELOCKED(vp, "fuse_io_invalbuf"); - - while (fvdat->flag & FN_FLUSHINPROG) { - struct proc *p = td->td_proc; - - if (vp->v_mount->mnt_kern_flag & MNTK_UNMOUNTF) - return EIO; - fvdat->flag |= FN_FLUSHWANT; - tsleep(&fvdat->flag, PRIBIO, "fusevinv", 2 * hz); - error = 0; - if (p != NULL) { - PROC_LOCK(p); - if (SIGNOTEMPTY(p->p_siglist) || - SIGNOTEMPTY(td->td_siglist)) - error = EINTR; - PROC_UNLOCK(p); - } - if (error == EINTR) - return EINTR; - } - fvdat->flag |= FN_FLUSHINPROG; - vnode_pager_clean_sync(vp); - error = vinvalbuf(vp, V_SAVE, PCATCH, 0); - while (error) { - if (error == ERESTART || error == EINTR) { - fvdat->flag &= ~FN_FLUSHINPROG; - if (fvdat->flag & FN_FLUSHWANT) { - fvdat->flag &= ~FN_FLUSHWANT; - wakeup(&fvdat->flag); - } - return EINTR; - } - error = vinvalbuf(vp, V_SAVE, PCATCH, 0); - } - fvdat->flag &= ~FN_FLUSHINPROG; - if (fvdat->flag & FN_FLUSHWANT) { - fvdat->flag &= ~FN_FLUSHWANT; - wakeup(&fvdat->flag); - } - return (error); + return (vinvalbuf(vp, V_SAVE, PCATCH, 0)); } diff --git a/sys/fs/fuse/fuse_node.h b/sys/fs/fuse/fuse_node.h index b6e388d01702..191a5859b031 100644 --- a/sys/fs/fuse/fuse_node.h +++ b/sys/fs/fuse/fuse_node.h @@ -71,8 +71,6 @@ #include "fuse_file.h" #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 diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c index f5c832cbbd13..1a4fc60760d8 100644 --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -879,8 +879,10 @@ fuse_vnop_close(struct vop_close_args *ap) int fflag = ap->a_fflag; struct thread *td; struct fuse_vnode_data *fvdat = VTOFUD(vp); + struct timespec va_atime; pid_t pid; int err = 0; + bool atime_change, size_change; /* NB: a_td will be NULL from some async kernel contexts */ td = ap->a_td ? ap->a_td : curthread; @@ -897,8 +899,14 @@ fuse_vnop_close(struct vop_close_args *ap) cred = td->td_ucred; err = fuse_flush(vp, cred, pid, fflag); - ASSERT_CACHED_ATTRS_LOCKED(vp); /* For fvdat->flag */ - if (err == 0 && (fvdat->flag & FN_ATIMECHANGE) && !vfs_isrdonly(mp)) { + + CACHED_ATTR_LOCK(vp); + atime_change = fvdat->flag & FN_ATIMECHANGE; + size_change = fvdat->flag & FN_SIZECHANGE; + va_atime = fvdat->cached_attrs.va_atime; + CACHED_ATTR_UNLOCK(vp); + + if (err == 0 && atime_change && !vfs_isrdonly(mp)) { struct vattr vap; struct fuse_data *data; int dataflags; @@ -915,19 +923,28 @@ fuse_vnop_close(struct vop_close_args *ap) } if (access_e == 0) { VATTR_NULL(&vap); - ASSERT_CACHED_ATTRS_LOCKED(vp); - vap.va_atime = fvdat->cached_attrs.va_atime; + vap.va_atime = va_atime; /* * Ignore errors setting when setting atime. That * should not cause close(2) to fail. */ + CACHED_ATTR_LOCK(vp); fuse_internal_setattr(vp, &vap, td, NULL); + CACHED_ATTR_UNLOCK(vp); } } /* TODO: close the file handle, if we're sure it's no longer used */ - if ((fvdat->flag & FN_SIZECHANGE) != 0) { + if (size_change != 0) { + /* + * NB: this may panic if MNTK_SHARED_WRITES is ever enabled. + * For now it cannot, because it is illegal to use fexecve to + * execute a file descriptor open for writing, there's no way + * to dirty a file's size without writing to it, and we don't + * set MNTK_SHARED_WRITES. + */ fuse_vnode_savesize(vp, cred, pid); } + return err; } diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index 539a54cf5303..4ba477450c0a 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -5,6 +5,7 @@ PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/fs/fusefs ATF_TESTS_SH+= ctl +ATF_TESTS_SH+= ext2-misc # 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 @@ -35,6 +36,7 @@ GTESTS+= link GTESTS+= locks GTESTS+= lookup GTESTS+= lseek +GTESTS+= misc GTESTS+= mkdir GTESTS+= mknod GTESTS+= mount diff --git a/tests/sys/fs/fusefs/ext2-misc.sh b/tests/sys/fs/fusefs/ext2-misc.sh new file mode 100644 index 000000000000..b2d713a1864a --- /dev/null +++ b/tests/sys/fs/fusefs/ext2-misc.sh @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 ConnectWise +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=295957 +# +# Almost any fuse file system would work, but this tests uses fusefs-ext2 +# because it's simple and its download is very small. +atf_test_case execute cleanup +execute_head() +{ + atf_set "descr" "Execute a file mounted on a fusefs file system" + atf_set "require.user" "root" + atf_set "require.progs" "fuse-ext2 mkfs.ext2" + atf_set "require.kmods" "fusefs" +} +execute_body() +{ + atf_check mkdir mnt + atf_check truncate -s 64m ext2.img + atf_check -o ignore -e ignore mkfs.ext2 ext2.img + atf_check fuse-ext2 -o rw+ ext2.img mnt + atf_check cp /usr/bin/true mnt + atf_check su -m nobody -c mnt/true +} +execute_cleanup() +{ + umount $PWD/mnt || true +} + +atf_init_test_cases() +{ + atf_add_test_case execute +} diff --git a/tests/sys/fs/fusefs/misc.cc b/tests/sys/fs/fusefs/misc.cc new file mode 100644 index 000000000000..d95356262c1f --- /dev/null +++ b/tests/sys/fs/fusefs/misc.cc @@ -0,0 +1,304 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 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. + */ + +/* Miscellaneous tests that don't relate to any particular fuse operation */ + +extern "C" { +#include <sys/wait.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + + +class Execv: public FuseTest { +public: +virtual void SetUp() { + /* Enable FUSE_ASYNC_READ to allow shared vnode locks */ + m_init_flags = FUSE_ASYNC_READ; + FuseTest::SetUp(); +} +}; +class Fexecv: public Execv {}; + +class FexecvDefaultPermissions: public Fexecv { +virtual void SetUp() { + m_default_permissions = true; + Fexecv::SetUp(); +} +}; + +/* + * Execute a file mounted on a fusefs file system. The server should get the + * FUSE_RELEASE request when sys_fexecve closes the file. + * + * Crucially, execve ignores the file system's MNTK_EXTENDED_SHARED flag. + * + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=295957 + */ +TEST_F(Execv, close) +{ + const static char FULLPATH[] = "mountpoint/true"; + const static char RELPATH[] = "true"; + const static size_t BUFSIZE = 16384; + FILE *true_file; + uint64_t ino = 42; + size_t true_len; + int status; + char *buf; + + buf = new char[BUFSIZE]; + true_file = fopen("/usr/bin/true", "r"); + ASSERT_TRUE(true_file) << strerror(errno); + true_len = fread(buf, 1, BUFSIZE, true_file); + ASSERT_LT(true_len, BUFSIZE) << "Must increase BUFSIZE"; + fclose(true_file); + + fork(false, &status, [&] { + expect_lookup(RELPATH, ino, S_IFREG | 0755, true_len, 1, + UINT64_MAX); + expect_open(ino, 0, 1); + expect_read(ino, 0, true_len, true_len, buf); + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .WillRepeatedly(Invoke(ReturnErrno(0))); + + }, [&] { + char *const argv[] = {__DECONST(char *, "true"), NULL}; + char *const env[] = {NULL}; + + execve(FULLPATH, argv, env); + fprintf(stderr, "execv: %s\n", strerror(errno)); + return 1; + }); + ASSERT_EQ(0, WEXITSTATUS(status)); + + delete[] buf; +} + +TEST_F(Fexecv, close) +{ + const static char FULLPATH[] = "mountpoint/true"; + const static char RELPATH[] = "true"; + const static size_t BUFSIZE = 16384; + FILE *true_file; + uint64_t ino = 42; + size_t true_len; + int status; + char *buf; + + buf = new char[BUFSIZE]; + true_file = fopen("/usr/bin/true", "r"); + ASSERT_TRUE(true_file) << strerror(errno); + true_len = fread(buf, 1, BUFSIZE, true_file); + ASSERT_LT(true_len, BUFSIZE) << "Must increase BUFSIZE"; + fclose(true_file); + + fork(false, &status, [&] { + expect_lookup(RELPATH, ino, S_IFREG | 0755, true_len, 1, + UINT64_MAX); + expect_open(ino, 0, 2); + expect_read(ino, 0, true_len, true_len, buf); + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnErrno(0))); + + }, [&] { + char *const argv[] = {__DECONST(char *, "true"), NULL}; + char *const env[] = {NULL}; + int fd; + + fd = open(FULLPATH, O_EXEC); + if (fd < 0) { + fprintf(stderr, "open: %s\n", strerror(errno)); + return 1; + } + fexecve(fd, argv, env); + fprintf(stderr, "execv: %s\n", strerror(errno)); + return 1; + }); + ASSERT_EQ(0, WEXITSTATUS(status)); + + delete[] buf; +} + +/* + * Execute a file stored on a fusefs file system that does not implement + * FUSE_OPEN + */ +TEST_F(Fexecv, close_noopen) +{ + const static char FULLPATH[] = "mountpoint/true"; + const static char RELPATH[] = "true"; + const static size_t BUFSIZE = 16384; + FILE *true_file; + uint64_t ino = 42; + size_t true_len; + int status; + char *buf; + + buf = new char[BUFSIZE]; + true_file = fopen("/usr/bin/true", "r"); + ASSERT_TRUE(true_file) << strerror(errno); + true_len = fread(buf, 1, BUFSIZE, true_file); + ASSERT_LT(true_len, BUFSIZE) << "Must increase BUFSIZE"; + fclose(true_file); + + fork(false, &status, [&] { + expect_lookup(RELPATH, ino, S_IFREG | 0755, true_len, 1, + UINT64_MAX); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .WillOnce(Invoke(ReturnErrno(ENOSYS))); + expect_read(ino, 0, true_len, true_len, buf, -1, 0); + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + }, [&] { + char *const argv[] = {__DECONST(char *, "true"), NULL}; + char *const env[] = {NULL}; + int fd; + + fd = open(FULLPATH, O_EXEC); + if (fd < 0) { + fprintf(stderr, "open: %s\n", strerror(errno)); + return 1; + } + fexecve(fd, argv, env); + fprintf(stderr, "execv: %s\n", strerror(errno)); + return 1; + }); + ASSERT_EQ(0, WEXITSTATUS(status)); + + delete[] buf; +} + +/* + * When execute a file with a dirty atime, fusefs must send FUSE_SETATTR to the + * daemon during close. + */ +TEST_F(FexecvDefaultPermissions, atime) +{ + const static char FULLPATH[] = "mountpoint/true"; + const static char RELPATH[] = "true"; + const static size_t BUFSIZE = 16384; + FILE *true_file; + uint64_t ino = 42; + size_t true_len; + int status; + char *buf; + + buf = new char[BUFSIZE]; + true_file = fopen("/usr/bin/true", "r"); + ASSERT_TRUE(true_file) << strerror(errno); + true_len = fread(buf, 1, BUFSIZE, true_file); + ASSERT_LT(true_len, BUFSIZE) << "Must increase BUFSIZE"; + fclose(true_file); + + fork(false, &status, [&] { + expect_lookup(RELPATH, ino, S_IFREG | 0777, true_len, 1, + UINT64_MAX); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).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 | 0777; + out.body.attr.attr.size = 0; + out.body.attr.attr_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .WillOnce(Invoke(ReturnErrno(ENOSYS))); + expect_read(ino, 0, true_len, true_len, buf, -1, 0); + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == FATTR_ATIME); + }, 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 | 0777; + }))); + }, [&] { + char *const argv[] = {__DECONST(char *, "true"), NULL}; + char *const env[] = {NULL}; + char buf[8]; + int fd; + + /* Note that fexecve doesn't actually require O_EXEC */ + fd = open(FULLPATH, O_RDONLY); + if (fd < 0) { + fprintf(stderr, "open: %s\n", strerror(errno)); + return 1; + } + /* Read a few bytes, just to dirty the file's atime */ + if (read(fd, buf, sizeof(buf)) < 0) { + fprintf(stderr, "read: %s\n", strerror(errno)); + return 1; + } + fexecve(fd, argv, env); + fprintf(stderr, "execv: %s\n", strerror(errno)); + return 1; + }); + ASSERT_EQ(0, WEXITSTATUS(status)); + + delete[] buf; +} diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index bc49542ce1a4..fe4979762d92 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -300,7 +300,7 @@ void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen) in.body.read.offset, in.body.read.size); if (verbosity > 1) - printf(" flags=%#x", in.body.read.flags); + printf(" fh=%#" PRIx64 " flags=%#x", in.body.read.fh, in.body.read.flags); break; case FUSE_READDIR: printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u", diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 93b850a7b7e3..3471032b99a1 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -386,7 +386,8 @@ void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize, in.body.read.size == isize && (flags == -1 ? (in.body.read.flags == O_RDONLY || - in.body.read.flags == O_RDWR) + in.body.read.flags == O_RDWR || + in.body.read.flags == O_EXEC) : in.body.read.flags == (uint32_t)flags)); }, Eq(true)), _) |
