diff --git a/doc/LICENSING.rst b/doc/LICENSING.rst
index f267160c8d36d..6a5488f6ad7bc 100644
--- a/doc/LICENSING.rst
+++ b/doc/LICENSING.rst
@@ -108,6 +108,15 @@ Python Devicetree library test files
* Various yaml files under ``scripts/dts/python-devicetree/tests``
+FUSE Interface Definition Header File
+--------------------------------------
+
+* *Licensing:* `BSD-2-clause`_
+* *Impact:* This header is used in Zephyr build only if :kconfig:option:`CONFIG_FUSE_CLIENT` is enabled.
+* *Files*:
+
+ * :zephyr_file:`subsys/fs/fuse_client/fuse_abi.h`
+
.. _Apache 2.0 License:
https://github.com/zephyrproject-rtos/zephyr/blob/main/LICENSE
@@ -120,6 +129,9 @@ Python Devicetree library test files
.. _BSD-3-clause:
https://opensource.org/license/bsd-3-clause
+.. _BSD-2-clause:
+ https://opensource.org/license/bsd-2-clause
+
.. _Coccinelle:
https://coccinelle.gitlabpages.inria.fr/website/
diff --git a/doc/hardware/virtualization/virtio.rst b/doc/hardware/virtualization/virtio.rst
index 9c67ad26e3fa9..12f35c3700dc8 100644
--- a/doc/hardware/virtualization/virtio.rst
+++ b/doc/hardware/virtualization/virtio.rst
@@ -123,6 +123,31 @@ virtqueue has to be acquired using :c:func:`virtio_get_virtqueue`. To send data
will be invoked once the device returns the given descriptor chain. After that, the virtqueue has to be notified using
:c:func:`virtio_notify_virtqueue` from the Virtio API.
+Guest-side Virtio drivers
+*************************
+Currently Zephyr provides drivers for Virtio over PCI and Virtio over MMIO and drivers for two devices using virtio - virtiofs, used
+to access the filesystem of the host and virtio-entropy, used as an entropy source.
+
+Virtiofs
+=========
+This driver provides support for `virtiofs `_ - a filesystem allowing a virtual machine guest to access
+a directory on the host. It uses FUSE messages to communicate between the host and the guest in order to perform filesystem operations such as
+opening and reading files. Every time the guest wants to perform some filesystem operation it places in the virtqueue a descriptor chain
+starting with the device readable part, containing the FUSE input header and input data, and ending it with the device writeable part, with place
+for the FUSE output header and output data.
+
+Virtio-entropy
+==============
+This driver allows using virtio-entropy as an entropy source in Zephyr. The operation of this device is simple - the driver places a
+buffer in the virtqueue and receives it back, filled with random data.
+
+Virtio samples
+**************
+A sample showcasing the use of a driver relying on Virtio is provided in :zephyr:code-sample:`virtiofs`. If you wish
+to check code interfacing directly with the Virtio driver, you can check the virtiofs driver, especially :c:func:`virtiofs_init`
+for initialization and :c:func:`virtiofs_send_receive` with the :c:func:`virtiofs_recv_cb` for data transfer to/from
+the Virtio device.
+
API Reference
*************
diff --git a/include/zephyr/fs/fs.h b/include/zephyr/fs/fs.h
index e2cf7dff73b40..a07f18442507c 100644
--- a/include/zephyr/fs/fs.h
+++ b/include/zephyr/fs/fs.h
@@ -61,6 +61,9 @@ enum {
/** Identifier for in-tree Ext2 file system. */
FS_EXT2,
+ /** Identifier for in-tree Virtiofs file system. */
+ FS_VIRTIOFS,
+
/** Base identifier for external file systems. */
FS_TYPE_EXTERNAL_BASE,
};
diff --git a/include/zephyr/fs/fs_interface.h b/include/zephyr/fs/fs_interface.h
index 1f0d26ce5bc1e..6c1bfb4385080 100644
--- a/include/zephyr/fs/fs_interface.h
+++ b/include/zephyr/fs/fs_interface.h
@@ -50,6 +50,10 @@ extern "C" {
#define MAX_FILE_NAME 255
#endif
+#if !defined(MAX_FILE_NAME) && defined(CONFIG_FILE_SYSTEM_VIRTIOFS)
+#define MAX_FILE_NAME 255
+#endif
+
#if !defined(MAX_FILE_NAME) /* filesystem selection */
/* Use standard 8.3 when no filesystem is explicitly selected */
#define MAX_FILE_NAME 12
diff --git a/include/zephyr/fs/virtiofs.h b/include/zephyr/fs/virtiofs.h
new file mode 100644
index 0000000000000..b7dec1c0bcaf8
--- /dev/null
+++ b/include/zephyr/fs/virtiofs.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ZEPHYR_INCLUDE_FS_VIRTIOFS_H_
+#define ZEPHYR_INCLUDE_FS_VIRTIOFS_H_
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct virtiofs_fs_data {
+ uint32_t max_write;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_INCLUDE_FS_VIRTIOFS_H_ */
diff --git a/samples/subsys/fs/virtiofs/CMakeLists.txt b/samples/subsys/fs/virtiofs/CMakeLists.txt
new file mode 100644
index 0000000000000..e3ee8757dc009
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/CMakeLists.txt
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(virtiofs)
+
+target_sources(app PRIVATE src/main.c)
diff --git a/samples/subsys/fs/virtiofs/README.rst b/samples/subsys/fs/virtiofs/README.rst
new file mode 100644
index 0000000000000..3ceeb02f554f4
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/README.rst
@@ -0,0 +1,73 @@
+.. zephyr:code-sample:: virtiofs
+ :name: virtiofs filesystem
+ :relevant-api: file_system_api
+
+ Use file system API over virtiofs.
+
+Overview
+********
+
+This sample app demonstrates the use of Zephyr's :ref:`file system API
+` over `virtiofs `_ by reading, creating and listing files and directories.
+In the case of virtiofs the mounted filesystem is a directory on the host.
+
+Requirements
+************
+This sample requires `virtiofsd `_ to run.
+
+Building
+********
+.. zephyr-app-commands::
+ :zephyr-app: samples/subsys/fs/virtiofs
+ :board: qemu_x86_64
+ :goals: build
+ :compact:
+
+
+Running
+*******
+Before launching QEMU ``virtiofsd`` has to be running. QEMU's arguments are embedded using :code:`CONFIG_QEMU_EXTRA_FLAGS` and socket path is set to :code:`/tmp/vhostqemu`, so ``virtiofsd`` has to be launched using
+
+.. code-block::
+
+ virtiofsd --socket-path=/tmp/vhostqemu -o source=shared_dir_path
+
+where :code:`shared_dir_path` is a directory that will be mounted on Zephyr side.
+Then you can launch QEMU using:
+
+.. code-block::
+
+ west build -t run
+
+This sample will list the files and directories in the mounted filesystem and print the contents of the file :code:`file` in the mounted directory.
+This sample will also create some files and directories.
+You can create the sample directory using :code:`prepare_sample_directory.sh`.
+
+Example output:
+
+.. code-block::
+
+ *** Booting Zephyr OS build v4.1.0-rc1-28-gc6816316fc50 ***
+ /virtiofs directory tree:
+ - dir2 (type=dir)
+ - b (type=file, size=3)
+ - a (type=file, size=2)
+ - c (type=file, size=4)
+ - dir (type=dir)
+ - some_file (type=file, size=0)
+ - nested_dir (type=dir)
+ - some_other_file (type=file, size=0)
+ - file (type=file, size=27)
+
+ /virtiofs/file content:
+ this is a file on the host
+
+
+After running the sample you can check the created files:
+
+.. code-block:: console
+
+ shared_dir_path$ cat file_created_by_zephyr
+ hello world
+ shared_dir_path$ cat second_file_created_by_zephyr
+ lorem ipsum
diff --git a/samples/subsys/fs/virtiofs/boards/qemu_x86.overlay b/samples/subsys/fs/virtiofs/boards/qemu_x86.overlay
new file mode 100644
index 0000000000000..c0bd658d4bf0f
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/boards/qemu_x86.overlay
@@ -0,0 +1,13 @@
+&pcie0 {
+ virtio_pci: virtio_pci {
+ compatible = "virtio,pci";
+
+ vendor-id = <0x1af4>;
+ device-id = <0x105a>;
+
+ interrupts = <0xb 0x0 0x0>;
+ interrupt-parent = <&intc>;
+
+ status = "okay";
+ };
+};
diff --git a/samples/subsys/fs/virtiofs/boards/qemu_x86_64.overlay b/samples/subsys/fs/virtiofs/boards/qemu_x86_64.overlay
new file mode 100644
index 0000000000000..c0bd658d4bf0f
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/boards/qemu_x86_64.overlay
@@ -0,0 +1,13 @@
+&pcie0 {
+ virtio_pci: virtio_pci {
+ compatible = "virtio,pci";
+
+ vendor-id = <0x1af4>;
+ device-id = <0x105a>;
+
+ interrupts = <0xb 0x0 0x0>;
+ interrupt-parent = <&intc>;
+
+ status = "okay";
+ };
+};
diff --git a/samples/subsys/fs/virtiofs/prepare_sample_directory.sh b/samples/subsys/fs/virtiofs/prepare_sample_directory.sh
new file mode 100755
index 0000000000000..deb4603325cea
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/prepare_sample_directory.sh
@@ -0,0 +1,11 @@
+# Copyright (c) 2025 Antmicro
+# SPDX-License-Identifier: Apache-2.0
+
+mkdir -p dir/nested_dir
+touch dir/some_file
+touch dir/nested_dir/some_other_file
+mkdir dir2
+echo "a" > dir2/a
+echo "bb" > dir2/b
+echo "ccc" > dir2/c
+echo "this is a file on the host" > file
diff --git a/samples/subsys/fs/virtiofs/prj.conf b/samples/subsys/fs/virtiofs/prj.conf
new file mode 100644
index 0000000000000..df178fc050c62
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/prj.conf
@@ -0,0 +1,7 @@
+CONFIG_VIRTIO=y
+CONFIG_PCIE=y
+CONFIG_FILE_SYSTEM=y
+CONFIG_FILE_SYSTEM_VIRTIOFS=y
+CONFIG_HEAP_MEM_POOL_SIZE=100000
+CONFIG_MAIN_STACK_SIZE=16384
+CONFIG_QEMU_EXTRA_FLAGS="-chardev socket,id=char0,path=/tmp/vhostqemu -device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=myfs -m 32M -object memory-backend-memfd,id=mem,size=32M,share=on -numa node,memdev=mem"
diff --git a/samples/subsys/fs/virtiofs/sample.yaml b/samples/subsys/fs/virtiofs/sample.yaml
new file mode 100644
index 0000000000000..c92b8aa03a054
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/sample.yaml
@@ -0,0 +1,10 @@
+sample:
+ name: virtio filesystem sample
+common:
+ tags:
+ - filesystem
+ - virtio
+tests:
+ sample.filesystem.virtiofs:
+ build_only: true
+ filter: CONFIG_DT_HAS_VIRTIO_PCI_ENABLED
diff --git a/samples/subsys/fs/virtiofs/src/main.c b/samples/subsys/fs/virtiofs/src/main.c
new file mode 100644
index 0000000000000..692df02b8aa86
--- /dev/null
+++ b/samples/subsys/fs/virtiofs/src/main.c
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define VIRTIO_DEV DEVICE_DT_GET(DT_NODELABEL(virtio_pci))
+
+#define MOUNT_POINT "/virtiofs"
+
+struct virtiofs_fs_data fs_data;
+
+static struct fs_mount_t mp = {
+ .type = FS_VIRTIOFS,
+ .fs_data = &fs_data,
+ .flags = 0,
+ .storage_dev = (void *)VIRTIO_DEV,
+ .mnt_point = MOUNT_POINT,
+};
+
+void dirtree(const char *path, int indent)
+{
+ struct fs_dir_t dir;
+
+ fs_dir_t_init(&dir);
+ if (fs_opendir(&dir, path) == 0) {
+ while (1) {
+ struct fs_dirent entry;
+
+ if (fs_readdir(&dir, &entry) == 0) {
+ if (entry.name[0] == '\0') {
+ break;
+ }
+ if (entry.type == FS_DIR_ENTRY_DIR) {
+ printf("%*s- %s (type=dir)\n", indent * 2, "", entry.name);
+ char *subdir_path = k_malloc(
+ strlen(path) + strlen(entry.name) + 2
+ );
+
+ if (subdir_path == NULL) {
+ printf("failed to allocate subdir path\n");
+ continue;
+ }
+ strcpy(subdir_path, path);
+ strcat(subdir_path, "/");
+ strcat(subdir_path, entry.name);
+ dirtree(subdir_path, indent + 1);
+ k_free(subdir_path);
+ } else {
+ printf(
+ "%*s- %s (type=file, size=%zu)\n",
+ indent * 2, "", entry.name, entry.size
+ );
+ }
+ } else {
+ printf("failed to readdir %s\n", path);
+ break;
+ }
+ };
+
+ fs_closedir(&dir);
+ } else {
+ printf("failed to opendir %s\n", path);
+ }
+}
+
+void create_file(const char *path, const char *content)
+{
+ struct fs_file_t file;
+
+ fs_file_t_init(&file);
+ if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) == 0) {
+ fs_write(&file, content, strlen(content) + 1);
+ } else {
+ printf("failed to create %s\n", path);
+ }
+ fs_close(&file);
+}
+
+void print_file(const char *path)
+{
+ struct fs_file_t file;
+
+ fs_file_t_init(&file);
+ if (fs_open(&file, path, FS_O_READ) == 0) {
+ char buf[256] = "\0";
+ int read_c = fs_read(&file, buf, sizeof(buf));
+
+ if (read_c >= 0) {
+ buf[read_c] = 0;
+
+ printf(
+ "%s content:\n"
+ "%s\n",
+ path, buf
+ );
+ } else {
+ printf("failed to read from %s\n", path);
+ }
+
+ fs_close(&file);
+ } else {
+ printf("failed to open %s\n", path);
+ }
+}
+
+int main(void)
+{
+ if (fs_mount(&mp) == 0) {
+ printf("%s directory tree:\n", MOUNT_POINT);
+ dirtree(MOUNT_POINT, 0);
+ printf("\n");
+
+ print_file(MOUNT_POINT"/file");
+
+ create_file("/virtiofs/file_created_by_zephyr", "hello world\n");
+
+ create_file("/virtiofs/second_file_created_by_zephyr", "lorem ipsum\n");
+
+ fs_mkdir("/virtiofs/dir_created_by_zephyr");
+ } else {
+ printf("failed to mount %s\n", MOUNT_POINT);
+ }
+
+ return 0;
+}
diff --git a/subsys/fs/CMakeLists.txt b/subsys/fs/CMakeLists.txt
index 48df605ae013a..79041623cb317 100644
--- a/subsys/fs/CMakeLists.txt
+++ b/subsys/fs/CMakeLists.txt
@@ -17,12 +17,16 @@ if(CONFIG_FILE_SYSTEM_LIB_LINK)
endif()
add_subdirectory_ifdef(CONFIG_FILE_SYSTEM_EXT2 ext2)
+ add_subdirectory_ifdef(CONFIG_FUSE_CLIENT fuse_client)
+ add_subdirectory_ifdef(CONFIG_FILE_SYSTEM_VIRTIOFS virtiofs)
zephyr_library_link_libraries(FS)
target_link_libraries_ifdef(CONFIG_FAT_FILESYSTEM_ELM FS INTERFACE ELMFAT)
target_link_libraries_ifdef(CONFIG_FILE_SYSTEM_LITTLEFS FS INTERFACE LITTLEFS)
target_link_libraries_ifdef(CONFIG_FILE_SYSTEM_EXT2 FS INTERFACE EXT2)
+ target_link_libraries_ifdef(CONFIG_FUSE_CLIENT FS INTERFACE FUSE_CLIENT)
+ target_link_libraries_ifdef(CONFIG_FILE_SYSTEM_VIRTIOFS FS INTERFACE VIRTIOFS)
endif()
add_subdirectory_ifdef(CONFIG_FCB ./fcb)
diff --git a/subsys/fs/Kconfig b/subsys/fs/Kconfig
index 4dde4b27100d9..709bb91bb5e89 100644
--- a/subsys/fs/Kconfig
+++ b/subsys/fs/Kconfig
@@ -119,6 +119,8 @@ source "subsys/logging/Kconfig.template.log_config"
rsource "Kconfig.fatfs"
rsource "Kconfig.littlefs"
rsource "ext2/Kconfig"
+rsource "fuse_client/Kconfig"
+rsource "virtiofs/Kconfig"
endif # FILE_SYSTEM_LIB_LINK
diff --git a/subsys/fs/fuse_client/CMakeLists.txt b/subsys/fs/fuse_client/CMakeLists.txt
new file mode 100644
index 0000000000000..ff77ea544d434
--- /dev/null
+++ b/subsys/fs/fuse_client/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Copyright (c) 2025 Antmicro
+# SPDX-License-Identifier: Apache-2.0
+
+# This library provides a set of functions for creating FUSE structures
+
+add_library(FUSE_CLIENT INTERFACE)
+target_include_directories(FUSE_CLIENT INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
+
+zephyr_library()
+zephyr_library_sources(
+ fuse_client.c
+)
+
+zephyr_library_link_libraries(FUSE_CLIENT)
diff --git a/subsys/fs/fuse_client/Kconfig b/subsys/fs/fuse_client/Kconfig
new file mode 100644
index 0000000000000..3ce9e8cf992b8
--- /dev/null
+++ b/subsys/fs/fuse_client/Kconfig
@@ -0,0 +1,32 @@
+# Copyright (c) 2025 Antmicro
+# SPDX-License-Identifier: Apache-2.0
+
+config FUSE_CLIENT
+ bool "FUSE client-side primitives support"
+ help
+ Enable FUSE client-side primitives support.
+
+config FUSE_CLIENT_UID_VALUE
+ int "FUSE user ID"
+ default 0
+ help
+ Each FUSE request contains user ID, this config allows setting
+ that value. The result is as if user with given UID accessed the file/resource.
+
+config FUSE_CLIENT_GID_VALUE
+ int "FUSE group ID"
+ default 0
+ help
+ Each FUSE request contains group ID, this config allows setting
+ that value. The result is as if user with given GID accessed the file/resource.
+
+config FUSE_CLIENT_PID_VALUE
+ int "FUSE process ID"
+ default 0
+ help
+ Each FUSE request contains process ID, this config allows setting
+ that value. The result is as if process with given PID accessed the file/resource.
+
+module = FUSE_CLIENT
+module-str = fuse
+source "subsys/logging/Kconfig.template.log_config"
diff --git a/subsys/fs/fuse_client/fuse_abi.h b/subsys/fs/fuse_client/fuse_abi.h
new file mode 100644
index 0000000000000..85ea5f46376b0
--- /dev/null
+++ b/subsys/fs/fuse_client/fuse_abi.h
@@ -0,0 +1,273 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-2-Clause) */
+
+/*
+ * This file is based on include/uapi/linux/fuse.h from Linux, and is used
+ * under the BSD-2-Clause license, as per the dual-license option
+ */
+/*
+ * This file defines the kernel interface of FUSE
+ * This -- and only this -- header file may also be distributed under
+ * the terms of the BSD Licence as follows:
+ *
+ * Copyright (C) 2001-2007 Miklos Szeredi. 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 SOFTWARE IS PROVIDED BY 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 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 ZEPHYR_SUBSYS_FS_FUSE_ABI_H_
+#define ZEPHYR_SUBSYS_FS_FUSE_ABI_H_
+#include
+
+#define FUSE_MAJOR_VERSION 7
+#define FUSE_MINOR_VERSION 31
+
+#define FUSE_LOOKUP 1
+#define FUSE_FORGET 2
+#define FUSE_SETATTR 4
+#define FUSE_MKDIR 9
+#define FUSE_UNLINK 10
+#define FUSE_RMDIR 11
+#define FUSE_RENAME 12
+#define FUSE_OPEN 14
+#define FUSE_READ 15
+#define FUSE_WRITE 16
+#define FUSE_STATFS 17
+#define FUSE_RELEASE 18
+#define FUSE_FSYNC 20
+#define FUSE_INIT 26
+#define FUSE_OPENDIR 27
+#define FUSE_READDIR 28
+#define FUSE_RELEASEDIR 29
+#define FUSE_CREATE 35
+#define FUSE_DESTROY 38
+#define FUSE_LSEEK 46
+
+#define FUSE_ROOT_INODE 1
+
+struct fuse_in_header {
+ uint32_t len;
+ uint32_t opcode;
+ uint64_t unique;
+ uint64_t nodeid;
+ uint32_t uid;
+ uint32_t gid;
+ uint32_t pid;
+ uint16_t total_extlen;
+ uint16_t padding;
+};
+
+struct fuse_out_header {
+ uint32_t len;
+ int32_t error;
+ uint64_t unique;
+};
+
+struct fuse_init_in {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t max_readahead;
+ uint32_t flags;
+ uint32_t flags2;
+ uint32_t unused[11];
+};
+
+struct fuse_init_out {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t max_readahead;
+ uint32_t flags;
+ uint16_t max_background;
+ uint16_t congestion_threshold;
+ uint32_t max_write;
+ uint32_t time_gran;
+ uint16_t max_pages;
+ uint16_t map_alignment;
+ uint32_t flags2;
+ uint32_t max_stack_depth;
+ uint32_t unused[6];
+};
+
+struct fuse_open_in {
+ uint32_t flags;
+ uint32_t open_flags;
+};
+
+struct fuse_open_out {
+ uint64_t fh;
+ uint32_t open_flags;
+ int32_t backing_id;
+};
+
+struct fuse_attr {
+ uint64_t ino;
+ uint64_t size;
+ uint64_t blocks;
+ uint64_t atime;
+ uint64_t mtime;
+ uint64_t ctime;
+ uint32_t atimensec;
+ uint32_t mtimensec;
+ uint32_t ctimensec;
+ uint32_t mode;
+ uint32_t nlink;
+ uint32_t uid;
+ uint32_t gid;
+ uint32_t rdev;
+ uint32_t blksize;
+ uint32_t flags;
+};
+
+struct fuse_entry_out {
+ uint64_t nodeid;
+ uint64_t generation;
+ uint64_t entry_valid;
+ uint64_t attr_valid;
+ uint32_t entry_valid_nsec;
+ uint32_t attr_valid_nsec;
+ struct fuse_attr attr;
+};
+
+struct fuse_read_in {
+ uint64_t fh;
+ uint64_t offset;
+ uint32_t size;
+ uint32_t read_flags;
+ uint64_t lock_owner;
+ uint32_t flags;
+ uint32_t padding;
+};
+
+struct fuse_release_in {
+ uint64_t fh;
+ uint32_t flags;
+ uint32_t release_flags;
+ uint64_t lock_owner;
+};
+
+struct fuse_create_in {
+ uint32_t flags;
+ uint32_t mode;
+ uint32_t umask;
+ uint32_t open_flags;
+};
+
+struct fuse_create_out {
+ struct fuse_entry_out entry_out;
+ struct fuse_open_out open_out;
+};
+
+struct fuse_write_in {
+ uint64_t fh;
+ uint64_t offset;
+ uint32_t size;
+ uint32_t write_flags;
+ uint64_t lock_owner;
+ uint32_t flags;
+ uint32_t padding;
+};
+
+struct fuse_write_out {
+ uint32_t size;
+ uint32_t padding;
+};
+
+struct fuse_lseek_in {
+ uint64_t fh;
+ uint64_t offset;
+ uint32_t whence;
+ uint32_t padding;
+};
+
+struct fuse_lseek_out {
+ uint64_t offset;
+};
+
+/* mask used to set file size, used in fuse_setattr_in::valid */
+#define FATTR_SIZE (1 << 3)
+
+struct fuse_setattr_in {
+ uint32_t valid;
+ uint32_t padding;
+ uint64_t fh;
+ uint64_t size;
+ uint64_t lock_owner;
+ uint64_t atime;
+ uint64_t mtime;
+ uint64_t ctime;
+ uint32_t atimensec;
+ uint32_t mtimensec;
+ uint32_t ctimensec;
+ uint32_t mode;
+ uint32_t unused4;
+ uint32_t uid;
+ uint32_t gid;
+ uint32_t unused5;
+};
+
+struct fuse_attr_out {
+ uint64_t attr_valid;
+ uint32_t attr_valid_nsec;
+ uint32_t dummy;
+ struct fuse_attr attr;
+};
+
+struct fuse_fsync_in {
+ uint64_t fh;
+ uint32_t fsync_flags;
+ uint32_t padding;
+};
+
+struct fuse_mkdir_in {
+ uint32_t mode;
+ uint32_t umask;
+};
+
+struct fuse_rename_in {
+ uint64_t newdir;
+};
+
+struct fuse_kstatfs {
+ uint64_t blocks;
+ uint64_t bfree;
+ uint64_t bavail;
+ uint64_t files;
+ uint64_t ffree;
+ uint32_t bsize;
+ uint32_t namelen;
+ uint32_t frsize;
+ uint32_t padding;
+ uint32_t spare[6];
+};
+
+struct fuse_dirent {
+ uint64_t ino;
+ uint64_t off;
+ uint32_t namelen;
+ uint32_t type;
+ char name[];
+};
+
+struct fuse_forget_in {
+ uint64_t nlookup;
+};
+
+#endif /* ZEPHYR_SUBSYS_FS_FUSE_ABI_H_ */
diff --git a/subsys/fs/fuse_client/fuse_client.c b/subsys/fs/fuse_client/fuse_client.c
new file mode 100644
index 0000000000000..b2fa93003bd72
--- /dev/null
+++ b/subsys/fs/fuse_client/fuse_client.c
@@ -0,0 +1,429 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+#include
+
+LOG_MODULE_REGISTER(fuse, CONFIG_FUSE_CLIENT_LOG_LEVEL);
+
+static uint64_t unique = 1; /* with unique==0 older virtiofsd asserts, so we are starting from 1 */
+
+static uint64_t fuse_get_unique(void)
+{
+ return unique++;
+}
+
+void fuse_fill_header(struct fuse_in_header *hdr, uint32_t len, uint32_t opcode, uint64_t nodeid)
+{
+ hdr->len = len;
+ hdr->opcode = opcode;
+ hdr->unique = fuse_get_unique();
+ hdr->nodeid = nodeid;
+ hdr->uid = CONFIG_FUSE_CLIENT_UID_VALUE;
+ hdr->gid = CONFIG_FUSE_CLIENT_GID_VALUE;
+ hdr->pid = CONFIG_FUSE_CLIENT_PID_VALUE;
+ hdr->total_extlen = 0;
+}
+
+void fuse_create_init_req(struct fuse_init_req *req)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(struct fuse_in_header) + sizeof(struct fuse_init_in),
+ FUSE_INIT, 0
+ );
+ req->init_in.major = FUSE_MAJOR_VERSION;
+ req->init_in.minor = FUSE_MINOR_VERSION;
+ req->init_in.max_readahead = 0;
+ req->init_in.flags = 0;
+ req->init_in.flags2 = 0;
+}
+
+void fuse_create_open_req(
+ struct fuse_open_req *req, uint64_t inode, uint32_t flags, enum fuse_object_type type)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(struct fuse_in_header) + sizeof(struct fuse_open_in),
+ type == FUSE_DIR ? FUSE_OPENDIR : FUSE_OPEN, inode
+ );
+ req->open_in.flags = flags;
+ req->open_in.open_flags = 0;
+}
+
+void fuse_create_lookup_req(struct fuse_lookup_req *req, uint64_t inode, uint32_t fname_len)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(struct fuse_in_header) + fname_len, FUSE_LOOKUP,
+ inode
+ );
+}
+
+void fuse_create_read_req(
+ struct fuse_read_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t size,
+ enum fuse_object_type type)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(struct fuse_in_header) + sizeof(struct fuse_read_in),
+ type == FUSE_FILE ? FUSE_READ : FUSE_READDIR, inode
+ );
+ req->read_in.fh = fh;
+ req->read_in.offset = offset;
+ req->read_in.size = size;
+ req->read_in.read_flags = 0;
+ req->read_in.lock_owner = 0;
+ req->read_in.flags = 0;
+}
+
+void fuse_create_release_req(struct fuse_release_req *req, uint64_t inode, uint64_t fh,
+ enum fuse_object_type type)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(struct fuse_in_header) + sizeof(struct fuse_release_in),
+ type == FUSE_DIR ? FUSE_RELEASEDIR : FUSE_RELEASE, inode
+ );
+ req->release_in.fh = fh;
+ req->release_in.flags = 0;
+ req->release_in.release_flags = 0;
+ req->release_in.lock_owner = 0;
+}
+
+void fuse_create_destroy_req(struct fuse_destroy_req *req)
+{
+ fuse_fill_header(&req->in_header, sizeof(struct fuse_in_header), FUSE_DESTROY, 0);
+}
+
+void fuse_create_create_req(
+ struct fuse_create_req *req, uint64_t inode, uint32_t fname_len, uint32_t flags,
+ uint32_t mode)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(req->create_in) + fname_len,
+ FUSE_CREATE, inode
+ );
+ req->create_in.flags = flags;
+ req->create_in.mode = mode;
+ req->create_in.open_flags = 0;
+ req->create_in.umask = 0;
+}
+
+void fuse_create_write_req(
+ struct fuse_write_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t size)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(req->write_in) + size, FUSE_WRITE,
+ inode
+ );
+ req->write_in.fh = fh;
+ req->write_in.offset = offset;
+ req->write_in.size = size;
+ req->write_in.write_flags = 0;
+ req->write_in.lock_owner = 0;
+ req->write_in.flags = 0;
+}
+
+void fuse_create_lseek_req(
+ struct fuse_lseek_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t whence)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(req->lseek_in), FUSE_LSEEK, inode
+ );
+ req->lseek_in.fh = fh;
+ req->lseek_in.offset = offset;
+ req->lseek_in.whence = whence;
+}
+
+void fuse_create_setattr_req(struct fuse_setattr_req *req, uint64_t inode)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(struct fuse_setattr_in),
+ FUSE_SETATTR, inode
+ );
+}
+
+void fuse_create_fsync_req(struct fuse_fsync_req *req, uint64_t inode, uint64_t fh)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(req->fsync_in), FUSE_FSYNC,
+ inode
+ );
+ req->fsync_in.fh = fh;
+ req->fsync_in.fsync_flags = 0;
+}
+
+void fuse_create_mkdir_req(
+ struct fuse_mkdir_req *req, uint64_t inode, uint32_t dirname_len, uint32_t mode)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + sizeof(req->mkdir_in) + dirname_len,
+ FUSE_MKDIR, inode
+ );
+
+ req->mkdir_in.mode = mode;
+ req->mkdir_in.umask = 0;
+}
+
+void fuse_create_unlink_req(
+ struct fuse_unlink_req *req, uint32_t fname_len, enum fuse_object_type type)
+{
+ fuse_fill_header(
+ &req->in_header, sizeof(req->in_header) + fname_len,
+ type == FUSE_DIR ? FUSE_RMDIR : FUSE_UNLINK, FUSE_ROOT_INODE
+ );
+}
+
+void fuse_create_rename_req(
+ struct fuse_rename_req *req, uint64_t old_dir_nodeid, uint32_t old_len,
+ uint64_t new_dir_nodeid, uint32_t new_len)
+{
+ fuse_fill_header(
+ &req->in_header,
+ sizeof(req->in_header) + sizeof(req->rename_in) + old_len + new_len,
+ FUSE_RENAME, old_dir_nodeid
+ );
+ req->rename_in.newdir = new_dir_nodeid;
+}
+
+const char *fuse_opcode_to_string(uint32_t opcode)
+{
+ switch (opcode) {
+ case FUSE_LOOKUP:
+ return "FUSE_LOOKUP";
+ case FUSE_FORGET:
+ return "FUSE_FORGET";
+ case FUSE_SETATTR:
+ return "FUSE_SETATTR";
+ case FUSE_MKDIR:
+ return "FUSE_MKDIR";
+ case FUSE_UNLINK:
+ return "FUSE_UNLINK";
+ case FUSE_RMDIR:
+ return "FUSE_RMDIR";
+ case FUSE_RENAME:
+ return "FUSE_RENAME";
+ case FUSE_OPEN:
+ return "FUSE_OPEN";
+ case FUSE_READ:
+ return "FUSE_READ";
+ case FUSE_WRITE:
+ return "FUSE_WRITE";
+ case FUSE_STATFS:
+ return "FUSE_STATFS";
+ case FUSE_RELEASE:
+ return "FUSE_RELEASE";
+ case FUSE_FSYNC:
+ return "FUSE_FSYNC";
+ case FUSE_INIT:
+ return "FUSE_INIT";
+ case FUSE_OPENDIR:
+ return "FUSE_OPENDIR";
+ case FUSE_READDIR:
+ return "FUSE_READDIR";
+ case FUSE_RELEASEDIR:
+ return "FUSE_RELEASEDIR";
+ case FUSE_CREATE:
+ return "FUSE_CREATE";
+ case FUSE_DESTROY:
+ return "FUSE_DESTROY";
+ case FUSE_LSEEK:
+ return "FUSE_LSEEK";
+ default:
+ return "";
+ }
+}
+
+void fuse_dump_init_req_out(struct fuse_init_req *req)
+{
+ LOG_INF(
+ "FUSE_INIT response:\n"
+ "major=%" PRIu32 "\n"
+ "minor=%" PRIu32 "\n"
+ "max_readahead=%" PRIu32 "\n"
+ "flags=%" PRIu32 "\n"
+ "max_background=%" PRIu16 "\n"
+ "congestion_threshold=%" PRIu16 "\n"
+ "max_write=%" PRIu32 "\n"
+ "time_gran=%" PRIu32 "\n"
+ "max_pages=%" PRIu16 "\n"
+ "map_alignment=%" PRIu16 "\n"
+ "flags2=%" PRIu32 "\n"
+ "max_stack_depth=%" PRIu32,
+ req->init_out.major,
+ req->init_out.minor,
+ req->init_out.max_readahead,
+ req->init_out.flags,
+ req->init_out.max_background,
+ req->init_out.congestion_threshold,
+ req->init_out.max_write,
+ req->init_out.time_gran,
+ req->init_out.max_pages,
+ req->init_out.map_alignment,
+ req->init_out.flags2,
+ req->init_out.max_stack_depth
+ );
+}
+
+void fuse_dump_entry_out(struct fuse_entry_out *eo)
+{
+ LOG_INF(
+ "FUSE LOOKUP response:\n"
+ "nodeid=%" PRIu64 "\n"
+ "generation=%" PRIu64 "\n"
+ "entry_valid=%" PRIu64 "\n"
+ "attr_valid=%" PRIu64 "\n"
+ "entry_valid_nsec=%" PRIu32 "\n"
+ "attr_valid_nsec=%" PRIu32 "\n"
+ "attr.ino=%" PRIu64 "\n"
+ "attr.size=%" PRIu64 "\n"
+ "attr.blocks=%" PRIu64 "\n"
+ "attr.atime=%" PRIu64 "\n"
+ "attr.mtime=%" PRIu64 "\n"
+ "attr.ctime=%" PRIu64 "\n"
+ "attr.atimensec=%" PRIu32 "\n"
+ "attr.mtimensec=%" PRIu32 "\n"
+ "attr.ctimensec=%" PRIu32 "\n"
+ "attr.mode=%" PRIu32 "\n"
+ "attr.nlink=%" PRIu32 "\n"
+ "attr.uid=%" PRIu32 "\n"
+ "attr.gid=%" PRIu32 "\n"
+ "attr.rdev=%" PRIu32 "\n"
+ "attr.blksize=%" PRIu32 "\n"
+ "attr.flags=%" PRIu32,
+ eo->nodeid,
+ eo->generation,
+ eo->entry_valid,
+ eo->attr_valid,
+ eo->entry_valid_nsec,
+ eo->attr_valid_nsec,
+ eo->attr.ino,
+ eo->attr.size,
+ eo->attr.blocks,
+ eo->attr.atime,
+ eo->attr.mtime,
+ eo->attr.ctime,
+ eo->attr.atimensec,
+ eo->attr.mtimensec,
+ eo->attr.ctimensec,
+ eo->attr.mode,
+ eo->attr.nlink,
+ eo->attr.uid,
+ eo->attr.gid,
+ eo->attr.rdev,
+ eo->attr.blksize,
+ eo->attr.flags
+ );
+}
+
+void fuse_dump_open_req_out(struct fuse_open_req *req)
+{
+ LOG_INF(
+ "FUSE OPEN response:\n"
+ "fh=%" PRIu64 "\n"
+ "open_flags=%" PRIu32 "\n"
+ "backing_id=%" PRIi32,
+ req->open_out.fh,
+ req->open_out.open_flags,
+ req->open_out.backing_id
+ );
+}
+
+void fuse_dump_create_req_out(struct fuse_create_out *req)
+{
+ LOG_INF(
+ "FUSE CREATE response:\n"
+ "nodeid=%" PRIu64 "\n"
+ "generation=%" PRIu64 "\n"
+ "entry_valid=%" PRIu64 "\n"
+ "attr_valid=%" PRIu64 "\n"
+ "entry_valid_nsec=%" PRIu32 "\n"
+ "attr_valid_nsec=%" PRIu32 "\n"
+ "attr.ino=%" PRIu64 "\n"
+ "attr.size=%" PRIu64 "\n"
+ "attr.blocks=%" PRIu64 "\n"
+ "attr.atime=%" PRIu64 "\n"
+ "attr.mtime=%" PRIu64 "\n"
+ "attr.ctime=%" PRIu64 "\n"
+ "attr.atimensec=%" PRIu32 "\n"
+ "attr.mtimensec=%" PRIu32 "\n"
+ "attr.ctimensec=%" PRIu32 "\n"
+ "attr.mode=%" PRIu32 "\n"
+ "attr.nlink=%" PRIu32 "\n"
+ "attr.uid=%" PRIu32 "\n"
+ "attr.gid=%" PRIu32 "\n"
+ "attr.rdev=%" PRIu32 "\n"
+ "attr.blksize=%" PRIu32 "\n"
+ "attr.flags=%" PRIu32 "\n"
+ "fh=%" PRIu64 "\n"
+ "open_flags=%" PRIu32 "\n"
+ "backing_id=%" PRIi32,
+ req->entry_out.nodeid,
+ req->entry_out.generation,
+ req->entry_out.entry_valid,
+ req->entry_out.attr_valid,
+ req->entry_out.entry_valid_nsec,
+ req->entry_out.attr_valid_nsec,
+ req->entry_out.attr.ino,
+ req->entry_out.attr.size,
+ req->entry_out.attr.blocks,
+ req->entry_out.attr.atime,
+ req->entry_out.attr.mtime,
+ req->entry_out.attr.ctime,
+ req->entry_out.attr.atimensec,
+ req->entry_out.attr.mtimensec,
+ req->entry_out.attr.ctimensec,
+ req->entry_out.attr.mode,
+ req->entry_out.attr.nlink,
+ req->entry_out.attr.uid,
+ req->entry_out.attr.gid,
+ req->entry_out.attr.rdev,
+ req->entry_out.attr.blksize,
+ req->entry_out.attr.flags,
+ req->open_out.fh,
+ req->open_out.open_flags,
+ req->open_out.backing_id
+ );
+}
+
+void fuse_dump_write_out(struct fuse_write_out *wo)
+{
+ LOG_INF("FUSE WRITE response:\nsize=%" PRIu32, wo->size);
+}
+
+void fuse_dump_lseek_out(struct fuse_lseek_out *lo)
+{
+ LOG_INF("FUSE WRITE response:\noffset=%" PRIu64, lo->offset);
+}
+
+void fuse_dump_attr_out(struct fuse_attr_out *ao)
+{
+ LOG_INF(
+ "attr_valid=%" PRIu64 "\n"
+ "attr_valid_nsec=%" PRIu32,
+ ao->attr_valid,
+ ao->attr_valid_nsec
+ );
+}
+
+void fuse_dump_kstafs(struct fuse_kstatfs *ks)
+{
+ LOG_INF(
+ "blocks=%" PRIu64 "\n"
+ "bfree=%" PRIu64 "\n"
+ "bavail=%" PRIu64 "\n"
+ "files=%" PRIu64 "\n"
+ "ffree=%" PRIu64 "\n"
+ "bsize=%" PRIu32 "\n"
+ "namelen=%" PRIu32 "\n"
+ "frsize=%" PRIu32,
+ ks->blocks,
+ ks->bfree,
+ ks->bavail,
+ ks->files,
+ ks->ffree,
+ ks->bsize,
+ ks->namelen,
+ ks->frsize
+ );
+}
diff --git a/subsys/fs/fuse_client/fuse_client.h b/subsys/fs/fuse_client/fuse_client.h
new file mode 100644
index 0000000000000..928a01e6de441
--- /dev/null
+++ b/subsys/fs/fuse_client/fuse_client.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * This header provides helper functions to pack FUSE client requests. Note the client is the
+ * side which initiates these requests, and that in a typical FUSE usage the client would be
+ * the Linux kernel. While in Zephyr's case this is to enable functionality like the embedded
+ * virtiofs client connecting to a virtiofsd daemon running in the host.
+ */
+
+#ifndef ZEPHYR_SUBSYS_FS_FUSE_CLIENT_H_
+#define ZEPHYR_SUBSYS_FS_FUSE_CLIENT_H_
+#include
+#include "fuse_abi.h"
+
+/*
+ * requests are put into structs to leverage the fact that they are contiguous in memory and can
+ * be passed to virtqueue as smaller amount of buffers, e.g. in_header + init_in can be sent as
+ * a single buffer containing both of them instead of two separate buffers
+ */
+
+struct fuse_init_req {
+ struct fuse_in_header in_header;
+ struct fuse_init_in init_in;
+ struct fuse_out_header out_header;
+ struct fuse_init_out init_out;
+};
+
+struct fuse_open_req {
+ struct fuse_in_header in_header;
+ struct fuse_open_in open_in;
+ struct fuse_out_header out_header;
+ struct fuse_open_out open_out;
+};
+
+struct fuse_create_req {
+ struct fuse_in_header in_header;
+ struct fuse_create_in create_in;
+ struct fuse_out_header out_header;
+ struct fuse_create_out create_out;
+};
+
+struct fuse_write_req {
+ struct fuse_in_header in_header;
+ struct fuse_write_in write_in;
+ struct fuse_out_header out_header;
+ struct fuse_write_out write_out;
+};
+
+struct fuse_lseek_req {
+ struct fuse_in_header in_header;
+ struct fuse_lseek_in lseek_in;
+ struct fuse_out_header out_header;
+ struct fuse_lseek_out lseek_out;
+};
+
+struct fuse_mkdir_req {
+ struct fuse_in_header in_header;
+ struct fuse_mkdir_in mkdir_in;
+ struct fuse_out_header out_header;
+ struct fuse_entry_out entry_out;
+};
+
+struct fuse_lookup_req {
+ struct fuse_in_header in_header;
+ struct fuse_out_header out_header;
+ struct fuse_entry_out entry_out;
+};
+
+struct fuse_read_req {
+ struct fuse_in_header in_header;
+ struct fuse_read_in read_in;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_release_req {
+ struct fuse_in_header in_header;
+ struct fuse_release_in release_in;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_destroy_req {
+ struct fuse_in_header in_header;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_setattr_req {
+ struct fuse_in_header in_header;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_fsync_req {
+ struct fuse_in_header in_header;
+ struct fuse_fsync_in fsync_in;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_unlink_req {
+ struct fuse_in_header in_header;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_rename_req {
+ struct fuse_in_header in_header;
+ struct fuse_rename_in rename_in;
+ struct fuse_out_header out_header;
+};
+
+struct fuse_kstatfs_req {
+ struct fuse_in_header in_header;
+ struct fuse_out_header out_header;
+ struct fuse_kstatfs kstatfs_out;
+};
+
+struct fuse_forget_req {
+ struct fuse_in_header in_header;
+ struct fuse_forget_in forget_in;
+};
+
+enum fuse_object_type {
+ FUSE_FILE,
+ FUSE_DIR
+};
+
+void fuse_fill_header(struct fuse_in_header *hdr, uint32_t len, uint32_t opcode, uint64_t nodeid);
+
+void fuse_create_init_req(struct fuse_init_req *req);
+void fuse_create_open_req(struct fuse_open_req *req, uint64_t inode, uint32_t flags,
+ enum fuse_object_type type);
+void fuse_create_lookup_req(struct fuse_lookup_req *req, uint64_t inode, uint32_t fname_len);
+void fuse_create_read_req(
+ struct fuse_read_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t size,
+ enum fuse_object_type type);
+void fuse_create_release_req(struct fuse_release_req *req, uint64_t inode, uint64_t fh,
+ enum fuse_object_type type);
+void fuse_create_destroy_req(struct fuse_destroy_req *req);
+void fuse_create_create_req(
+ struct fuse_create_req *req, uint64_t inode, uint32_t fname_len, uint32_t flags,
+ uint32_t mode);
+void fuse_create_write_req(
+ struct fuse_write_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t size);
+void fuse_create_lseek_req(
+ struct fuse_lseek_req *req, uint64_t inode, uint64_t fh, uint64_t offset, uint32_t whence);
+void fuse_create_setattr_req(struct fuse_setattr_req *req, uint64_t inode);
+void fuse_create_fsync_req(struct fuse_fsync_req *req, uint64_t inode, uint64_t fh);
+void fuse_create_mkdir_req(
+ struct fuse_mkdir_req *req, uint64_t inode, uint32_t dirname_len, uint32_t mode);
+void fuse_create_unlink_req(
+ struct fuse_unlink_req *req, uint32_t fname_len, enum fuse_object_type type);
+void fuse_create_rename_req(
+ struct fuse_rename_req *req, uint64_t old_dir_nodeid, uint32_t old_len,
+ uint64_t new_dir_nodeid, uint32_t new_len);
+
+const char *fuse_opcode_to_string(uint32_t opcode);
+
+void fuse_dump_init_req_out(struct fuse_init_req *req);
+void fuse_dump_entry_out(struct fuse_entry_out *eo);
+void fuse_dump_open_req_out(struct fuse_open_req *req);
+void fuse_dump_create_req_out(struct fuse_create_out *req);
+void fuse_dump_write_out(struct fuse_write_out *wo);
+void fuse_dump_lseek_out(struct fuse_lseek_out *lo);
+void fuse_dump_attr_out(struct fuse_attr_out *ao);
+void fuse_dump_kstafs(struct fuse_kstatfs *ks);
+
+#endif /* ZEPHYR_SUBSYS_FS_FUSE_CLIENT_H_ */
diff --git a/subsys/fs/virtiofs/CMakeLists.txt b/subsys/fs/virtiofs/CMakeLists.txt
new file mode 100644
index 0000000000000..d107ac82f8efb
--- /dev/null
+++ b/subsys/fs/virtiofs/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Copyright (c) 2025 Antmicro
+# SPDX-License-Identifier: Apache-2.0
+
+add_library(VIRTIOFS INTERFACE)
+target_include_directories(VIRTIOFS INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
+
+zephyr_library()
+zephyr_library_sources(
+ virtiofs.c
+ virtiofs_zfs.c
+)
+
+zephyr_library_link_libraries(VIRTIOFS)
diff --git a/subsys/fs/virtiofs/Kconfig b/subsys/fs/virtiofs/Kconfig
new file mode 100644
index 0000000000000..fed3f482478fb
--- /dev/null
+++ b/subsys/fs/virtiofs/Kconfig
@@ -0,0 +1,52 @@
+# Copyright (c) 2025 Antmicro
+# SPDX-License-Identifier: Apache-2.0
+
+config FILE_SYSTEM_VIRTIOFS
+ bool "Virtiofs file system support"
+ depends on FILE_SYSTEM
+ select FUSE_CLIENT
+ help
+ Enable virtiofs file system support.
+
+config VIRTIOFS_DEBUG
+ bool "Print virtiofs verbose debug information"
+ help
+ Enables printing of virtiofs verbose debug information
+
+config VIRTIOFS_MAX_FILES
+ int "Virtiofs max open files"
+ default 1024
+ help
+ Virtiofs max simultaneously open files
+
+config VIRTIOFS_MAX_VQUEUE_SIZE
+ int "Virtiofs max virtqueue size"
+ default 1024
+ help
+ Maximum size of virtqueue
+
+config VIRTIOFS_NO_NOTIFICATION_QUEUE_SLOT
+ bool "Omit notification queue (idx 1) and assume that idx 1 is the first request queue"
+ default y
+ help
+ According to virtio specification v1.3 section 5.11.2 queue at idx 1 is notification queue and
+ request queues start at idx 2, however on qemu+virtiofsd thats not true and idx 1 is
+ the first request queue
+
+config VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
+ bool "Fix unlink() with some virtiofsd versions"
+ default y
+ help
+ Some virtiofsd versions (at least Debian 1:7.2+dfsg-7+deb12u7)
+ will fail with EIO on unlink if the file wasn't looked up before
+
+config VIRTIOFS_CREATE_MODE_VALUE
+ int "Virtiofs mode value used during file/directory creation"
+ default 438 #0666
+ help
+ During creation of file or directory we have to set mode, this config allows
+ configuring that value. This determines access permissions for created files and directories.
+
+module = VIRTIOFS
+module-str = virtiofs
+source "subsys/logging/Kconfig.template.log_config"
diff --git a/subsys/fs/virtiofs/virtiofs.c b/subsys/fs/virtiofs/virtiofs.c
new file mode 100644
index 0000000000000..ad594ed0c6442
--- /dev/null
+++ b/subsys/fs/virtiofs/virtiofs.c
@@ -0,0 +1,778 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+#include
+#include "virtiofs.h"
+#include
+
+LOG_MODULE_REGISTER(virtiofs, CONFIG_VIRTIOFS_LOG_LEVEL);
+
+/*
+ * According to 5.11.2 of virtio specification v1.3 the virtiofs queues are indexed as
+ * follows:
+ * - idx 0 - hiprio
+ * - idx 1 - notification queue
+ * - idx 2..n - request queues
+ * notification queue is available only if VIRTIO_FS_F_NOTIFICATION is present and
+ * there is no mention that in its absence the request queues will be shifted and start
+ * at idx 1, so the request queues shall start at idx 2. However in case of qemu+virtiofsd
+ * who don't support VIRTIO_FS_F_NOTIFICATION, the last available queue is at idx 1 and
+ * virtio_fs_config.num_request_queues states that there is a single request queue present
+ * which must be the one at idx 1
+ */
+#ifdef CONFIG_VIRTIOFS_NO_NOTIFICATION_QUEUE_SLOT
+#define REQUEST_QUEUE 1
+#else
+#define REQUEST_QUEUE 2
+#endif
+
+
+struct virtio_fs_config {
+ char tag[36];
+ uint32_t num_request_queues;
+};
+
+static int virtiofs_validate_response(
+ const struct fuse_out_header *header, uint32_t opcode, uint32_t used_len,
+ uint32_t expected_len)
+{
+ if (used_len < sizeof(*header)) {
+ LOG_ERR("used length is smaller than size of fuse_out_header");
+ return -EIO;
+ }
+
+ if (header->error != 0) {
+ LOG_ERR(
+ "%s error %d (%s)",
+ fuse_opcode_to_string(opcode),
+ -header->error,
+ strerror(-header->error)
+ );
+ return header->error;
+ }
+
+ if (expected_len != -1 && header->len != expected_len) {
+ LOG_ERR(
+ "%s return message has invalid length (0x%x), expected 0x%x",
+ fuse_opcode_to_string(opcode),
+ header->len,
+ expected_len
+ );
+ return -EIO;
+ }
+
+ return 0;
+}
+
+struct recv_cb_param {
+ struct k_sem sem;
+ uint32_t used_len;
+};
+
+void virtiofs_recv_cb(void *opaque, uint32_t used_len)
+{
+ struct recv_cb_param *arg = opaque;
+
+ arg->used_len = used_len;
+ k_sem_give(&arg->sem);
+}
+
+static uint32_t virtiofs_send_receive(
+ const struct device *dev, uint16_t virtq, struct virtq_buf *bufs,
+ uint16_t bufs_size, uint16_t device_readable)
+{
+ struct virtq *virtqueue = virtio_get_virtqueue(dev, virtq);
+ struct recv_cb_param cb_arg;
+
+ k_sem_init(&cb_arg.sem, 0, 1);
+
+ virtq_add_buffer_chain(
+ virtqueue, bufs, bufs_size, device_readable, virtiofs_recv_cb, &cb_arg,
+ K_FOREVER
+ );
+ virtio_notify_virtqueue(dev, virtq);
+
+ k_sem_take(&cb_arg.sem, K_FOREVER);
+
+ return cb_arg.used_len;
+}
+
+static uint16_t virtiofs_queue_enum_cb(uint16_t queue_idx, uint16_t max_size, void *unused)
+{
+ if (queue_idx == REQUEST_QUEUE) {
+ return MIN(CONFIG_VIRTIOFS_MAX_VQUEUE_SIZE, max_size);
+ } else {
+ return 0;
+ }
+}
+
+int virtiofs_init(const struct device *dev, struct fuse_init_out *response)
+{
+ struct virtio_fs_config *fs_config = virtio_get_device_specific_config(dev);
+ struct fuse_init_req req;
+ int ret = 0;
+
+ if (!fs_config) {
+ LOG_ERR("no virtio_fs_config present");
+ return -ENXIO;
+ }
+ if (fs_config->num_request_queues < 1) {
+ /* this shouldn't ever happen */
+ LOG_ERR("no request queue present");
+ return -ENODEV;
+ }
+
+ ret = virtio_commit_feature_bits(dev);
+ if (ret != 0) {
+ return ret;
+ }
+
+ ret = virtio_init_virtqueues(dev, REQUEST_QUEUE, virtiofs_queue_enum_cb, NULL);
+ if (ret != 0) {
+ LOG_ERR("failed to initialize fs virtqueues");
+ return ret;
+ }
+
+ virtio_finalize_init(dev);
+
+ fuse_create_init_req(&req);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) + sizeof(req.init_in) },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.init_out) }
+ };
+
+ LOG_INF("sending FUSE_INIT, unique=%" PRIu64, req.in_header.unique);
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF("received FUSE_INIT response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_INIT, used_len, buf[1].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ if (req.init_out.major != FUSE_MAJOR_VERSION) {
+ LOG_ERR(
+ "FUSE_INIT major version mismatch (%d), version %d is supported",
+ req.init_out.major,
+ FUSE_MAJOR_VERSION
+ );
+ return -ENOTSUP;
+ }
+
+ if (req.init_out.minor < FUSE_MINOR_VERSION) {
+ LOG_ERR(
+ "FUSE_INIT minor version is too low (%d), version %d is supported",
+ req.init_out.minor,
+ FUSE_MINOR_VERSION
+ );
+ return -ENOTSUP;
+ }
+
+ *response = req.init_out;
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_init_req_out(&req.init_out);
+#endif
+
+ return 0;
+}
+
+/**
+ * @brief lookups object in the virtiofs filesystem
+ *
+ * @param dev virtio device its used on
+ * @param inode inode to start from
+ * @param path path to object we are looking for
+ * @param response virtiofs response for object
+ * @param parent_inode will be set to immediate parent inode of object that we are looking for.
+ * If immediate parent doesn't exist it will be set to 0. If not 0 it has to be FUSE_FORGET by
+ * caller. Can be NULL.
+ * @return 0 or error code on failure
+ */
+int virtiofs_lookup(
+ const struct device *dev, uint64_t inode, const char *path, struct fuse_entry_out *response,
+ uint64_t *parent_inode)
+{
+ uint32_t path_len = strlen(path) + 1;
+ const char *curr = path;
+ uint32_t curr_len = 0;
+ uint64_t curr_inode = inode;
+ struct fuse_lookup_req req;
+
+ /*
+ * we have to split path and lookup it dir by dir, because FUSE_LOOKUP doesn't work with
+ * full paths like abc/xyz/file. We have to lookup abc, then lookup xyz with abc's inode
+ * as a base and then lookup file with xyz's inode as a base
+ */
+ while (curr < path + path_len) {
+ curr_len = 0;
+ for (const char *c = curr; c < path + path_len - 1 && *c != '/'; c++) {
+ curr_len++;
+ }
+
+ fuse_create_lookup_req(&req, curr_inode, curr_len + 1);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(struct fuse_in_header) },
+ { .addr = (void *)curr, .len = curr_len },
+ /*
+ * despite length being part of in_header this still has to be null
+ * terminated
+ */
+ { .addr = "", .len = 1},
+ { .addr = &req.out_header,
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out) }
+ };
+
+ LOG_INF(
+ "sending FUSE_LOOKUP for \"%s\", nodeid=%" PRIu64 ", unique=%" PRIu64,
+ curr, curr_inode, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 4, 3);
+
+ LOG_INF("received FUSE_LOOKUP response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_LOOKUP, used_len,
+ sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out)
+ );
+
+ if (parent_inode) {
+ *parent_inode = curr_inode;
+ }
+
+ *response = req.entry_out;
+ if (valid_ret != 0) {
+ if (parent_inode && (curr + curr_len + 1 != path + path_len)) {
+ /* there is no immediate parent */
+ if (*parent_inode != inode) {
+ virtiofs_forget(dev, *parent_inode, 1);
+ }
+ *parent_inode = 0;
+ }
+ return valid_ret;
+ }
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_entry_out(&req.entry_out);
+#endif
+ bool is_curr_parent = true;
+
+ for (const char *c = curr; c < path + path_len; c++) {
+ if (*c == '/') {
+ is_curr_parent = false;
+ }
+ }
+
+ /*
+ * unless its inode param passed to this function or a parent of object we
+ * are looking for, curr_inode won't be used anymore so we can forget it
+ */
+ if (curr_inode != inode && (!parent_inode || !is_curr_parent)) {
+ virtiofs_forget(dev, curr_inode, 1);
+ }
+
+ curr_inode = req.entry_out.nodeid;
+ curr += curr_len + 1;
+ }
+
+ return 0;
+}
+
+int virtiofs_open(
+ const struct device *dev, uint64_t inode, uint32_t flags, struct fuse_open_out *response,
+ enum fuse_object_type type)
+{
+ struct fuse_open_req req;
+
+ fuse_create_open_req(&req, inode, flags, type);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = req.in_header.len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.open_out) }
+ };
+
+ LOG_INF(
+ "sending %s, nodeid=%" PRIu64 ", flags=0%" PRIo32 ", unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_OPENDIR" : "FUSE_OPEN", inode, flags, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF(
+ "received %s response, unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_OPENDIR" : "FUSE_OPEN", req.out_header.unique
+ );
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, type == FUSE_DIR ? FUSE_OPENDIR : FUSE_OPEN, used_len, buf[1].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ *response = req.open_out;
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_open_req_out(&req.open_out);
+#endif
+
+ return 0;
+}
+
+int virtiofs_read(
+ const struct device *dev, uint64_t inode, uint64_t fh,
+ uint64_t offset, uint32_t size, uint8_t *readbuf)
+{
+ struct fuse_read_req req;
+
+ fuse_create_read_req(&req, inode, fh, offset, size, FUSE_FILE);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = req.in_header.len },
+ { .addr = &req.out_header, .len = sizeof(struct fuse_out_header) },
+ { .addr = readbuf, .len = size }
+ };
+
+ LOG_INF(
+ "sending FUSE_READ, nodeid=%" PRIu64 ", fh=%" PRIu64 ", offset=%" PRIu64
+ ", size=%" PRIu32 ", unique=%" PRIu64,
+ inode, fh, offset, size, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 3, 1);
+
+ LOG_INF("received FUSE_READ response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(&req.out_header, FUSE_READ, used_len, -1);
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ return req.out_header.len - sizeof(req.out_header);
+}
+
+int virtiofs_release(const struct device *dev, uint64_t inode, uint64_t fh,
+ enum fuse_object_type type)
+{
+ struct fuse_release_req req;
+
+ fuse_create_release_req(&req, inode, fh, type);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = req.in_header.len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) }
+ };
+
+ LOG_INF(
+ "sending %s, inode=%" PRIu64 ", fh=%" PRIu64 ", unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_RELEASEDIR" : "FUSE_RELEASE", inode, fh,
+ req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF(
+ "received %s response, unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_RELEASEDIR" : "FUSE_RELEASE", req.out_header.unique
+ );
+
+ return virtiofs_validate_response(
+ &req.out_header, type == FUSE_DIR ? FUSE_RELEASEDIR : FUSE_RELEASE, used_len, -1
+ );
+}
+
+int virtiofs_destroy(const struct device *dev)
+{
+ struct fuse_destroy_req req;
+
+ fuse_create_destroy_req(&req);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) }
+ };
+
+ LOG_INF("sending FUSE_DESTROY, unique=%" PRIu64, req.in_header.unique);
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF("received FUSE_DESTROY response, unique=%" PRIu64, req.in_header.unique);
+
+ return virtiofs_validate_response(&req.out_header, FUSE_DESTROY, used_len, -1);
+}
+
+int virtiofs_create(
+ const struct device *dev, uint64_t inode, const char *fname, uint32_t flags,
+ uint32_t mode, struct fuse_create_out *response)
+{
+ uint32_t fname_len = strlen(fname) + 1;
+ struct fuse_create_req req;
+
+ fuse_create_create_req(&req, inode, fname_len, flags, mode);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) + sizeof(req.create_in) },
+ { .addr = (void *)fname, .len = fname_len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.create_out) }
+ };
+
+ LOG_INF(
+ "sending FUSE_CREATE for \"%s\", nodeid=%" PRIu64 ", flags=0%" PRIo32
+ ", unique=%" PRIu64,
+ fname, inode, flags, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 3, 2);
+
+ LOG_INF("received FUSE_CREATE response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_CREATE, used_len, buf[2].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ *response = req.create_out;
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_create_req_out(&req.create_out);
+#endif
+
+ return 0;
+}
+
+int virtiofs_write(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint32_t size, const uint8_t *write_buf)
+{
+ struct fuse_write_req req;
+
+ fuse_create_write_req(&req, inode, fh, offset, size);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) + sizeof(req.write_in) },
+ { .addr = (void *)write_buf, .len = size },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.write_out) }
+ };
+
+ LOG_INF(
+ "sending FUSE_WRITE, nodeid=%" PRIu64", fh=%" PRIu64 ", offset=%" PRIu64
+ ", size=%" PRIu32 ", unique=%" PRIu64,
+ inode, fh, offset, size, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 3, 2);
+
+ LOG_INF("received FUSE_WRITE response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_WRITE, used_len, buf[2].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_write_out(&req.write_out);
+#endif
+
+ return req.write_out.size;
+}
+
+int virtiofs_lseek(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint32_t whence, struct fuse_lseek_out *response)
+{
+ struct fuse_lseek_req req;
+
+ fuse_create_lseek_req(&req, inode, fh, offset, whence);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = req.in_header.len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.lseek_out) }
+ };
+
+ LOG_INF(
+ "sending FUSE_LSEEK, nodeid=%" PRIu64 ", fh=%" PRIu64 ", offset=%" PRIu64
+ ", whence=%" PRIu32 ", unique=%" PRIu64,
+ inode, fh, offset, whence, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF("received FUSE_LSEEK response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_LSEEK, used_len, buf[1].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ *response = req.lseek_out;
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_lseek_out(&req.lseek_out);
+#endif
+
+ return 0;
+}
+
+int virtiofs_setattr(
+ const struct device *dev, uint64_t inode, struct fuse_setattr_in *in,
+ struct fuse_attr_out *response)
+{
+ struct fuse_setattr_req req;
+
+ fuse_create_setattr_req(&req, inode);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) },
+ { .addr = in, .len = sizeof(*in) },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) },
+ { .addr = response, .len = sizeof(*response) },
+ };
+
+ LOG_INF("sending FUSE_SETATTR, unique=%" PRIu64, req.in_header.unique);
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 4, 2);
+
+ LOG_INF("received FUSE_SETATTR response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_SETATTR, used_len, sizeof(req.out_header) + sizeof(*response)
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_attr_out(response);
+#endif
+
+ return 0;
+}
+
+int virtiofs_fsync(const struct device *dev, uint64_t inode, uint64_t fh)
+{
+ struct fuse_fsync_req req;
+
+ fuse_create_fsync_req(&req, inode, fh);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header,
+ .len = sizeof(req.in_header) + sizeof(req.fsync_in) },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) }
+ };
+
+ LOG_INF(
+ "sending FUSE_FSYNC, nodeid=%" PRIu64 ", fh=%" PRIu64 ", unique=%" PRIu64,
+ inode, fh, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF("received FUSE_FSYNC response, unique=%" PRIu64, req.out_header.unique);
+
+ return virtiofs_validate_response(
+ &req.out_header, FUSE_FSYNC, used_len, sizeof(req.out_header)
+ );
+}
+
+int virtiofs_mkdir(const struct device *dev, uint64_t inode, const char *dirname, uint32_t mode)
+{
+ struct fuse_mkdir_req req;
+ uint32_t dirname_len = strlen(dirname) + 1;
+
+ fuse_create_mkdir_req(&req, inode, dirname_len, mode);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) + sizeof(req.mkdir_in) },
+ { .addr = (void *)dirname, .len = dirname_len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) + sizeof(req.entry_out) }
+ };
+
+ LOG_INF(
+ "sending FUSE_MKDIR %s, inode=%" PRIu64 ", unique=%" PRIu64,
+ dirname, inode, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 3, 2);
+
+ LOG_INF("received FUSE_MKDIR response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_MKDIR, used_len, buf[2].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ return 0;
+}
+
+int virtiofs_unlink(const struct device *dev, const char *fname, enum fuse_object_type type)
+{
+ struct fuse_unlink_req req;
+ uint32_t fname_len = strlen(fname) + 1;
+
+ fuse_create_unlink_req(&req, fname_len, type);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) },
+ { .addr = (void *)fname, .len = fname_len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) }
+ };
+
+ LOG_INF(
+ "sending %s for %s, unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_RMDIR" : "FUSE_UNLINK", fname, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 3, 2);
+
+ LOG_INF(
+ "received %s response, unique=%" PRIu64,
+ type == FUSE_DIR ? "FUSE_RMDIR" : "FUSE_UNLINK", req.out_header.unique
+ );
+
+ return virtiofs_validate_response(
+ &req.out_header, type == FUSE_DIR ? FUSE_RMDIR : FUSE_UNLINK, used_len,
+ sizeof(req.out_header)
+ );
+}
+
+int virtiofs_rename(
+ const struct device *dev, uint64_t old_dir_inode, const char *old_name,
+ uint64_t new_dir_inode, const char *new_name)
+{
+ struct fuse_rename_req req;
+ uint32_t old_len = strlen(old_name) + 1;
+ uint32_t new_len = strlen(new_name) + 1;
+
+ fuse_create_rename_req(&req, old_dir_inode, old_len, new_dir_inode, new_len);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) + sizeof(req.rename_in) },
+ { .addr = (void *)old_name, .len = old_len },
+ { .addr = (void *)new_name, .len = new_len },
+ { .addr = &req.out_header, .len = sizeof(req.out_header) }
+ };
+
+ LOG_INF(
+ "sending FUSE_RENAME %s to %s, unique=%" PRIu64,
+ old_name, new_name, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 4, 3);
+
+ LOG_INF("received FUSE_RENAME response, unique=%" PRIu64, req.out_header.unique);
+
+ return virtiofs_validate_response(
+ &req.out_header, FUSE_RENAME, used_len, sizeof(req.out_header)
+ );
+}
+
+int virtiofs_statfs(const struct device *dev, struct fuse_kstatfs *response)
+{
+ struct fuse_kstatfs_req req;
+
+ fuse_fill_header(&req.in_header, sizeof(req.in_header), FUSE_STATFS, FUSE_ROOT_INODE);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = sizeof(req.in_header) },
+ { .addr = &req.out_header,
+ .len = sizeof(req.out_header) + sizeof(req.kstatfs_out) }
+ };
+
+ LOG_INF("sending FUSE_STATFS, unique=%" PRIu64, req.in_header.unique);
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 2, 1);
+
+ LOG_INF("received FUSE_STATFS response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(
+ &req.out_header, FUSE_STATFS, used_len, buf[1].len
+ );
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+#ifdef CONFIG_VIRTIOFS_DEBUG
+ fuse_dump_kstafs(&req.kstatfs_out);
+#endif
+
+ *response = req.kstatfs_out;
+
+ return 0;
+}
+
+int virtiofs_readdir(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint8_t *dirent_buf, uint32_t dirent_size, uint8_t *name_buf, uint32_t name_size)
+{
+ struct fuse_read_req req;
+
+ fuse_create_read_req(&req, inode, fh, offset, dirent_size + name_size, FUSE_DIR);
+
+ struct virtq_buf buf[] = {
+ { .addr = &req.in_header, .len = req.in_header.len },
+ { .addr = &req.out_header, .len = sizeof(struct fuse_out_header) },
+ { .addr = dirent_buf, .len = dirent_size },
+ { .addr = name_buf, .len = name_size }
+ };
+
+ LOG_INF(
+ "sending FUSE_READDIR, nodeid=%" PRIu64 ", fh=%" PRIu64 ", offset=%" PRIu64
+ ", size=%" PRIu32 ", unique=%" PRIu64,
+ inode, fh, offset, dirent_size + name_size, req.in_header.unique
+ );
+ uint32_t used_len = virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 4, 1);
+
+ LOG_INF("received FUSE_READDIR response, unique=%" PRIu64, req.out_header.unique);
+
+ int valid_ret = virtiofs_validate_response(&req.out_header, FUSE_READDIR, used_len, -1);
+
+ if (valid_ret != 0) {
+ return valid_ret;
+ }
+
+ return req.out_header.len - sizeof(req.out_header);
+}
+
+void virtiofs_forget(const struct device *dev, uint64_t inode, uint64_t nlookup)
+{
+ if (inode == FUSE_ROOT_INODE) {
+ return;
+ }
+
+ struct fuse_forget_req req;
+
+ fuse_fill_header(&req.in_header, sizeof(req.in_header), FUSE_FORGET, inode);
+ req.forget_in.nlookup = nlookup; /* refcount will be decreased by this value */
+
+ struct virtq_buf buf[] = {
+ { .addr = &req, .len = sizeof(req.in_header) + sizeof(req.forget_in) }
+ };
+
+ LOG_INF(
+ "sending FUSE_FORGET nodeid=%" PRIu64 ", nlookup=%" PRIu64 ", unique=%" PRIu64,
+ inode, nlookup, req.in_header.unique
+ );
+ virtiofs_send_receive(dev, REQUEST_QUEUE, buf, 1, 1);
+ LOG_INF("received FUSE_FORGET response, unique=%" PRIu64, req.in_header.unique);
+
+ /*
+ * In comparison to other fuse operations this one doesn't return fuse_out_header,
+ * despite virtio spec v1.3 5.11.6.1 saying that out header is common to all
+ * types of fuse requests (comment in include/uapi/linux/fuse.h states otherwise that
+ * FUSE_FORGET has no reply), so there is no error code to return
+ */
+}
diff --git a/subsys/fs/virtiofs/virtiofs.h b/subsys/fs/virtiofs/virtiofs.h
new file mode 100644
index 0000000000000..fc5e863833d5c
--- /dev/null
+++ b/subsys/fs/virtiofs/virtiofs.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ZEPHYR_SUBSYS_FS_VIRTIOFS_VIRTIOFS_H_
+#define ZEPHYR_SUBSYS_FS_VIRTIOFS_VIRTIOFS_H_
+#include
+#include <../fuse_client/fuse_client.h>
+
+int virtiofs_init(const struct device *dev, struct fuse_init_out *response);
+int virtiofs_lookup(
+ const struct device *dev, uint64_t inode, const char *name, struct fuse_entry_out *response,
+ uint64_t *parent_inode);
+int virtiofs_open(
+ const struct device *dev, uint64_t inode, uint32_t flags, struct fuse_open_out *response,
+ enum fuse_object_type type);
+int virtiofs_read(
+ const struct device *dev, uint64_t inode, uint64_t fh,
+ uint64_t offset, uint32_t size, uint8_t *buf);
+int virtiofs_release(const struct device *dev, uint64_t inode, uint64_t fh,
+ enum fuse_object_type type);
+int virtiofs_destroy(const struct device *dev);
+int virtiofs_create(
+ const struct device *dev, uint64_t inode, const char *fname, uint32_t flags,
+ uint32_t mode, struct fuse_create_out *response);
+int virtiofs_write(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint32_t size, const uint8_t *write_buf);
+int virtiofs_lseek(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint32_t whence, struct fuse_lseek_out *response);
+int virtiofs_setattr(
+ const struct device *dev, uint64_t inode, struct fuse_setattr_in *in,
+ struct fuse_attr_out *response);
+int virtiofs_fsync(const struct device *dev, uint64_t inode, uint64_t fh);
+int virtiofs_mkdir(const struct device *dev, uint64_t inode, const char *dirname, uint32_t mode);
+int virtiofs_unlink(const struct device *dev, const char *fname, enum fuse_object_type type);
+int virtiofs_rename(
+ const struct device *dev, uint64_t old_dir_inode, const char *old_name,
+ uint64_t new_dir_inode, const char *new_name);
+int virtiofs_statfs(const struct device *dev, struct fuse_kstatfs *response);
+int virtiofs_readdir(
+ const struct device *dev, uint64_t inode, uint64_t fh, uint64_t offset,
+ uint8_t *dirent_buf, uint32_t dirent_size, uint8_t *name_buf, uint32_t name_size);
+void virtiofs_forget(const struct device *dev, uint64_t inode, uint64_t nlookup);
+
+#endif /* ZEPHYR_SUBSYS_FS_VIRTIOFS_VIRTIOFS_H_ */
diff --git a/subsys/fs/virtiofs/virtiofs_zfs.c b/subsys/fs/virtiofs/virtiofs_zfs.c
new file mode 100644
index 0000000000000..4a0f50fcb1ec4
--- /dev/null
+++ b/subsys/fs/virtiofs/virtiofs_zfs.c
@@ -0,0 +1,669 @@
+/*
+ * Copyright (c) 2025 Antmicro
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include "../fs_impl.h"
+#include
+#include "virtiofs.h"
+
+#define MODE_FTYPE_MASK 0170000
+#define MODE_FTYPE_DIR 040000
+
+#define DT_DIR 4
+#define DT_REG 8
+
+struct virtiofs_file {
+ uint64_t fh;
+ uint64_t nodeid;
+ uint64_t offset;
+ uint32_t open_flags;
+};
+
+struct virtiofs_dir {
+ uint64_t fh;
+ uint64_t nodeid;
+ uint64_t offset;
+};
+
+K_MEM_SLAB_DEFINE_STATIC(
+ file_struct_slab, sizeof(struct virtiofs_file), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
+);
+K_MEM_SLAB_DEFINE_STATIC(
+ dir_struct_slab, sizeof(struct virtiofs_dir), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
+);
+
+static int zephyr_mode_to_posix(int m)
+{
+ int mode = (m & FS_O_CREATE) ? O_CREAT : 0;
+
+ mode |= (m & FS_O_APPEND) ? O_APPEND : 0;
+ mode |= (m & FS_O_TRUNC) ? O_TRUNC : 0;
+
+ switch (m & FS_O_MODE_MASK) {
+ case FS_O_READ:
+ mode |= O_RDONLY;
+ break;
+ case FS_O_WRITE:
+ mode |= O_WRONLY;
+ break;
+ case FS_O_RDWR:
+ mode |= O_RDWR;
+ break;
+ default:
+ break;
+ }
+
+ return mode;
+}
+
+static const char *virtiofs_strip_prefix(const char *path, const struct fs_mount_t *mp)
+{
+ const char *virtiofs_path = fs_impl_strip_prefix(path, mp);
+
+ if (virtiofs_path[0] == '/') {
+ virtiofs_path++;
+ }
+ return virtiofs_path;
+}
+
+static const char *strip_path(const char *fpath)
+{
+ const char *c = fpath + strlen(fpath);
+
+ for (; c > fpath; c--) {
+ if (*c == '/') {
+ c++;
+ break;
+ }
+ }
+
+ return c;
+}
+
+/*
+ * despite the similarity of fuse/virtiofs to posix fs functions there are some notable differences:
+ * - open() is split into lookup+open in case of existing files and lookup+create in case of
+ * O_CREATE
+ * - opendir() is split into lookup+opendir
+ * - lookups are non-recursive, we have to traverse through each directory in the path
+ * - close()/closedir() is split into release+forget/releasedir+forget
+ * - read()/write()/readdir() takes offset as a parameter
+ * - there is sort of reverse stat() - settatr, that can be used to i.e. truncate the file
+ */
+
+static int virtiofs_zfs_open_existing(
+ struct fs_file_t *filp, struct fuse_entry_out lookup_ret, int flags)
+{
+ struct fuse_open_out open_ret;
+ struct virtiofs_file *file;
+
+ int ret = virtiofs_open(
+ filp->mp->storage_dev, lookup_ret.nodeid, zephyr_mode_to_posix(flags), &open_ret,
+ FUSE_FILE
+ );
+ if (ret == 0) {
+ ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
+ if (ret != 0) {
+ virtiofs_release(
+ filp->mp->storage_dev, lookup_ret.nodeid, open_ret.fh, FUSE_FILE
+ );
+ return ret;
+ }
+
+ file->fh = open_ret.fh;
+ file->nodeid = lookup_ret.nodeid;
+ file->offset = 0;
+ file->open_flags = flags;
+
+ filp->filep = file;
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_open_create(
+ struct fs_file_t *filp, int flags, const char *path, uint64_t parent_inode)
+{
+ struct fuse_create_out create_ret;
+ const char *fname = strip_path(path);
+ struct virtiofs_file *file;
+
+ int ret = virtiofs_create(
+ filp->mp->storage_dev, parent_inode, fname,
+ zephyr_mode_to_posix(flags), CONFIG_VIRTIOFS_CREATE_MODE_VALUE, &create_ret
+ );
+ if (ret == 0) {
+ ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
+ if (ret != 0) {
+ virtiofs_release(
+ filp->mp->storage_dev, create_ret.entry_out.nodeid,
+ create_ret.open_out.fh, FUSE_FILE
+ );
+ return ret;
+ }
+
+ file->fh = create_ret.open_out.fh;
+ file->nodeid = create_ret.entry_out.nodeid;
+ file->offset = 0;
+ file->open_flags = flags;
+
+ filp->filep = file;
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_open(struct fs_file_t *filp, const char *fs_path, fs_mode_t flags)
+{
+ int ret = 0;
+ struct fuse_entry_out lookup_ret;
+ const char *path = virtiofs_strip_prefix(fs_path, filp->mp);
+ uint64_t parent_inode = FUSE_ROOT_INODE;
+
+ ret = virtiofs_lookup(
+ filp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
+ );
+ if (ret == 0) {
+ ret = virtiofs_zfs_open_existing(filp, lookup_ret, flags & ~FS_O_CREATE);
+ } else if ((flags & FS_O_CREATE) && parent_inode != 0) {
+ ret = virtiofs_zfs_open_create(filp, flags, path, parent_inode);
+ } else {
+ if (parent_inode != 0) {
+ virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
+ }
+ return ret;
+ }
+
+ if (parent_inode != 0) {
+ virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
+ }
+
+ if (ret != 0) {
+ virtiofs_forget(filp->mp->storage_dev, lookup_ret.nodeid, 1);
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_close(struct fs_file_t *filp)
+{
+ struct virtiofs_file *file = filp->filep;
+ uint64_t nodeid = file->nodeid;
+ int ret = virtiofs_release(filp->mp->storage_dev, file->nodeid, file->fh, FUSE_FILE);
+
+ if (ret == 0) {
+ k_mem_slab_free(&file_struct_slab, file);
+ } else {
+ return ret;
+ }
+
+ virtiofs_forget(filp->mp->storage_dev, nodeid, 1);
+
+ return 0;
+}
+
+static ssize_t virtiofs_zfs_read(struct fs_file_t *filp, void *dest, size_t nbytes)
+{
+ struct virtiofs_file *file = filp->filep;
+ int read_c = virtiofs_read(
+ filp->mp->storage_dev, file->nodeid, file->fh, file->offset, nbytes, dest
+ );
+
+ if (read_c >= 0) {
+ file->offset += read_c;
+ }
+
+ return read_c;
+}
+
+#define FUSE_SEEK_SET 0
+#define FUSE_SEEK_CUR 1
+#define FUSE_SEEK_END 2
+
+static int zephyr_whence_to_posix(int whence)
+{
+ switch (whence) {
+ case SEEK_SET:
+ return FUSE_SEEK_SET;
+ case SEEK_CUR:
+ return FUSE_SEEK_CUR;
+ case SEEK_END:
+ return FUSE_SEEK_END;
+ default:
+ return whence;
+ }
+}
+
+static int virtio_zfs_lseek(struct fs_file_t *filp, off_t off, int whence)
+{
+ struct virtiofs_file *file = filp->filep;
+ struct fuse_lseek_out lseek_ret;
+ uint64_t off_arg = off;
+
+ whence = zephyr_whence_to_posix(whence);
+
+ /*
+ * SEEK_CUR is kind of broken with FUSE_LSEEK as reads/writes don't update the file
+ * offset on the host side so if we never used FUSE_LSEEK since opening the file, but
+ * did some reads/writes in the meantime and then used FUSE_LSEEK with SEEK_CUR+x,
+ * the returned offset would've been x instead of sum of read/written bytes + x. One
+ * solution is to pair each read/write with lseek(SEEK_CUR, read_c/write_c) to keep
+ * the offset updated on the host side, but we just don't use SEEK_CUR+x and instead
+ * use SEEK_SET with file->offset+x. Essentially the only thing FUSE_LSEEK provides
+ * for us is bounds checking and easier handling of SEEK_END (otherwise we would have
+ * to use other fuse call to determine file size)
+ */
+ if (whence == FUSE_SEEK_CUR) {
+ whence = FUSE_SEEK_SET;
+ off_arg = file->offset + off;
+ }
+
+ int ret = virtiofs_lseek(
+ filp->mp->storage_dev, file->nodeid, file->fh, off_arg, whence, &lseek_ret
+ );
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ file->offset = lseek_ret.offset;
+
+ return file->offset;
+}
+
+static ssize_t virtio_zfs_write(struct fs_file_t *filp, const void *src, size_t nbytes)
+{
+ struct virtiofs_file *file = filp->filep;
+ struct virtiofs_fs_data *fs_data = filp->mp->fs_data;
+ const uint8_t *curr_addr = src;
+ int write_c = 0;
+
+ while (nbytes > 0) {
+ /*
+ * max write size is limited to max_write from fuse_init_out received during fs
+ * initalization, so we have to split bigger writes into multiple smaller ones.
+ * If we try to write more the recent virtiofsd it will return 12 (Not enough
+ * space), but the older one will assert, rendering fs unusable until restart.
+ */
+ size_t curr_size = MIN(nbytes, fs_data->max_write);
+
+ /*
+ * while FUSE_WRITE will always write to the end if O_APPEND was passed when opening
+ * file (ignoring offset param) the file offset itself will remain unmodified on
+ * zephyr side, so we have to update it here
+ */
+ if (file->open_flags & FS_O_APPEND) {
+ virtio_zfs_lseek(filp, 0, SEEK_END);
+ }
+
+ int ret = virtiofs_write(
+ filp->mp->storage_dev, file->nodeid, file->fh, file->offset + write_c,
+ curr_size, curr_addr
+ );
+
+ if (ret >= 0) {
+ write_c += ret;
+ } else {
+ /*
+ * according to fs_write comment in fs.h zephyr handles partial
+ * failures like that
+ */
+ if (write_c > 0) {
+ errno = ret;
+ file->offset += write_c;
+ return write_c;
+ } else {
+ return ret;
+ }
+ }
+
+ nbytes -= curr_size;
+ curr_addr += curr_size;
+ }
+
+ file->offset += write_c;
+ return write_c;
+}
+
+static off_t virtiofs_zfs_tell(struct fs_file_t *filp)
+{
+ struct virtiofs_file *file = filp->filep;
+
+ return file->offset;
+}
+
+static int virtiofs_zfs_truncate(struct fs_file_t *filp, off_t length)
+{
+ struct virtiofs_file *file = filp->filep;
+ struct fuse_setattr_in attrs;
+ struct fuse_attr_out setattr_ret;
+
+ attrs.fh = file->fh;
+ attrs.size = length;
+ attrs.valid = FATTR_SIZE;
+
+ return virtiofs_setattr(filp->mp->storage_dev, file->nodeid, &attrs, &setattr_ret);
+}
+
+static int virtiofs_zfs_sync(struct fs_file_t *filp)
+{
+ struct virtiofs_file *file = filp->filep;
+
+ return virtiofs_fsync(filp->mp->storage_dev, file->nodeid, file->fh);
+}
+
+static int virtiofs_zfs_mkdir(struct fs_mount_t *mountp, const char *name)
+{
+ const char *path = virtiofs_strip_prefix(name, mountp);
+ struct fuse_entry_out lookup_ret;
+ uint64_t parent_inode = FUSE_ROOT_INODE;
+ int ret = virtiofs_lookup(
+ mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
+ );
+
+ if (parent_inode != 0) {
+ ret = virtiofs_mkdir(
+ mountp->storage_dev, parent_inode, strip_path(name),
+ CONFIG_VIRTIOFS_CREATE_MODE_VALUE
+ );
+ virtiofs_forget(mountp->storage_dev, parent_inode, 1);
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_opendir(struct fs_dir_t *dirp, const char *fs_path)
+{
+ int ret = 0;
+ struct virtiofs_dir *dir;
+ struct fuse_entry_out lookup_ret;
+ const char *path = virtiofs_strip_prefix(fs_path, dirp->mp);
+
+ if (path[0] == '\0') {
+ /* looking up for "" or "/" yields nothing, so we have to use "." for root dir */
+ path = ".";
+ }
+
+ ret = virtiofs_lookup(dirp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
+ if (ret == 0) {
+ struct fuse_open_out open_ret;
+
+ ret = virtiofs_open(
+ dirp->mp->storage_dev, lookup_ret.nodeid, O_RDONLY, &open_ret, FUSE_DIR
+ );
+ if (ret == 0) {
+ ret = k_mem_slab_alloc(&dir_struct_slab, (void **)&dir, K_NO_WAIT);
+ if (ret != 0) {
+ virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
+ return ret;
+ }
+
+ dir->fh = open_ret.fh;
+ dir->nodeid = lookup_ret.nodeid;
+ dir->offset = 0;
+ dirp->dirp = dir;
+ }
+ } else {
+ virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_readdir(struct fs_dir_t *dirp, struct fs_dirent *entry)
+{
+ struct virtiofs_dir *dir = dirp->dirp;
+ struct fuse_dirent de;
+
+ int read_c = virtiofs_readdir(
+ dirp->mp->storage_dev, dir->nodeid, dir->fh, dir->offset,
+ (uint8_t *)&de, sizeof(de), (uint8_t *)&entry->name, sizeof(entry->name)
+ );
+
+ /* end of dir */
+ if (read_c == 0) {
+ entry->name[0] = '\0';
+ return 0;
+ }
+
+ if (read_c < sizeof(de) || de.namelen >= sizeof(entry->name) - 1) {
+ return -EIO;
+ }
+
+ /*
+ * usually name is already null terminated, but sometimes name of the last entry
+ * in directory is not (maybe also in other cases), so we null terminate it here
+ */
+ entry->name[de.namelen] = '\0';
+
+ dir->offset = de.off;
+
+ if (de.type == DT_REG) {
+ struct fuse_entry_out lookup_ret;
+ int ret = virtiofs_lookup(
+ dirp->mp->storage_dev, dir->nodeid, entry->name, &lookup_ret, NULL
+ );
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
+
+ entry->type = FS_DIR_ENTRY_FILE;
+ entry->size = lookup_ret.attr.size;
+ } else if (de.type == DT_DIR) {
+ entry->type = FS_DIR_ENTRY_DIR;
+ entry->size = 0;
+ } else {
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+static int virtiofs_zfs_closedir(struct fs_dir_t *dirp)
+{
+ struct virtiofs_dir *dir = dirp->dirp;
+ uint64_t nodeid = dir->nodeid;
+ int ret = virtiofs_release(dirp->mp->storage_dev, dir->nodeid, dir->fh, FUSE_DIR);
+
+ if (ret == 0) {
+ k_mem_slab_free(&dir_struct_slab, dir);
+ } else {
+ return ret;
+ }
+
+ virtiofs_forget(dirp->mp->storage_dev, nodeid, 1);
+
+ return 0;
+}
+
+static int virtiofs_zfs_mount(struct fs_mount_t *mountp)
+{
+ struct fuse_init_out out;
+ int ret = virtiofs_init(mountp->storage_dev, &out);
+
+ if (ret == 0) {
+ struct virtiofs_fs_data *fs_data = mountp->fs_data;
+
+ fs_data->max_write = out.max_write;
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_unmount(struct fs_mount_t *mountp)
+{
+ return virtiofs_destroy(mountp->storage_dev);
+}
+
+static int virtiofs_zfs_stat(
+ struct fs_mount_t *mountp, const char *fs_path, struct fs_dirent *entry)
+{
+ const char *path = virtiofs_strip_prefix(fs_path, mountp);
+ const char *name = strip_path(fs_path);
+
+ if (strlen(name) + 1 > sizeof(entry->name)) {
+ return -ENOBUFS;
+ }
+
+ struct fuse_entry_out lookup_ret;
+ int ret = virtiofs_lookup(mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ strcpy((char *)&entry->name, name);
+
+ if ((lookup_ret.attr.mode & MODE_FTYPE_MASK) == MODE_FTYPE_DIR) {
+ entry->type = FS_DIR_ENTRY_DIR;
+ entry->size = 0;
+ } else {
+ entry->type = FS_DIR_ENTRY_FILE;
+ entry->size = lookup_ret.attr.size;
+ }
+
+ virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
+
+ return 0;
+}
+
+static int virtiofs_zfs_unlink(struct fs_mount_t *mountp, const char *name)
+{
+ const char *path = virtiofs_strip_prefix(name, mountp);
+ struct fs_dirent d;
+ int ret = virtiofs_zfs_stat(mountp, name, &d);
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ if (d.type == FS_DIR_ENTRY_FILE) {
+#ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
+ struct fuse_entry_out lookup_ret;
+ /*
+ * Even if unlink doesn't take nodeid as a param it still fails with -EIO if the
+ * file wasn't looked up using some virtiofsd versions. It happens at least with
+ * the one from Debian's package (Debian 1:7.2+dfsg-7+deb12u7). Virtiofsd 1.12.0
+ * built from sources doesn't need it
+ */
+ ret = virtiofs_lookup(
+ mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL
+ );
+
+ if (ret != 0) {
+ return ret;
+ }
+#endif
+
+ ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_FILE);
+
+#ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
+ virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
+#endif
+ } else {
+ ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_DIR);
+ }
+
+ return ret;
+}
+
+static int virtiofs_zfs_rename(struct fs_mount_t *mountp, const char *from, const char *to)
+{
+ const char *old_path = virtiofs_strip_prefix(from, mountp);
+ const char *new_path = virtiofs_strip_prefix(to, mountp);
+ uint64_t old_dir = FUSE_ROOT_INODE;
+ uint64_t new_dir = FUSE_ROOT_INODE;
+ struct fuse_entry_out old_lookup_ret;
+ struct fuse_entry_out new_lookup_ret;
+ int ret;
+
+ ret = virtiofs_lookup(
+ mountp->storage_dev, FUSE_ROOT_INODE, old_path, &old_lookup_ret, &old_dir
+ );
+
+ if (ret != 0) {
+ if (old_dir != 0 && old_dir != FUSE_ROOT_INODE) {
+ virtiofs_forget(mountp->storage_dev, old_dir, 1);
+ }
+ return ret;
+ }
+
+ ret = virtiofs_lookup(
+ mountp->storage_dev, FUSE_ROOT_INODE, new_path, &new_lookup_ret, &new_dir
+ );
+
+ /* there is no immediate parent of object's new path */
+ if (ret != 0 && new_dir == 0) {
+ virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
+ return ret;
+ }
+
+ ret = virtiofs_rename(
+ mountp->storage_dev,
+ old_dir, strip_path(old_path),
+ new_dir, strip_path(new_path)
+ );
+
+ virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
+ virtiofs_forget(mountp->storage_dev, new_lookup_ret.nodeid, 1);
+ virtiofs_forget(mountp->storage_dev, old_dir, 1);
+ virtiofs_forget(mountp->storage_dev, new_dir, 1);
+
+ return ret;
+}
+
+static int virtiofs_zfs_statvfs(
+ struct fs_mount_t *mountp, const char *fs_path, struct fs_statvfs *stat)
+{
+ struct fuse_kstatfs statfs_out;
+ int ret = virtiofs_statfs(mountp->storage_dev, &statfs_out);
+
+ if (ret != 0) {
+ return ret;
+ }
+
+ stat->f_bsize = statfs_out.bsize;
+ stat->f_frsize = statfs_out.frsize;
+ stat->f_blocks = statfs_out.blocks;
+ stat->f_bfree = statfs_out.bfree;
+
+ return 0;
+}
+
+static const struct fs_file_system_t virtiofs_ops = {
+ .open = virtiofs_zfs_open,
+ .close = virtiofs_zfs_close,
+ .read = virtiofs_zfs_read,
+ .write = virtio_zfs_write,
+ .lseek = virtio_zfs_lseek,
+ .tell = virtiofs_zfs_tell,
+ .truncate = virtiofs_zfs_truncate,
+ .sync = virtiofs_zfs_sync,
+ .mkdir = virtiofs_zfs_mkdir,
+ .opendir = virtiofs_zfs_opendir,
+ .readdir = virtiofs_zfs_readdir,
+ .closedir = virtiofs_zfs_closedir,
+ .mount = virtiofs_zfs_mount,
+ .unmount = virtiofs_zfs_unmount,
+ .unlink = virtiofs_zfs_unlink,
+ .rename = virtiofs_zfs_rename,
+ .stat = virtiofs_zfs_stat,
+ .statvfs = virtiofs_zfs_statvfs
+};
+
+static int virtiofs_register(void)
+{
+ return fs_register(FS_VIRTIOFS, &virtiofs_ops);
+}
+
+SYS_INIT(virtiofs_register, POST_KERNEL, 99);