Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kernel+Userland: Add FUSE support #23098

Merged
merged 3 commits into from
May 7, 2024
Merged

Conversation

implicitfield
Copy link
Contributor

At this point, there's practically full support for reading, and limited support for writing (modifying inodes should work for the most part, but there's no support for altering the directory structure.)

You'll need to be root for all interactions with the filesystem, as support for other uids and gids is currently nonexistent :p

To test this, you can use the following self-contained POC daemon:

source
#include <AK/Assertions.h>
#include <AK/ByteString.h>
#include <AK/Format.h>
#include <Kernel/API/FileSystem/MountSpecificFlags.h>
#include <Kernel/API/Ioctl.h>
#include <Kernel/FileSystem/FUSE/Definitions.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>

static size_t s_file_size = 1024;
static char const* hello_str = "Hello World!\n";

static bool get_request(char* buffer, size_t buffer_size, int fd)
{
retry:
    if (read(fd, buffer, buffer_size) == -1) {
        if (errno == ENOENT) {
            usleep(10000);
            goto retry;
        }

        printf("[X] Error: %s\n", strerror(errno));
        return false;
    }

    return true;
}

static void ack_init(struct fuse_in_header* in, int fd)
{
    struct fuse_out_header out;
    out.unique = in->unique;
    out.error = 0;

    struct fuse_init_out init_out;
    init_out.major = 7;
    init_out.minor = 39;

    struct iovec iov[2];
    iov[0].iov_base = &out;
    iov[0].iov_len = sizeof(struct fuse_out_header);

    iov[1].iov_base = &init_out;
    iov[1].iov_len = sizeof(struct fuse_init_out);

    out.len = iov[0].iov_len + iov[1].iov_len;

    if (writev(fd, iov, 2) == -1)
        printf("[X] Error: %s\n", strerror(errno));
}

static bool set_mount_flag(ByteString key, u64 value, int mount_fd)
{
    MountSpecificFlag flag;
    flag.key_string_length = key.bytes().size();
    flag.key_string_addr = key.bytes().data();
    flag.value_type = MountSpecificFlag::ValueType::UnsignedInteger;
    flag.value_length = 8;
    flag.value_addr = &value;

    if (ioctl(mount_fd, MOUNT_IOCTL_SET_MOUNT_SPECIFIC_FLAG, &flag) == -1) {
        printf("[X] Error: %s\n", strerror(errno));
        return false;
    }

    return true;
}

static bool reply_with_buffer(struct fuse_in_header* in, char* buffer, size_t buffer_size, int fd)
{
    struct fuse_out_header out;
    out.unique = in->unique;
    out.error = 0;

    struct iovec iov[2];
    iov[0].iov_base = &out;
    iov[0].iov_len = sizeof(struct fuse_out_header);

    iov[1].iov_base = buffer;
    iov[1].iov_len = buffer_size;

    out.len = iov[0].iov_len + iov[1].iov_len;

    if (writev(fd, iov, 2) == -1) {
        printf("[X] Error: %s\n", strerror(errno));
        return false;
    }

    return true;
}

static bool reply_with_error(struct fuse_in_header* in, int32_t error, int fd)
{
    struct fuse_out_header out;
    out.len = sizeof(struct fuse_out_header);
    out.unique = in->unique;
    out.error = -error;

    if (write(fd, &out, out.len) == -1) {
        printf("[X] Error: %s\n", strerror(errno));
        return false;
    }

    return true;
}

static void fill_entry(struct fuse_entry_out* entry_out, uint64_t nodeid, bool directory)
{
    entry_out->nodeid = nodeid;
    entry_out->generation = 0;
    entry_out->entry_valid = ULONG_MAX;
    entry_out->entry_valid_nsec = 999999999;
    entry_out->attr_valid = ULONG_MAX;
    entry_out->attr_valid_nsec = 999999999;

    if (directory) {
        entry_out->attr.mode = S_IFDIR | 0755;
        entry_out->attr.nlink = 2;
    } else {
        entry_out->attr.mode = S_IFREG | 0444;
        entry_out->attr.nlink = 1;
        entry_out->attr.size = s_file_size;
    }
}

static size_t get_dirent_entry_length(char const* name)
{
    return strlen(name) + FUSE_NAME_OFFSET;
}

static size_t get_dirent_entry_length_padded(char const* name)
{
    return FUSE_DIRENT_ALIGN(get_dirent_entry_length(name));
}

