Skip site navigation (1)Skip section navigation (2)
Date:      Sat, 10 Jan 2026 00:07:00 +0000
From:      Warner Losh <imp@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Cc:        CismonX <admin@cismon.net>
Subject:   git: 17ba6f428683 - main - fusefs: support FUSE_IOCTL
Message-ID:  <69619824.3634e.83e2f70@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by imp:

URL: https://cgit.FreeBSD.org/src/commit/?id=17ba6f428683b661178b50a9d59f8b9e0dd2138a

commit 17ba6f428683b661178b50a9d59f8b9e0dd2138a
Author:     CismonX <admin@cismon.net>
AuthorDate: 2025-07-05 12:46:27 +0000
Commit:     Warner Losh <imp@FreeBSD.org>
CommitDate: 2026-01-10 00:06:21 +0000

    fusefs: support FUSE_IOCTL
    
    MFC After: 1 week
    Signed-off-by: CismonX <admin@cismon.net>
    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(-)

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 <sys/vmmeter.h>
 #define EXTERR_CATEGORY EXTERR_CAT_FUSE_VNOPS
 #include <sys/exterrvar.h>
+#include <sys/sysent.h>
 
 #include <vm/vm.h>
 #include <vm/vm_extern.h>
@@ -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 <admin@cismon.net>
+ *
+ * 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/ioctl.h>
+#include <fcntl.h>
+#include <string.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+using IoctlTestProcT = std::function<void (int)>;
+
+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.


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?69619824.3634e.83e2f70>