From 17ba6f428683b661178b50a9d59f8b9e0dd2138a Mon Sep 17 00:00:00 2001 From: CismonX Date: Sat, 5 Jul 2025 20:46:27 +0800 Subject: [PATCH] fusefs: support FUSE_IOCTL MFC After: 1 week Signed-off-by: CismonX Reviewed by: imp Pull Request: https://github.com/freebsd/freebsd-src/pull/1470 --- sys/fs/fuse/fuse_internal.c | 3 +- sys/fs/fuse/fuse_ipc.c | 4 + sys/fs/fuse/fuse_vnops.c | 95 ++++++++++++++- tests/sys/fs/fusefs/Makefile | 1 + tests/sys/fs/fusefs/ioctl.cc | 213 ++++++++++++++++++++++++++++++++++ tests/sys/fs/fusefs/mockfs.cc | 13 ++- tests/sys/fs/fusefs/mockfs.hh | 2 + 7 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 tests/sys/fs/fusefs/ioctl.cc diff --git a/sys/fs/fuse/fuse_internal.c b/sys/fs/fuse/fuse_internal.c index eba0a8a79ff3..a3590060f44a 100644 --- a/sys/fs/fuse/fuse_internal.c +++ b/sys/fs/fuse/fuse_internal.c @@ -1103,7 +1103,6 @@ fuse_internal_send_init(struct fuse_data *data, struct thread *td) * FUSE_SPLICE_WRITE, FUSE_SPLICE_MOVE, FUSE_SPLICE_READ: FreeBSD * doesn't have splice(2). * FUSE_FLOCK_LOCKS: not yet implemented - * FUSE_HAS_IOCTL_DIR: not yet implemented * FUSE_AUTO_INVAL_DATA: not yet implemented * FUSE_DO_READDIRPLUS: not yet implemented * FUSE_READDIRPLUS_AUTO: not yet implemented @@ -1116,7 +1115,7 @@ fuse_internal_send_init(struct fuse_data *data, struct thread *td) * FUSE_MAX_PAGES: not yet implemented */ fiii->flags = FUSE_ASYNC_READ | FUSE_POSIX_LOCKS | FUSE_EXPORT_SUPPORT - | FUSE_BIG_WRITES | FUSE_WRITEBACK_CACHE + | FUSE_BIG_WRITES | FUSE_HAS_IOCTL_DIR | FUSE_WRITEBACK_CACHE | FUSE_NO_OPEN_SUPPORT | FUSE_NO_OPENDIR_SUPPORT | FUSE_SETXATTR_EXT; diff --git a/sys/fs/fuse/fuse_ipc.c b/sys/fs/fuse/fuse_ipc.c index bc36f0070d7d..f3d92d861352 100644 --- a/sys/fs/fuse/fuse_ipc.c +++ b/sys/fs/fuse/fuse_ipc.c @@ -835,6 +835,10 @@ fuse_body_audit(struct fuse_ticket *ftick, size_t blen) err = (blen == 0) ? 0 : EINVAL; break; + case FUSE_IOCTL: + err = (blen >= sizeof(struct fuse_ioctl_out)) ? 0 : EINVAL; + break; + case FUSE_FALLOCATE: err = (blen == 0) ? 0 : EINVAL; break; diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c index 0e049b1f07a9..22d5893d4fbc 100644 --- a/sys/fs/fuse/fuse_vnops.c +++ b/sys/fs/fuse/fuse_vnops.c @@ -91,6 +91,7 @@ #include #define EXTERR_CATEGORY EXTERR_CAT_FUSE_VNOPS #include +#include #include #include @@ -374,6 +375,84 @@ fuse_inval_buf_range(struct vnode *vp, off_t filesize, off_t start, off_t end) return (0); } +/* Send FUSE_IOCTL for this node */ +static int +fuse_vnop_do_ioctl(struct vnode *vp, u_long cmd, void *arg, int fflag, + struct ucred *cred, struct thread *td) +{ + struct fuse_dispatcher fdi; + struct fuse_ioctl_in *fii; + struct fuse_ioctl_out *fio; + struct fuse_filehandle *fufh; + uint32_t flags = 0; + uint32_t insize = 0; + uint32_t outsize = 0; + int err; + + err = fuse_filehandle_getrw(vp, fflag, &fufh, cred, td->td_proc->p_pid); + if (err != 0) + return (err); + + if (vnode_isdir(vp)) { + struct fuse_data *data = fuse_get_mpdata(vnode_mount(vp)); + + if (!fuse_libabi_geq(data, 7, 18)) + return (ENOTTY); + flags |= FUSE_IOCTL_DIR; + } +#ifdef __LP64__ +#ifdef COMPAT_FREEBSD32 + if (SV_PROC_FLAG(td->td_proc, SV_ILP32)) + flags |= FUSE_IOCTL_32BIT; +#endif +#else /* !defined(__LP64__) */ + flags |= FUSE_IOCTL_32BIT; +#endif + + if ((cmd & IOC_OUT) != 0) + outsize = IOCPARM_LEN(cmd); + /* _IOWINT() sets IOC_VOID */ + if ((cmd & (IOC_VOID | IOC_IN)) != 0) + insize = IOCPARM_LEN(cmd); + + fdisp_init(&fdi, sizeof(*fii) + insize); + fdisp_make_vp(&fdi, FUSE_IOCTL, vp, td, cred); + fii = fdi.indata; + fii->fh = fufh->fh_id; + fii->flags = flags; + fii->cmd = cmd; + fii->arg = (uintptr_t)arg; + fii->in_size = insize; + fii->out_size = outsize; + if (insize > 0) + memcpy((char *)fii + sizeof(*fii), arg, insize); + + err = fdisp_wait_answ(&fdi); + if (err != 0) { + if (err == ENOSYS) + err = ENOTTY; + goto out; + } + + fio = fdi.answ; + if (fdi.iosize > sizeof(*fio)) { + size_t realoutsize = fdi.iosize - sizeof(*fio); + + if (realoutsize > outsize) { + err = EIO; + goto out; + } + memcpy(arg, (char *)fio + sizeof(*fio), realoutsize); + } + if (fio->result > 0) + td->td_retval[0] = fio->result; + else + err = -fio->result; + +out: + fdisp_destroy(&fdi); + return (err); +} /* Send FUSE_LSEEK for this node */ static int @@ -1294,25 +1373,29 @@ fuse_vnop_ioctl(struct vop_ioctl_args *ap) struct vnode *vp = ap->a_vp; struct mount *mp = vnode_mount(vp); struct ucred *cred = ap->a_cred; - off_t *offp; - pid_t pid = ap->a_td->td_proc->p_pid; + struct thread *td = ap->a_td; int err; + if (fuse_isdeadfs(vp)) { + return (ENXIO); + } + switch (ap->a_command) { case FIOSEEKDATA: case FIOSEEKHOLE: /* Call FUSE_LSEEK, if we can, or fall back to vop_stdioctl */ if (fsess_maybe_impl(mp, FUSE_LSEEK)) { + off_t *offp = ap->a_data; + pid_t pid = td->td_proc->p_pid; int whence; - offp = ap->a_data; if (ap->a_command == FIOSEEKDATA) whence = SEEK_DATA; else whence = SEEK_HOLE; vn_lock(vp, LK_SHARED | LK_RETRY); - err = fuse_vnop_do_lseek(vp, ap->a_td, cred, pid, offp, + err = fuse_vnop_do_lseek(vp, td, cred, pid, offp, whence); VOP_UNLOCK(vp); } @@ -1320,8 +1403,8 @@ fuse_vnop_ioctl(struct vop_ioctl_args *ap) err = vop_stdioctl(ap); break; default: - /* TODO: implement FUSE_IOCTL */ - err = ENOTTY; + err = fuse_vnop_do_ioctl(vp, ap->a_command, ap->a_data, + ap->a_fflag, cred, td); break; } return (err); diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index 6366676b6fc5..8e5fe112a1e4 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -29,6 +29,7 @@ GTESTS+= fsyncdir GTESTS+= getattr GTESTS+= interrupt GTESTS+= io +GTESTS+= ioctl GTESTS+= last_local_modify GTESTS+= link GTESTS+= locks diff --git a/tests/sys/fs/fusefs/ioctl.cc b/tests/sys/fs/fusefs/ioctl.cc new file mode 100644 index 000000000000..da048efc51c6 --- /dev/null +++ b/tests/sys/fs/fusefs/ioctl.cc @@ -0,0 +1,213 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 CismonX + * + * 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 +#include +#include +#include +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +using IoctlTestProcT = std::function; + +static const char INPUT_DATA[] = "input_data"; +static const char OUTPUT_DATA[] = "output_data"; + +class Ioctl: public FuseTest { +public: +void expect_ioctl(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_IOCTL && + in.header.nodeid == ino); + }, Eq(true)), _) + ).WillOnce(Invoke(r)).RetiresOnSaturation(); +} + +void expect_ioctl_rw(uint64_t ino) +{ + /* + * _IOR(): Compare the input data with INPUT_DATA. + * _IOW(): Copy out OUTPUT_DATA. + * _IOWR(): Combination of above. + * _IOWINT(): Return the integer argument value. + */ + expect_ioctl(ino, ReturnImmediate([](auto in, auto& out) { + uint8_t *in_buf = in.body.bytes + sizeof(in.body.ioctl); + uint8_t *out_buf = out.body.bytes + sizeof(out.body.ioctl); + uint32_t cmd = in.body.ioctl.cmd; + uint32_t arg_len = IOCPARM_LEN(cmd); + int result = 0; + + out.header.error = 0; + SET_OUT_HEADER_LEN(out, ioctl); + if ((cmd & IOC_VOID) != 0 && arg_len > 0) { + memcpy(&result, in_buf, sizeof(int)); + goto out; + } + if ((cmd & IOC_IN) != 0) { + if (0 != strncmp(INPUT_DATA, (char *)in_buf, arg_len)) { + result = -EINVAL; + goto out; + } + } + if ((cmd & IOC_OUT) != 0) { + memcpy(out_buf, OUTPUT_DATA, sizeof(OUTPUT_DATA)); + out.header.len += sizeof(OUTPUT_DATA); + } + +out: + out.body.ioctl.result = result; + })); +} +}; + +/** + * If the server does not implement FUSE_IOCTL handler (returns ENOSYS), + * the kernel should return ENOTTY to the user instead. + */ +TEST_F(Ioctl, enosys) +{ + unsigned long req = _IO(0xff, 0); + int fd; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl(FUSE_ROOT_ID, ReturnErrno(ENOSYS)); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + EXPECT_EQ(-1, ioctl(fd, req)); + EXPECT_EQ(ENOTTY, errno); + + leak(fd); +} + +/* + * For _IOR() and _IOWR(), The server is allowed to write fewer bytes + * than IOCPARM_LEN(req). + */ +TEST_F(Ioctl, ior) +{ + char buf[sizeof(OUTPUT_DATA) + 1] = { 0 }; + unsigned long req = _IOR(0xff, 1, buf); + int fd; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl_rw(FUSE_ROOT_ID); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + EXPECT_EQ(0, ioctl(fd, req, buf)) << strerror(errno); + EXPECT_EQ(0, memcmp(buf, OUTPUT_DATA, sizeof(OUTPUT_DATA))); + + leak(fd); +} + +/* + * For _IOR() and _IOWR(), if the server attempts to write more bytes + * than IOCPARM_LEN(req), the kernel should fail the syscall with EIO. + */ +TEST_F(Ioctl, ior_overflow) +{ + char buf[sizeof(OUTPUT_DATA) - 1] = { 0 }; + unsigned long req = _IOR(0xff, 2, buf); + int fd; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl_rw(FUSE_ROOT_ID); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + EXPECT_EQ(-1, ioctl(fd, req, buf)); + EXPECT_EQ(EIO, errno); + + leak(fd); +} + +TEST_F(Ioctl, iow) +{ + unsigned long req = _IOW(0xff, 3, INPUT_DATA); + int fd; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl_rw(FUSE_ROOT_ID); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + EXPECT_EQ(0, ioctl(fd, req, INPUT_DATA)) << strerror(errno); + + leak(fd); +} + +TEST_F(Ioctl, iowr) +{ + char buf[std::max(sizeof(INPUT_DATA), sizeof(OUTPUT_DATA))] = { 0 }; + unsigned long req = _IOWR(0xff, 4, buf); + int fd; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl_rw(FUSE_ROOT_ID); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + memcpy(buf, INPUT_DATA, sizeof(INPUT_DATA)); + EXPECT_EQ(0, ioctl(fd, req, buf)) << strerror(errno); + EXPECT_EQ(0, memcmp(buf, OUTPUT_DATA, sizeof(OUTPUT_DATA))); + + leak(fd); +} + +TEST_F(Ioctl, iowint) +{ + unsigned long req = _IOWINT(0xff, 5); + int arg = 1337; + int fd, r; + + expect_opendir(FUSE_ROOT_ID); + expect_ioctl_rw(FUSE_ROOT_ID); + + fd = open("mountpoint", O_RDONLY | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + /* The server is allowed to return a positive value on success */ + r = ioctl(fd, req, arg); + EXPECT_LE(0, r) << strerror(errno); + EXPECT_EQ(arg, r); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index a377ba832ef5..ee47d9e0e01c 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -241,6 +241,12 @@ void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen) case FUSE_INTERRUPT: printf(" unique=%" PRIu64, in.body.interrupt.unique); break; + case FUSE_IOCTL: + printf(" flags=%#x cmd=%#x in_size=%" PRIu32 + " out_size=%" PRIu32, + in.body.ioctl.flags, in.body.ioctl.cmd, + in.body.ioctl.in_size, in.body.ioctl.out_size); + break; case FUSE_LINK: printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid); break; @@ -678,6 +684,12 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { EXPECT_EQ(inlen, fih + sizeof(in.body.init)); EXPECT_EQ((size_t)buflen, inlen); break; + case FUSE_IOCTL: + EXPECT_GE(inlen, fih + sizeof(in.body.ioctl)); + EXPECT_EQ(inlen, + fih + sizeof(in.body.ioctl) + in.body.ioctl.in_size); + EXPECT_EQ((size_t)buflen, inlen); + break; case FUSE_OPENDIR: EXPECT_EQ(inlen, fih + sizeof(in.body.opendir)); EXPECT_EQ((size_t)buflen, inlen); @@ -733,7 +745,6 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { break; case FUSE_NOTIFY_REPLY: case FUSE_BATCH_FORGET: - case FUSE_IOCTL: case FUSE_POLL: case FUSE_READDIRPLUS: FAIL() << "Unsupported opcode?"; diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index f98a5337c9d1..00503332f820 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -166,6 +166,7 @@ union fuse_payloads_in { fuse_forget_in forget; fuse_getattr_in getattr; fuse_interrupt_in interrupt; + fuse_ioctl_in ioctl; fuse_lk_in getlk; fuse_getxattr_in getxattr; fuse_init_in init; @@ -222,6 +223,7 @@ union fuse_payloads_out { fuse_listxattr_out listxattr; fuse_open_out open; fuse_statfs_out statfs; + fuse_ioctl_out ioctl; /* * The protocol places no limits on the length of the string. This is * merely convenient for testing.