static size_t push_dirent(char* buffer, uint64_t nodeid, bool is_directory, char const* name, size_t offset)
{
    uint32_t type = is_directory ? (S_IFDIR | 0755) : (S_IFREG | 0444);
    uint32_t dirent_type = (type & S_IFMT) >> 12;
    struct fuse_dirent* dirent = (struct fuse_dirent*)(buffer + offset);
    dirent->ino = nodeid;
    dirent->off = offset + get_dirent_entry_length_padded(name);
    dirent->namelen = strlen(name);
    dirent->type = dirent_type;
    memcpy(dirent->name, name, dirent->namelen);
    memset(dirent->name + dirent->namelen, 0, get_dirent_entry_length_padded(name) - get_dirent_entry_length(name));

    return dirent->off;
}

static void* realloc_s(void* buffer, size_t old_size, size_t new_size)
{
    VERIFY(buffer);
    void* new_buffer = malloc(new_size);
    memcpy(new_buffer, buffer, new_size > old_size ? old_size : new_size);
    free(buffer);
    if (new_size > old_size)
        memset((char*)new_buffer + old_size, 0, new_size - old_size);
    return new_buffer;
}

int main()
{
    int fd = open("/dev/fuse", O_RDWR);
    if (fd == -1) {
        printf("Could not open device\n");
        return 1;
    }

    printf("[*] Opened fd: %d\n", fd);

    size_t options_size = 1024;
    char* options = (char*)malloc(options_size);
    memset(options, 0, options_size);

    sprintf(options, "allow_other,fd=%i,rootmode=%u,user_id=%u,group_id=%u", fd, 40000, 0, 0);

    printf("Trying to mount with options: %s\n", options);

    int mount_fd = fsopen("FUSE", 0);
    if (mount_fd < 0) {
        printf("[X] Error: %s\n", strerror(errno));
        return 1;
    }

    if (!set_mount_flag("fd", fd, mount_fd))
        return 1;
    if (!set_mount_flag("rootmode", 40000, mount_fd))
        return 1;

    if (fsmount(mount_fd, -1, "/mnt/") == -1) {
        printf("[X] Error: %s\n", strerror(errno));
        return 1;
    }

    free(options);
    options = NULL;
    printf("Mount complete\n");

    size_t buffer_size = 0x21000;
    char* buffer = (char*)malloc(buffer_size);
    memset(buffer, 0, buffer_size);

    char* file_contents = (char*)malloc(s_file_size);
    memset(file_contents, 0, s_file_size);
    memcpy(file_contents, hello_str, strlen(hello_str));

    while (true) {
        if (!get_request(buffer, buffer_size, fd))
            return 1;

        struct fuse_in_header in;
        memcpy(&in, buffer, sizeof(in));
        printf("Request header: len: %u, opcode: %u, unique: %lu, nodeid: %lu, uid: %u, gid: %u, pid: %u\n", in.len, in.opcode, in.unique, in.nodeid, in.uid, in.gid, in.pid);

        switch (in.opcode) {
        case FUSE_INIT: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_init_in));

            struct fuse_init_in init;
            memcpy(&init, buffer + sizeof(struct fuse_in_header), sizeof(init));
            printf("Init request: major: %u, minor: %u, max_readahead: %u, flags: 0x%x\n", init.major, init.minor, init.max_readahead, init.flags);

            ack_init(&in, fd);
            break;
        }
        case FUSE_LOOKUP: {
            size_t len = in.len - sizeof(struct fuse_in_header);
            char* name = (char*)malloc(len);
            memset(name, 0, len);
            memcpy(name, buffer + sizeof(struct fuse_in_header), len);

            printf("Lookup request for filename: '%s', length: %zu, with parent fd: %lu\n", name, len, in.nodeid);
            if (in.nodeid != 1 || strcmp(name, "hello") != 0) {
                reply_with_error(&in, ENOENT, fd);
                free(name);
                break;
            }

            free(name);
            struct fuse_entry_out entry { };
            fill_entry(&entry, 2, false);
            reply_with_buffer(&in, (char*)&entry, sizeof(entry), fd);

            break;
        }
        case FUSE_STATX: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_statx_in));

            struct fuse_statx_in statx;
            memcpy(&statx, buffer + sizeof(struct fuse_in_header), sizeof(statx));
            printf("statx request: getattr_flags: %u, fh: %lu, sx_flags: 0x%x, sx_mask: 0x%x\n", statx.getattr_flags, statx.fh, statx.sx_flags, statx.sx_mask);

            struct fuse_statx_out statx_out;
            statx_out.attr_valid = ULONG_MAX;
            statx_out.attr_valid_nsec = 999999999;

            if (in.nodeid == 1) {
                statx_out.stat.mode = S_IFDIR | 0755;
                statx_out.stat.nlink = 2;
            } else {
                statx_out.stat.mode = S_IFREG | 0444;
                statx_out.stat.nlink = 1;
                statx_out.stat.size = s_file_size;
            }

            reply_with_buffer(&in, (char*)&statx_out, sizeof(statx_out), fd);
            break;
        }
        case FUSE_READ:
        case FUSE_READDIR: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_read_in));

            struct fuse_read_in read;
            memcpy(&read, buffer + sizeof(struct fuse_in_header), sizeof(read));
            printf("Read(dir(plus)) request: fh: %lu, offset: %lu, size: %u, read_flags: %u, lock_owner: %lu, flags: 0x%x\n", read.fh, read.offset, read.size, read.read_flags, read.lock_owner, read.flags);

            if (in.nodeid == 2) {
                VERIFY(in.opcode == FUSE_READ);
                size_t data_size = min(s_file_size - read.offset, read.size);
                printf("Replying with data buffer of size: %zu\n", data_size);
                reply_with_buffer(&in, file_contents + read.offset, data_size, fd);
                break;
            }

            uint32_t offset = 0;
            char* dirent_buffer = (char*)malloc(read.size);

            offset = push_dirent(dirent_buffer, 1, true, ".", offset);
            offset = push_dirent(dirent_buffer, 1, true, "..", offset);
            offset = push_dirent(dirent_buffer, 2, false, "hello", offset);

            if (read.offset >= offset) {
                reply_with_buffer(&in, NULL, 0, fd);
                free(dirent_buffer);
                break;
            }

            printf("Replying with buffer of size: %u\n", offset);
            reply_with_buffer(&in, dirent_buffer + read.offset, offset, fd);
            free(dirent_buffer);

            break;
        }
        case FUSE_OPEN:
        case FUSE_OPENDIR: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_open_in));

            struct fuse_open_in open;
            memcpy(&open, buffer + sizeof(struct fuse_in_header), sizeof(open));
            printf("Open(dir) request: flags: 0x%x\n", open.flags);

            struct fuse_open_out open_out;
            open_out.fh = in.nodeid;
            open_out.open_flags = 0;
            reply_with_buffer(&in, (char*)&open_out, sizeof(open_out), fd);

            break;
        }
        case FUSE_FLUSH: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_flush_in));

            struct fuse_flush_in flush;
            memcpy(&flush, buffer + sizeof(struct fuse_in_header), sizeof(flush));
            printf("Flush request: fh: %lu, lock_owner: %lu\n", flush.fh, flush.lock_owner);

            // Not really an error, we just need a symbolic response.
            reply_with_error(&in, 0, fd);

            break;
        }
        case FUSE_RELEASE:
        case FUSE_RELEASEDIR: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_release_in));

            struct fuse_release_in release;
            memcpy(&release, buffer + sizeof(struct fuse_in_header), sizeof(release));
            printf("Release(dir) request: fh: %lu, flags: 0x%x, release_flags: 0x%x, lock_owner: %lu\n", release.fh, release.flags, release.release_flags, release.lock_owner);

            // Not really an error, we just need a symbolic response.
            reply_with_error(&in, 0, fd);

            break;
        }
        case FUSE_WRITE: {
            struct fuse_write_in write;
            memcpy(&write, buffer + sizeof(struct fuse_in_header), sizeof(write));
            printf("write request: fh: %lu, offset: %lu, size: %u, write_flags: 0x%x, lock_owner: %lu, flags: 0x%x\n", write.fh, write.offset, write.size, write.write_flags, write.lock_owner, write.flags);
            if (write.size + write.offset >= s_file_size) {
                file_contents = (char*)realloc_s(file_contents, s_file_size, write.size + write.offset);
                s_file_size = write.size + write.offset;
            }
            memcpy(file_contents + write.offset, buffer + sizeof(struct fuse_in_header) + sizeof(struct fuse_write_in), write.size);
            struct fuse_write_out reply;
            reply.size = write.size;
            reply_with_buffer(&in, (char*)&reply, sizeof(reply), fd);
            break;
        }
        case FUSE_SETATTR: {
            VERIFY(in.len - sizeof(struct fuse_in_header) == sizeof(struct fuse_setattr_in));

            struct fuse_setattr_in attr;
            memcpy(&attr, buffer + sizeof(struct fuse_in_header), sizeof(attr));
            printf("setattr request: size: %lu\n", attr.size);
            if (attr.size != s_file_size && (attr.valid & FATTR_SIZE)) {
                file_contents = (char*)realloc_s(file_contents, s_file_size, attr.size);
                s_file_size = attr.size;
            }

            struct fuse_attr_out setattr_reply;
            setattr_reply.attr_valid = ULONG_MAX;
            setattr_reply.attr_valid_nsec = 999999999;
            if (in.nodeid == 1) {
                setattr_reply.attr.mode = S_IFDIR | 0755;
                setattr_reply.attr.nlink = 2;
            } else {
                setattr_reply.attr.mode = S_IFREG | 0444;
                setattr_reply.attr.nlink = 1;
                setattr_reply.attr.size = s_file_size;
            }

            reply_with_buffer(&in, (char*)&setattr_reply, sizeof(setattr_reply), fd);
            break;
        }
        default:
            printf("Unsupported request!\n");
            return 1;
        }
    }

    return 0;
}

