| /* |
| * Copyright (c) 2025 Antmicro <www.antmicro.com> |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/init.h> |
| #include <zephyr/fs/fs.h> |
| #include <zephyr/fs/fs_sys.h> |
| #include <zephyr/fs/virtiofs.h> |
| #include <zephyr/posix/fcntl.h> |
| #include "../fs_impl.h" |
| #include <string.h> |
| #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); |