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);