Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F164000704
D58265.id182093.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
30 KB
Referenced Files
None
Subscribers
None
D58265.id182093.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D58265: fusefs: add a simple in-memory file system for the tests
Attached
Detach File
Event Timeline
Log In to Comment