Page MenuHomeFreeBSD

D58265.id182093.diff
No OneTemporary

D58265.id182093.diff

diff --git a/ObsoleteFiles.inc b/ObsoleteFiles.inc
--- a/ObsoleteFiles.inc
+++ b/ObsoleteFiles.inc
@@ -51,6 +51,9 @@
# xargs -n1 | sort | uniq -d;
# done
+# 20260715: test file renamed
+OLD_FILES+=usr/tests/sys/fs/fusefs/ext2-misc
+
# 20260602: Removed by 0a36787e4c1f on 20210723
OLD_FILES+=usr/share/locale/hr_HR.ISO8859-2/LC_MESSAGES
OLD_FILES+=usr/share/locale/nl_BE.ISO8859-1/LC_MESSAGES
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
--- a/tests/sys/fs/fusefs/Makefile
+++ b/tests/sys/fs/fusefs/Makefile
@@ -5,7 +5,13 @@
TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
ATF_TESTS_SH+= ctl
-ATF_TESTS_SH+= ext2-misc
+ATF_TESTS_SH+= integration
+ATF_TESTS_SH+= memfs_test
+
+BINDIR= ${TESTSDIR}
+
+PROGS_CXX+= memfs
+SRCS.memfs= memfs.cc
# 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
diff --git a/tests/sys/fs/fusefs/ctl.sh b/tests/sys/fs/fusefs/ctl.sh
--- a/tests/sys/fs/fusefs/ctl.sh
+++ b/tests/sys/fs/fusefs/ctl.sh
@@ -25,26 +25,41 @@
. $(atf_get_srcdir)/../../cam/ctl/ctl.subr
+_wait_for_mount()
+{
+ mountpoint=$1
+ i=0
+
+ while [ $i -lt 50 ]; do
+ if mount | grep -q " on ${mountpoint} "; then
+ return 0
+ fi
+ i=$((i + 1))
+ sleep 0.1
+ done
+ atf_fail "timed out waiting for ${mountpoint} to mount"
+}
+
# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283402
#
-# Almost any fuse file system would work, but this tests uses fusefs-ext2
-# because it's simple and its download is very small.
+# Almost any fuse file system would work; this test uses memfs.
atf_test_case remove_lun_with_atime cleanup
remove_lun_with_atime_head()
{
atf_set "descr" "Remove a fuse-backed CTL LUN when atime is enabled"
atf_set "require.user" "root"
- atf_set "require.progs" "fuse-ext2 mkfs.ext2"
}
remove_lun_with_atime_body()
{
- MOUNTPOINT=$PWD/mnt
- atf_check mkdir $MOUNTPOINT
- atf_check truncate -s 1g ext2.img
- atf_check mkfs.ext2 -q ext2.img
+ MOUNTPOINT=$PWD/mountpoint
+ MEMFS="$(atf_get_srcdir)"/memfs
+
# Note: both default_permissions and atime must be enabled
- atf_check fuse-ext2 -o default_permissions,allow_other,rw+ ext2.img \
- $MOUNTPOINT
+ atf_check mkdir $MOUNTPOINT
+ atf_check mount_fusefs -o default_permissions,allow_other \
+ auto $MOUNTPOINT $MEMFS &
+ mount_pid=$!
+ _wait_for_mount $MOUNTPOINT
atf_check truncate -s 1m $MOUNTPOINT/file
create_block -o file=$MOUNTPOINT/file
@@ -56,11 +71,14 @@
atf_check -o ignore ctladm remove -b block -l $LUN
rm lun-create.txt # So we don't try to remove the LUN twice
+
+ atf_check umount $MOUNTPOINT
+ wait $mount_pid
}
remove_lun_with_atime_cleanup()
{
cleanup
- umount $PWD/mnt
+ umount -f $PWD/mountpoint 2>/dev/null || true
}
atf_init_test_cases()
diff --git a/tests/sys/fs/fusefs/ext2-misc.sh b/tests/sys/fs/fusefs/integration.sh
rename from tests/sys/fs/fusefs/ext2-misc.sh
rename to tests/sys/fs/fusefs/integration.sh
--- a/tests/sys/fs/fusefs/ext2-misc.sh
+++ b/tests/sys/fs/fusefs/integration.sh
@@ -23,30 +23,51 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# Various tests that execute real fuse file systems
+
+_wait_for_mount()
+{
+ mountpoint=$1
+ i=0
+
+ while [ $i -lt 50 ]; do
+ if mount | grep -q " on ${mountpoint} "; then
+ return 0
+ fi
+ i=$((i + 1))
+ sleep 0.1
+ done
+ atf_fail "timed out waiting for ${mountpoint} to mount"
+}
+
# 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.
+# Almost any fuse file system would work; this test uses memfs.
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
+ MOUNTPOINT=$PWD/mountpoint
+ MEMFS="$(atf_get_srcdir)"/memfs
+
+ atf_check mkdir $MOUNTPOINT
+ atf_check mount_fusefs -o allow_other auto $MOUNTPOINT $MEMFS &
+ mount_pid=$!
+ _wait_for_mount $MOUNTPOINT
+
+ atf_check cp /usr/bin/true $MOUNTPOINT
+ atf_check su -m nobody -c $MOUNTPOINT/true
+
+ atf_check umount $MOUNTPOINT
+ wait $mount_pid
}
execute_cleanup()
{
- umount $PWD/mnt || true
+ umount -f $PWD/mountpoint 2>/dev/null || true
}
atf_init_test_cases()
diff --git a/tests/sys/fs/fusefs/memfs.hh b/tests/sys/fs/fusefs/memfs.hh
new file mode 100644
--- /dev/null
+++ b/tests/sys/fs/fusefs/memfs.hh
@@ -0,0 +1,158 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 ConnectWise
+ *
+ * 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.
+ */
+
+#ifndef _MEMFS_HH_
+#define _MEMFS_HH_
+
+extern "C" {
+#include <sys/types.h>
+
+#include "fuse_kernel.h"
+}
+
+#include <cstdint>
+#include <cstring>
+#include <map>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#define MEMFS_SET_OUT_HEADER_LEN(out, variant) { \
+ (out).header.len = (sizeof((out).header) + \
+ sizeof((out).body.variant)); \
+}
+
+union memfs_payloads_in {
+ fuse_create_in create;
+ fuse_forget_in forget;
+ fuse_getattr_in getattr;
+ fuse_init_in init;
+ fuse_read_in read;
+ fuse_read_in readdir;
+ fuse_release_in release;
+ fuse_setattr_in setattr;
+ fuse_write_in write;
+ uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)];
+ char lookup[0];
+};
+
+struct memfs_buf_in {
+ fuse_in_header header;
+ union memfs_payloads_in body;
+};
+
+union memfs_payloads_out {
+ struct {
+ struct fuse_entry_out entry;
+ struct fuse_open_out open;
+ } create;
+ fuse_attr_out attr;
+ fuse_entry_out entry;
+ fuse_init_out init;
+ uint8_t bytes[0x40000];
+ fuse_statfs_out statfs;
+ fuse_write_out write;
+};
+
+struct memfs_buf_out {
+ fuse_out_header header;
+ union memfs_payloads_out body;
+
+ memfs_buf_out() {
+ memset(this, 0, sizeof(*this));
+ }
+};
+
+enum class MemInodeType {
+ File,
+ Directory,
+};
+
+struct MemInode {
+ uint64_t ino;
+ MemInodeType type;
+ mode_t mode;
+ uid_t uid;
+ gid_t gid;
+ uint32_t nlink;
+ uint64_t nlookup;
+ time_t atime;
+ time_t mtime;
+ time_t ctime;
+ std::vector<uint8_t> data;
+ std::map<std::string, uint64_t> children;
+};
+
+/*
+ * Simple in-memory FUSE file system.
+ *
+ * Talks directly to /dev/fuse without using libfuse(3).
+ */
+class MemFS {
+ int m_fuse_fd;
+ uint32_t m_maxwrite;
+ bool m_quit;
+ std::unordered_map<uint64_t, MemInode> m_inodes;
+ uint64_t m_next_ino;
+
+ void init_root(void);
+ MemInode *lookup_inode(uint64_t ino);
+ const MemInode *lookup_inode(uint64_t ino) const;
+ uint64_t alloc_ino(void);
+ void fill_attr(const MemInode &inode, fuse_attr &attr) const;
+ void fill_entry(const MemInode &inode, fuse_entry_out &entry) const;
+ const MemInode *lookup_name(uint64_t parent, const char *name) const;
+ int lookup_parent_ino(uint64_t dir_ino) const;
+ void bump_nlookup(MemInode &inode, uint64_t count);
+
+ void do_getattr(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_init(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_lookup(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_forget(const memfs_buf_in &in);
+ void do_read(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_write(const memfs_buf_in &in, const ssize_t buflen,
+ memfs_buf_out &out);
+ void do_statfs(memfs_buf_out &out) const;
+ void do_readdir(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_create(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_destroy(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_release(const memfs_buf_in &in, memfs_buf_out &out);
+ void do_setattr(const memfs_buf_in &in, memfs_buf_out &out);
+
+ void process(const memfs_buf_in &in, ssize_t buflen,
+ std::vector<memfs_buf_out> &out);
+ void read_request(memfs_buf_in &in, ssize_t &res);
+ void write_response(const memfs_buf_out &out);
+
+public:
+ MemFS();
+ ~MemFS();
+
+ void loop(void);
+};
+
+#endif /* _MEMFS_HH_ */
diff --git a/tests/sys/fs/fusefs/memfs.cc b/tests/sys/fs/fusefs/memfs.cc
new file mode 100644
--- /dev/null
+++ b/tests/sys/fs/fusefs/memfs.cc
@@ -0,0 +1,770 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 ConnectWise
+ *
+ * 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.
+ */
+
+/*
+ * memfs is a minimal in-memory FUSE file server used by the fusefs test
+ * suite. It is intended to be launched by mount_fusefs(8), similar to file
+ * systems that use libfuse.
+ *
+ * File system state lives entirely in RAM: each inode is a MemInode holding
+ * attributes and either a byte vector (regular files) or a name-to-ino map
+ * (directories). The server runs in the foreground, handling only basic
+ * operations (GETATTR, LOOKUP, FORGET, READ, WRITE, STATFS, READDIR, CREATE,
+ * SETATTR, RELEASE, INIT, and DESTROY); all other opcodes receive ENOSYS.
+ */
+
+extern "C" {
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+}
+
+#include <algorithm>
+#include <cinttypes>
+#include <cstdio>
+#include <ctime>
+#include <iostream>
+#include <stdexcept>
+#include <system_error>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "memfs.hh"
+
+static const uint32_t memfs_init_flags =
+ FUSE_ASYNC_READ | FUSE_EXPORT_SUPPORT | FUSE_BIG_WRITES;
+
+static const char *
+opcode_name(uint32_t opcode)
+{
+ static const char * const table[] = {
+ "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",
+ "IOCTL",
+ "POLL",
+ "NOTIFY_REPLY",
+ "BATCH_FORGET",
+ "FALLOCATE",
+ "READDIRPLUS",
+ "RENAME2",
+ "LSEEK",
+ "COPY_FILE_RANGE",
+ };
+
+ if (opcode >= sizeof(table) / sizeof(table[0]))
+ return ("Unknown");
+ return (table[opcode]);
+}
+
+static uint32_t
+dirent_type(mode_t mode)
+{
+ if (S_ISDIR(mode))
+ return (DT_DIR);
+ if (S_ISREG(mode))
+ return (DT_REG);
+ return (DT_UNKNOWN);
+}
+
+MemFS::MemFS()
+ : m_fuse_fd(-1),
+ m_maxwrite(128 * 1024),
+ m_quit(false),
+ m_next_ino(FUSE_ROOT_ID + 1)
+{
+ const char *fdenv;
+
+ init_root();
+
+ fdenv = getenv("FUSE_DEV_FD");
+ if (fdenv == NULL)
+ throw (std::runtime_error("memfs must be launched by mount_fusefs"));
+ m_fuse_fd = atoi(fdenv);
+ if (m_fuse_fd < 0)
+ throw (std::runtime_error("invalid FUSE_DEV_FD"));
+}
+
+MemFS::~MemFS()
+{
+ if (m_fuse_fd >= 0) {
+ close(m_fuse_fd);
+ m_fuse_fd = -1;
+ }
+}
+
+void
+MemFS::init_root(void)
+{
+ MemInode root;
+
+ root.ino = FUSE_ROOT_ID;
+ root.type = MemInodeType::Directory;
+ root.mode = S_IFDIR | 0755;
+ root.uid = getuid();
+ root.gid = getgid();
+ root.nlink = 2;
+ root.nlookup = 1;
+ root.atime = root.mtime = root.ctime = time(NULL);
+ m_inodes.emplace(FUSE_ROOT_ID, std::move(root));
+}
+
+MemInode *
+MemFS::lookup_inode(uint64_t ino)
+{
+ auto it = m_inodes.find(ino);
+
+ if (it == m_inodes.end())
+ return (NULL);
+ return (&it->second);
+}
+
+const MemInode *
+MemFS::lookup_inode(uint64_t ino) const
+{
+ auto it = m_inodes.find(ino);
+
+ if (it == m_inodes.end())
+ return (NULL);
+ return (&it->second);
+}
+
+uint64_t
+MemFS::alloc_ino(void)
+{
+ return (m_next_ino++);
+}
+
+void
+MemFS::fill_attr(const MemInode &inode, fuse_attr &attr) const
+{
+ const uint64_t size = inode.type == MemInodeType::File ?
+ inode.data.size() : 0;
+
+ bzero(&attr, sizeof(attr));
+ attr.ino = inode.ino;
+ attr.size = size;
+ attr.blocks = (size + 511) / 512;
+ attr.atime = inode.atime;
+ attr.mtime = inode.mtime;
+ attr.ctime = inode.ctime;
+ attr.mode = inode.mode;
+ attr.nlink = inode.nlink;
+ attr.uid = inode.uid;
+ attr.gid = inode.gid;
+ attr.blksize = 512;
+}
+
+void
+MemFS::fill_entry(const MemInode &inode, fuse_entry_out &entry) const
+{
+ bzero(&entry, sizeof(entry));
+ entry.nodeid = inode.ino;
+ entry.generation = 1;
+ entry.entry_valid = UINT64_MAX;
+ entry.attr_valid = UINT64_MAX;
+ fill_attr(inode, entry.attr);
+}
+
+const MemInode *
+MemFS::lookup_name(uint64_t parent, const char *name) const
+{
+ const MemInode *dir;
+ const MemInode *inode;
+
+ if (strcmp(name, ".") == 0)
+ return (lookup_inode(parent));
+ if (strcmp(name, "..") == 0)
+ return (lookup_inode(lookup_parent_ino(parent)));
+
+ dir = lookup_inode(parent);
+ if (dir == NULL || dir->type != MemInodeType::Directory)
+ return (NULL);
+ auto it = dir->children.find(name);
+ if (it == dir->children.end())
+ return (NULL);
+ inode = lookup_inode(it->second);
+ return (inode);
+}
+
+int
+MemFS::lookup_parent_ino(uint64_t dir_ino) const
+{
+ if (dir_ino == FUSE_ROOT_ID)
+ return (FUSE_ROOT_ID);
+
+ for (const auto &pit : m_inodes) {
+ const MemInode &pdir = pit.second;
+
+ if (pdir.type != MemInodeType::Directory)
+ continue;
+ for (const auto &cit : pdir.children) {
+ if (cit.second == dir_ino)
+ return (pdir.ino);
+ }
+ }
+ return (FUSE_ROOT_ID);
+}
+
+void
+MemFS::bump_nlookup(MemInode &inode, uint64_t count)
+{
+ inode.nlookup += count;
+}
+
+void
+MemFS::do_init(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+ out.body.init.flags = in.body.init.flags & memfs_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = in.body.init.max_readahead;
+ out.body.init.time_gran = 1;
+ MEMFS_SET_OUT_HEADER_LEN(out, init);
+}
+
+void
+MemFS::do_getattr(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const MemInode *inode;
+
+ inode = lookup_inode(in.header.nodeid);
+ if (inode == NULL) {
+ out.header.unique = in.header.unique;
+ out.header.error = -ENOENT;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ bzero(&out.body.attr, sizeof(out.body.attr));
+ out.body.attr.attr_valid = UINT64_MAX;
+ fill_attr(*inode, out.body.attr.attr);
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ MEMFS_SET_OUT_HEADER_LEN(out, attr);
+}
+
+void
+MemFS::do_lookup(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const char *name = in.body.lookup;
+ const MemInode *inode;
+ MemInode *mut;
+
+ inode = lookup_name(in.header.nodeid, name);
+ if (inode == NULL) {
+ out.header.unique = in.header.unique;
+ out.header.error = -ENOENT;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ mut = lookup_inode(inode->ino);
+ bump_nlookup(*mut, 1);
+ fill_entry(*inode, out.body.entry);
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ MEMFS_SET_OUT_HEADER_LEN(out, entry);
+}
+
+void
+MemFS::do_forget(const memfs_buf_in &in)
+{
+ MemInode *inode;
+
+ inode = lookup_inode(in.header.nodeid);
+ if (inode == NULL)
+ return;
+ if (in.body.forget.nlookup >= inode->nlookup)
+ inode->nlookup -= in.body.forget.nlookup;
+ else
+ inode->nlookup = 0;
+}
+
+void
+MemFS::do_read(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const MemInode *inode;
+ size_t copysize;
+
+ inode = lookup_inode(in.header.nodeid);
+ if (inode == NULL || inode->type != MemInodeType::File) {
+ out.header.unique = in.header.unique;
+ out.header.error = -EIO;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ if (in.body.read.offset >= inode->data.size())
+ copysize = 0;
+ else
+ copysize = std::min((size_t)in.body.read.size,
+ inode->data.size() - (size_t)in.body.read.offset);
+ memcpy(out.body.bytes, inode->data.data() + in.body.read.offset,
+ copysize);
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.header.len = sizeof(out.header) + copysize;
+}
+
+void
+MemFS::do_write(const memfs_buf_in &in, const ssize_t buflen,
+ memfs_buf_out &out)
+{
+ const char *buf;
+ MemInode *inode;
+ size_t end;
+ size_t hdrsz;
+
+ hdrsz = sizeof(in.header) + sizeof(in.body.write);
+ if (buflen < (ssize_t)(hdrsz + in.body.write.size)) {
+ out.header.unique = in.header.unique;
+ out.header.error = -EINVAL;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ inode = lookup_inode(in.header.nodeid);
+ if (inode == NULL || inode->type != MemInodeType::File) {
+ out.header.unique = in.header.unique;
+ out.header.error = -EIO;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ buf = (const char *)in.body.bytes + sizeof(in.body.write);
+ end = (size_t)in.body.write.offset + in.body.write.size;
+ if (end > inode->data.size())
+ inode->data.resize(end);
+ memcpy(inode->data.data() + in.body.write.offset, buf,
+ in.body.write.size);
+
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.body.write.size = in.body.write.size;
+ MEMFS_SET_OUT_HEADER_LEN(out, write);
+}
+
+void
+MemFS::do_statfs(memfs_buf_out &out) const
+{
+ uint64_t blocks = 1024 * 1024;
+ uint64_t used = 0;
+
+ for (const auto &it : m_inodes) {
+ if (it.second.type == MemInodeType::File)
+ used += (it.second.data.size() + 511) / 512;
+ }
+
+ bzero(&out.body.statfs, sizeof(out.body.statfs));
+ out.body.statfs.st.blocks = blocks;
+ out.body.statfs.st.bfree = blocks - used;
+ out.body.statfs.st.bavail = blocks - used;
+ out.body.statfs.st.files = m_inodes.size();
+ out.body.statfs.st.ffree = 1024;
+ out.body.statfs.st.bsize = 512;
+ out.body.statfs.st.namelen = 255;
+ out.body.statfs.st.frsize = 512;
+ out.header.error = 0;
+ MEMFS_SET_OUT_HEADER_LEN(out, statfs);
+}
+
+void
+MemFS::do_readdir(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const MemInode *dir;
+ std::vector<std::tuple<uint64_t, uint32_t, std::string>> ents;
+ uint64_t nextoff;
+ size_t bufsize;
+ size_t used;
+
+ dir = lookup_inode(in.header.nodeid);
+ if (dir == NULL || dir->type != MemInodeType::Directory) {
+ out.header.unique = in.header.unique;
+ out.header.error = -ENOTDIR;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ nextoff = 1;
+ ents.emplace_back(nextoff++, DT_DIR, "..");
+ ents.emplace_back(nextoff++, DT_DIR, ".");
+ for (const auto &child : dir->children) {
+ const MemInode *inode = lookup_inode(child.second);
+
+ if (inode == NULL)
+ continue;
+ ents.emplace_back(nextoff++, dirent_type(inode->mode),
+ child.first);
+ }
+
+ bufsize = in.body.readdir.size;
+ used = 0;
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+
+ for (const auto &ent : ents) {
+ struct fuse_dirent de;
+ size_t entsize;
+ uint64_t off;
+ uint32_t type;
+ std::string name;
+
+ std::tie(off, type, name) = ent;
+ if (off <= in.body.readdir.offset)
+ continue;
+
+ bzero(&de, sizeof(de));
+ if (name == ".")
+ de.ino = dir->ino;
+ else if (name == "..")
+ de.ino = lookup_parent_ino(dir->ino);
+ else
+ de.ino = dir->children.at(name);
+ de.off = off;
+ de.namelen = name.size();
+ de.type = type;
+ entsize = FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + de.namelen);
+ if (used + entsize > bufsize)
+ break;
+
+ memcpy(out.body.bytes + used, &de, FUSE_NAME_OFFSET);
+ memcpy(out.body.bytes + used + FUSE_NAME_OFFSET, name.data(),
+ name.size());
+ used += entsize;
+ }
+
+ out.header.len += used;
+}
+
+void
+MemFS::do_create(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const char *name;
+ MemInode *parent;
+ MemInode file;
+ mode_t mode;
+
+ name = (const char *)in.body.bytes + sizeof(fuse_create_in);
+ parent = lookup_inode(in.header.nodeid);
+ if (parent == NULL || parent->type != MemInodeType::Directory) {
+ out.header.unique = in.header.unique;
+ out.header.error = -ENOTDIR;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+ if (parent->children.find(name) != parent->children.end()) {
+ out.header.unique = in.header.unique;
+ out.header.error = -EEXIST;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ mode = (in.body.create.mode & ~in.body.create.umask) | S_IFREG;
+
+ file.ino = alloc_ino();
+ file.type = MemInodeType::File;
+ file.mode = mode;
+ file.uid = in.header.uid;
+ file.gid = in.header.gid;
+ file.nlink = 1;
+ file.nlookup = 1;
+ file.atime = file.mtime = file.ctime = time(NULL);
+
+ parent->children.emplace(name, file.ino);
+ fill_entry(file, out.body.create.entry);
+ out.body.create.open.fh = file.ino;
+ out.body.create.open.open_flags = 0;
+
+ m_inodes.emplace(file.ino, std::move(file));
+
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ MEMFS_SET_OUT_HEADER_LEN(out, create);
+}
+
+void
+MemFS::do_destroy(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ m_quit = true;
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+}
+
+void
+MemFS::do_release(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ MemInode *inode;
+
+ inode = lookup_inode(in.header.nodeid);
+ if (inode != NULL && inode->nlookup > 0)
+ inode->nlookup--;
+
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+}
+
+void
+MemFS::do_setattr(const memfs_buf_in &in, memfs_buf_out &out)
+{
+ const fuse_setattr_in *sa;
+ MemInode *inode;
+ time_t now;
+
+ sa = &in.body.setattr;
+ inode = lookup_inode(in.header.nodeid);
+ if (inode == NULL) {
+ out.header.unique = in.header.unique;
+ out.header.error = -ENOENT;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+
+ if (sa->valid & FATTR_MODE)
+ inode->mode = (inode->mode & S_IFMT) | (sa->mode & 07777);
+ if (sa->valid & FATTR_UID)
+ inode->uid = sa->uid;
+ if (sa->valid & FATTR_GID)
+ inode->gid = sa->gid;
+ if (sa->valid & FATTR_SIZE) {
+ if (inode->type != MemInodeType::File) {
+ out.header.unique = in.header.unique;
+ out.header.error = -EISDIR;
+ out.header.len = sizeof(out.header);
+ return;
+ }
+ inode->data.resize(sa->size);
+ }
+ now = time(NULL);
+ if (sa->valid & FATTR_ATIME)
+ inode->atime = sa->atime;
+ else if (sa->valid & FATTR_ATIME_NOW)
+ inode->atime = now;
+ if (sa->valid & FATTR_MTIME)
+ inode->mtime = sa->mtime;
+ else if (sa->valid & FATTR_MTIME_NOW)
+ inode->mtime = now;
+ if (sa->valid & FATTR_CTIME)
+ inode->ctime = sa->ctime;
+ else if (sa->valid & (FATTR_MODE | FATTR_UID | FATTR_GID | FATTR_SIZE |
+ FATTR_ATIME | FATTR_ATIME_NOW | FATTR_MTIME | FATTR_MTIME_NOW))
+ inode->ctime = now;
+
+ bzero(&out.body.attr, sizeof(out.body.attr));
+ out.body.attr.attr_valid = UINT64_MAX;
+ fill_attr(*inode, out.body.attr.attr);
+ out.header.unique = in.header.unique;
+ out.header.error = 0;
+ MEMFS_SET_OUT_HEADER_LEN(out, attr);
+}
+
+void
+MemFS::process(const memfs_buf_in &in, ssize_t buflen,
+ std::vector<memfs_buf_out> &out)
+{
+ memfs_buf_out resp;
+
+ resp.header.unique = in.header.unique;
+ switch (in.header.opcode) {
+ case FUSE_INIT:
+ do_init(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_LOOKUP:
+ do_lookup(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_GETATTR:
+ do_getattr(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_FORGET:
+ do_forget(in);
+ break;
+ case FUSE_READ:
+ do_read(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_WRITE:
+ do_write(in, buflen, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_STATFS:
+ do_statfs(resp);
+ resp.header.unique = in.header.unique;
+ out.push_back(resp);
+ break;
+ case FUSE_READDIR:
+ do_readdir(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_CREATE:
+ do_create(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_SETATTR:
+ do_setattr(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_RELEASE:
+ do_release(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_DESTROY:
+ do_destroy(in, resp);
+ out.push_back(resp);
+ break;
+ case FUSE_OPEN:
+ case FUSE_OPENDIR:
+ case FUSE_ACCESS:
+ case FUSE_FLUSH:
+ case FUSE_LSEEK:
+ resp.header.error = -ENOSYS;
+ resp.header.len = sizeof(resp.header);
+ out.push_back(resp);
+ break;
+ default:
+ fprintf(stderr, "memfs: unhandled FUSE opcode %s (%" PRIu32 ")\n",
+ opcode_name(in.header.opcode), in.header.opcode);
+ resp.header.error = -ENOSYS;
+ resp.header.len = sizeof(resp.header);
+ out.push_back(resp);
+ break;
+ }
+}
+
+void
+MemFS::read_request(memfs_buf_in &in, ssize_t &res)
+{
+ res = read(m_fuse_fd, &in, sizeof(in));
+ if (res < 0 && errno == ENODEV)
+ m_quit = true;
+ if (res < 0 && errno != EBADF && !m_quit)
+ throw (std::system_error(errno, std::system_category(),
+ "read from /dev/fuse"));
+ if (res >= (ssize_t)sizeof(in.header) && !m_quit &&
+ res != (ssize_t)in.header.len)
+ throw (std::runtime_error("short FUSE read"));
+}
+
+void
+MemFS::write_response(const memfs_buf_out &out)
+{
+ ssize_t r;
+
+ r = write(m_fuse_fd, &out, out.header.len);
+ if (r < 0 && !m_quit)
+ throw (std::system_error(errno, std::system_category(),
+ "write to /dev/fuse"));
+}
+
+void
+MemFS::loop(void)
+{
+ memfs_buf_in *in = new memfs_buf_in;
+ std::vector<memfs_buf_out> out;
+
+ while (!m_quit) {
+ ssize_t buflen;
+
+ bzero(in, sizeof(*in));
+ read_request(*in, buflen);
+ if (m_quit)
+ break;
+ out.clear();
+ process(*in, buflen, out);
+ for (const auto &resp : out)
+ write_response(resp);
+ }
+
+ delete in;
+}
+
+int
+main(void)
+{
+ try {
+ MemFS fs;
+
+ fs.loop();
+ } catch (const std::system_error &err) {
+ std::cerr << err.what() << '\n';
+ return (1);
+ } catch (const std::exception &err) {
+ std::cerr << err.what() << '\n';
+ return (1);
+ }
+
+ return (0);
+}
diff --git a/tests/sys/fs/fusefs/memfs_test.sh b/tests/sys/fs/fusefs/memfs_test.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/fs/fusefs/memfs_test.sh
@@ -0,0 +1,97 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 ConnectWise
+#
+# 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.
+
+_wait_for_mount()
+{
+ mountpoint=$1
+ i=0
+
+ while [ $i -lt 50 ]; do
+ if mount | grep -q " on ${mountpoint} "; then
+ return 0
+ fi
+ i=$((i + 1))
+ sleep 0.1
+ done
+ atf_fail "timed out waiting for ${mountpoint} to mount"
+}
+
+atf_test_case create_write_read cleanup
+create_write_read_head()
+{
+ atf_set "descr" "Create, write, and read a file on MemFS"
+}
+create_write_read_body()
+{
+ MOUNTPOINT=$PWD/mountpoint
+ MSG="hello, memfs"
+ MEMFS="$(atf_get_srcdir)"/memfs
+
+ atf_check mkdir $MOUNTPOINT
+ atf_check mount_fusefs auto $MOUNTPOINT $MEMFS &
+ mount_pid=$!
+ _wait_for_mount $MOUNTPOINT
+
+ atf_check sh -c "printf '%s' '$MSG' > $MOUNTPOINT/test.txt"
+ atf_check -o inline:"$MSG" cat $MOUNTPOINT/test.txt
+
+ atf_check umount $MOUNTPOINT
+ wait $mount_pid
+}
+create_write_read_cleanup()
+{
+ umount -f $PWD/mountpoint 2>/dev/null || true
+}
+
+atf_test_case statfs cleanup
+statfs_head()
+{
+ atf_set "descr" "Query MemFS statfs information"
+}
+statfs_body()
+{
+ MOUNTPOINT=$PWD/mountpoint
+ MEMFS="$(atf_get_srcdir)"/memfs
+
+ atf_check mkdir $MOUNTPOINT
+ atf_check mount_fusefs auto $MOUNTPOINT $MEMFS &
+ mount_pid=$!
+ _wait_for_mount $MOUNTPOINT
+
+ atf_check -o match:"fusefs" df -T $MOUNTPOINT
+
+ atf_check umount $MOUNTPOINT
+ wait $mount_pid
+}
+statfs_cleanup()
+{
+ umount -f $PWD/mountpoint 2>/dev/null || true
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case create_write_read
+ atf_add_test_case statfs
+}

File Metadata

Mime Type
text/plain
Expires
Tue, Jul 28, 6:48 PM (4 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
35487613
Default Alt Text
D58265.id182093.diff (30 KB)

Event Timeline