Kernel+Userland: Add FUSE support
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.
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 :)
Latest push resolves the conflicts, and also improves the FUSE device so that it should now properly support multiple clients.
@supercomputer7 do you have any time to look at this?
@supercomputer7 do you have any time to look at this?
Sure. I will give this a review soon.
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 :)
Thanks! I haven't gotten around to using Discord yet, but I appreciate the review here :)