Alternatively, you can use the example programs from the libfuse port, which I'll PR shortly.

@github-actions github-actions bot added the 👀 pr-needs-review PR needs review from a maintainer or community member label Feb 6, 2024
@supercomputer7
Copy link
Member

This is a very interesting feature. I imagine we could port filesystems like exfat (which are licensed with a license that doesn't permit us to directly port it to our codebase), which is quite nice.
I want to do a review over this, so hopefully that will happen soon :)

@implicitfield
Copy link
Contributor Author

Latest push resolves the conflicts, and also improves the FUSE device so that it should now properly support multiple clients.

@ADKaster
Copy link
Member

ADKaster commented Apr 8, 2024

@supercomputer7 do you have any time to look at this?

@supercomputer7
Copy link
Member

@supercomputer7 do you have any time to look at this?

Sure. I will give this a review soon.


fuse_out_header* header = bit_cast<fuse_out_header*>(response->data());
if (header->error)
return Error::from_errno(-header->error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something smelly about this code. We follow a raw pointer here, and we negate the return code. Can we at least VERIFY that the pointer is valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a problem here, as the pointer is backed by a KBuffer, and send_request_and_wait_for_a_reply() guarantees that said buffer is at least large enough to contain a fuse_out_header. The negation of the error code is needed because Error::from_errno() expects a positive input, while errors in FUSE are always negative.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way than just casting it after the call site? maybe return fuse_out_header?

Copy link
Contributor Author

@implicitfield implicitfield Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not quite as simple as just returning a fuse_out_header would be, since often the response buffer should actually contain more data than just a header, such as when we send (for example) a FUSE_READ or a FUSE_READDIR request. Properly parsing the response also depends on the type of the original request, so its hard to make send_request_and_wait_for_a_reply() have a more specific return type.

This is useful for parsing user-provided integers that should be
interpreted as octals.
@supercomputer7
Copy link
Member

supercomputer7 commented Apr 29, 2024

Just want to say I am really happy about the amendments you made to your patches - it looks much better now! :)
Are you on the Discord server? maybe we can talk over there for exchanging ideas in a faster way.
Anyway, feel free to ask questions and raise suggestions on my comments :)

@implicitfield
Copy link
Contributor Author

Thanks! I haven't gotten around to using Discord yet, but I appreciate the review here :)

This adds both the fuse device (used for communication between the
kernel and the filesystem) and filesystem implementation itself.
This only contains the bare minimum amount of functionality required
by libfuse.
Copy link
Member

@ADKaster ADKaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty sweet, thanks for expanding our Filesystem code! This looks like it's in a good enough state to iterate on in-tree. I'm a bit nervous about the Dual-licensed Defintions file, but given that one of the licenses was BSD 2 Clause, the same as our license, I think(?) your modifications on top of it are suitable to not get us in trouble. It might be a good idea in a follow-up to add a link or blurb about where the file is orignally from (Linux/BSD/Some other FOSS project/etc) and why we want to keep its license intact rather than using the SPDX wording.

@ADKaster ADKaster merged commit f5a74d7 into SerenityOS:master May 7, 2024
14 checks passed
@github-actions github-actions bot removed the 👀 pr-needs-review PR needs review from a maintainer or community member label May 7, 2024
@implicitfield implicitfield deleted the fuse branch May 8, 2024 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants