diff --git a/Kernel/Arch/init.cpp b/Kernel/Arch/init.cpp index ad3a429bb69..756d91dfa31 100644 --- a/Kernel/Arch/init.cpp +++ b/Kernel/Arch/init.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -442,6 +443,7 @@ void init_stage2(void*) (void)MemoryDevice::must_create().leak_ref(); (void)ZeroDevice::must_create().leak_ref(); (void)FullDevice::must_create().leak_ref(); + (void)FUSEDevice::must_create().leak_ref(); (void)RandomDevice::must_create().leak_ref(); (void)SelfTTYDevice::must_create().leak_ref(); PTYMultiplexer::initialize(); diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index 5b1bd23c1f0..4772925f749 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -66,6 +66,7 @@ set(KERNEL_SOURCES Devices/CharacterDevice.cpp Devices/Device.cpp Devices/DeviceManagement.cpp + Devices/FUSEDevice.cpp Devices/PCISerialDevice.cpp Devices/SerialDevice.cpp Devices/HID/AllMiceDevice.cpp @@ -149,6 +150,8 @@ set(KERNEL_SOURCES FileSystem/File.cpp FileSystem/FileBackedFileSystem.cpp FileSystem/FileSystem.cpp + FileSystem/FUSE/FileSystem.cpp + FileSystem/FUSE/Inode.cpp FileSystem/Inode.cpp FileSystem/InodeFile.cpp FileSystem/InodeMetadata.cpp diff --git a/Kernel/Debug.h.in b/Kernel/Debug.h.in index 4deff94392e..0810d2b7a9f 100644 --- a/Kernel/Debug.h.in +++ b/Kernel/Debug.h.in @@ -87,6 +87,10 @@ #cmakedefine01 FORK_DEBUG #endif +#ifndef FUSE_DEBUG +#cmakedefine01 FUSE_DEBUG +#endif + #ifndef FUTEX_DEBUG #cmakedefine01 FUTEX_DEBUG #endif diff --git a/Kernel/Devices/FUSEDevice.cpp b/Kernel/Devices/FUSEDevice.cpp new file mode 100644 index 00000000000..309f4585092 --- /dev/null +++ b/Kernel/Devices/FUSEDevice.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace Kernel { + +UNMAP_AFTER_INIT NonnullLockRefPtr FUSEDevice::must_create() +{ + return MUST(DeviceManagement::try_create_device()); +} + +UNMAP_AFTER_INIT FUSEDevice::FUSEDevice() + : CharacterDevice(10, 229) +{ +} + +UNMAP_AFTER_INIT FUSEDevice::~FUSEDevice() = default; + +ErrorOr FUSEDevice::initialize_instance(OpenFileDescription const& fd) +{ + return m_instances.with([&](auto& instances) -> ErrorOr { + for (auto const& instance : instances) + VERIFY(instance.fd != &fd); + + TRY(instances.try_append({ + &fd, + TRY(KBuffer::try_create_with_size("FUSE: Pending request buffer"sv, 0x21000)), + TRY(KBuffer::try_create_with_size("FUSE: Response buffer"sv, 0x21000)), + })); + + return {}; + }); +} + +bool FUSEDevice::can_read(OpenFileDescription const& fd, u64) const +{ + return m_instances.with([&](auto& instances) { + Optional instance_index = {}; + for (size_t i = 0; i < instances.size(); ++i) { + if (instances[i].fd == &fd) { + instance_index = i; + break; + } + } + if (!instance_index.has_value()) { + VERIFY(instances.is_empty()); + return false; + } + + auto& instance = instances[instance_index.value()]; + return instance.buffer_ready || instance.drop_request; + }); +} + +bool FUSEDevice::can_write(OpenFileDescription const&, u64) const +{ + return true; +} + +ErrorOr FUSEDevice::read(OpenFileDescription& fd, u64, UserOrKernelBuffer& buffer, size_t size) +{ + return m_instances.with([&](auto& instances) -> ErrorOr { + Optional instance_index = {}; + for (size_t i = 0; i < instances.size(); ++i) { + if (instances[i].fd == &fd) { + instance_index = i; + break; + } + } + + if (!instance_index.has_value()) + return Error::from_errno(ENODEV); + + auto& instance = instances[instance_index.value()]; + + if (instance.drop_request) { + instance.drop_request = false; + + instances.remove(instance_index.value()); + return Error::from_errno(ENODEV); + } + + if (size < 0x21000) + return Error::from_errno(EIO); + + if (!instance.buffer_ready) + return Error::from_errno(ENOENT); + + TRY(buffer.write(instance.pending_request->bytes())); + instance.buffer_ready = false; + return instance.pending_request->size(); + }); +} + +ErrorOr FUSEDevice::write(OpenFileDescription& description, u64, UserOrKernelBuffer const& buffer, size_t size) +{ + return m_instances.with([&](auto& instances) -> ErrorOr { + Optional instance_index = {}; + for (size_t i = 0; i < instances.size(); ++i) { + if (instances[i].fd == &description) { + instance_index = i; + break; + } + } + + if (!instance_index.has_value()) + return Error::from_errno(ENODEV); + + auto& instance = instances[instance_index.value()]; + + if (instance.expecting_header) { + memset(instance.response->data(), 0, instance.response->size()); + + fuse_out_header header; + TRY(buffer.read(&header, 0, sizeof(fuse_out_header))); + + dbgln_if(FUSE_DEBUG, "header: length: {}, error: {}, unique: {}", header.len, header.error, header.unique); + memcpy(instance.response->data(), &header, sizeof(fuse_out_header)); + + if (header.len > sizeof(fuse_out_header)) + instance.expecting_header = false; + else + instance.response_ready = true; + } else { + fuse_out_header* existing_header = bit_cast(instance.response->data()); + + instance.expecting_header = true; + if (existing_header->len > instance.response->size()) + return Error::from_errno(EINVAL); + + instance.response_ready = true; + u64 length = existing_header->len - sizeof(fuse_out_header); + dbgln_if(FUSE_DEBUG, "request: response length: {}", length); + TRY(buffer.read(instance.response->data() + sizeof(fuse_out_header), 0, length)); + } + + return size; + }); +} + +ErrorOr> FUSEDevice::send_request_and_wait_for_a_reply(OpenFileDescription const& description, Bytes bytes) +{ + return m_instances.with([&](auto& instances) -> ErrorOr> { + Optional instance_index = {}; + for (size_t i = 0; i < instances.size(); ++i) { + if (instances[i].fd == &description) { + instance_index = i; + break; + } + } + + VERIFY(instance_index.has_value()); + auto& instance = instances[instance_index.value()]; + + VERIFY(!instance.drop_request); + VERIFY(bytes.size() <= 0x21000); + + memset(instance.pending_request->data(), 0, instance.pending_request->size()); + memcpy(instance.pending_request->data(), bytes.data(), bytes.size()); + instance.buffer_ready = true; + evaluate_block_conditions(); + + while (!instance.response_ready) + (void)Thread::current()->sleep(Duration::from_microseconds(100)); + + auto result = KBuffer::try_create_with_bytes("FUSEDevice: Response"sv, instance.response->bytes()); + instance.response_ready = false; + + return result; + }); +} + +void FUSEDevice::shutdown_for_description(OpenFileDescription const& description) +{ + m_instances.with([&](auto& instances) { + Optional instance_index = {}; + for (size_t i = 0; i < instances.size(); ++i) { + if (instances[i].fd == &description) { + instance_index = i; + break; + } + } + VERIFY(instance_index.has_value()); + instances[instance_index.value()].drop_request = true; + }); + + evaluate_block_conditions(); +} + +} diff --git a/Kernel/Devices/FUSEDevice.h b/Kernel/Devices/FUSEDevice.h new file mode 100644 index 00000000000..cc62834ef2b --- /dev/null +++ b/Kernel/Devices/FUSEDevice.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Kernel { + +struct FUSEInstance { + OpenFileDescription const* fd = nullptr; + NonnullOwnPtr pending_request; + NonnullOwnPtr response; + bool drop_request = false; + bool buffer_ready = false; + bool response_ready = false; + bool expecting_header = true; +}; + +class FUSEDevice final : public CharacterDevice { + friend class DeviceManagement; + +public: + static NonnullLockRefPtr must_create(); + virtual ~FUSEDevice() override; + + ErrorOr initialize_instance(OpenFileDescription const&); + ErrorOr> send_request_and_wait_for_a_reply(OpenFileDescription const&, Bytes); + void shutdown_for_description(OpenFileDescription const&); + +private: + FUSEDevice(); + + // ^Device + virtual bool is_openable_by_jailed_processes() const override { return false; } + + // ^CharacterDevice + virtual ErrorOr read(OpenFileDescription&, u64, UserOrKernelBuffer&, size_t) override; + virtual ErrorOr write(OpenFileDescription&, u64, UserOrKernelBuffer const&, size_t) override; + virtual bool can_read(OpenFileDescription const&, u64) const override; + virtual bool can_write(OpenFileDescription const&, u64) const override; + virtual StringView class_name() const override { return "FUSEDevice"sv; } + + SpinlockProtected, LockRank::None> m_instances; +}; + +} diff --git a/Kernel/FileSystem/FUSE/Definitions.h b/Kernel/FileSystem/FUSE/Definitions.h new file mode 100644 index 00000000000..cadc65aa5b9 --- /dev/null +++ b/Kernel/FileSystem/FUSE/Definitions.h @@ -0,0 +1,308 @@ +/* + * 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. + */ + +#pragma once + +#include + +#define FUSE_KERNEL_VERSION 7 +#define FUSE_KERNEL_MINOR_VERSION 39 + +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; +}; + +// Bitmasks for fuse_setattr_in.valid +#define FATTR_MODE (1 << 0) +#define FATTR_UID (1 << 1) +#define FATTR_GID (1 << 2) +#define FATTR_SIZE (1 << 3) +#define FATTR_ATIME (1 << 4) +#define FATTR_MTIME (1 << 5) +#define FATTR_FH (1 << 6) +#define FATTR_ATIME_NOW (1 << 7) +#define FATTR_MTIME_NOW (1 << 8) +#define FATTR_LOCKOWNER (1 << 9) +#define FATTR_CTIME (1 << 10) +#define FATTR_KILL_SUIDGID (1 << 11) + +enum class FUSEOpcode { + FUSE_LOOKUP = 1, + FUSE_FORGET = 2, // no reply + FUSE_GETATTR = 3, + FUSE_SETATTR = 4, + FUSE_READLINK = 5, + FUSE_SYMLINK = 6, + FUSE_MKNOD = 8, + FUSE_MKDIR = 9, + FUSE_UNLINK = 10, + FUSE_RMDIR = 11, + FUSE_RENAME = 12, + FUSE_LINK = 13, + FUSE_OPEN = 14, + FUSE_READ = 15, + FUSE_WRITE = 16, + FUSE_STATFS = 17, + FUSE_RELEASE = 18, + FUSE_FSYNC = 20, + FUSE_SETXATTR = 21, + FUSE_GETXATTR = 22, + FUSE_LISTXATTR = 23, + FUSE_REMOVEXATTR = 24, + FUSE_FLUSH = 25, + FUSE_INIT = 26, + FUSE_OPENDIR = 27, + FUSE_READDIR = 28, + FUSE_RELEASEDIR = 29, + FUSE_FSYNCDIR = 30, + FUSE_GETLK = 31, + FUSE_SETLK = 32, + FUSE_SETLKW = 33, + FUSE_ACCESS = 34, + FUSE_CREATE = 35, + FUSE_INTERRUPT = 36, + FUSE_BMAP = 37, + FUSE_DESTROY = 38, + FUSE_IOCTL = 39, + FUSE_POLL = 40, + FUSE_NOTIFY_REPLY = 41, + FUSE_BATCH_FORGET = 42, + FUSE_FALLOCATE = 43, + FUSE_READDIRPLUS = 44, + FUSE_RENAME2 = 45, + FUSE_LSEEK = 46, + FUSE_COPY_FILE_RANGE = 47, + FUSE_SETUPMAPPING = 48, + FUSE_REMOVEMAPPING = 49, + FUSE_SYNCFS = 50, + FUSE_TMPFILE = 51, + FUSE_STATX = 52, + + // CUSE specific operations + CUSE_INIT = 4096, + + // Reserved opcodes: helpful to detect structure endian-ness + CUSE_INIT_BSWAP_RESERVED = 1048576, // CUSE_INIT << 8 + FUSE_INIT_BSWAP_RESERVED = 436207616, // FUSE_INIT << 24 +}; + +struct fuse_entry_out { + uint64_t nodeid; /* Inode ID */ + uint64_t generation; /* Inode generation: nodeid:gen must + be unique for the fs's lifetime */ + uint64_t entry_valid; /* Cache timeout for the name */ + uint64_t attr_valid; /* Cache timeout for the attributes */ + uint32_t entry_valid_nsec; + uint32_t attr_valid_nsec; + struct fuse_attr attr; +}; + +struct fuse_getattr_in { + uint32_t getattr_flags; + uint32_t dummy; + uint64_t fh; +}; + +struct fuse_attr_out { + uint64_t attr_valid; /* Cache timeout for the attributes */ + uint32_t attr_valid_nsec; + uint32_t dummy; + struct fuse_attr attr; +}; + +struct fuse_mknod_in { + uint32_t mode; + uint32_t rdev; + uint32_t umask; + uint32_t padding; +}; + +struct fuse_mkdir_in { + uint32_t mode; + uint32_t umask; +}; + +struct fuse_rename_in { + uint64_t newdir; +}; + +struct fuse_link_in { + uint64_t oldnodeid; +}; + +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_open_in { + uint32_t flags; + uint32_t open_flags; /* FUSE_OPEN_... */ +}; + +struct fuse_create_in { + uint32_t flags; + uint32_t mode; + uint32_t umask; + uint32_t open_flags; /* FUSE_OPEN_... */ +}; + +struct fuse_open_out { + uint64_t fh; + uint32_t open_flags; + uint32_t padding; +}; + +struct fuse_release_in { + uint64_t fh; + uint32_t flags; + uint32_t release_flags; + uint64_t lock_owner; +}; + +struct fuse_flush_in { + uint64_t fh; + uint32_t unused; + uint32_t padding; + uint64_t lock_owner; +}; + +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_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_access_in { + uint32_t mask; + uint32_t padding; +}; + +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 unused[7]; +}; + +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; /* length of extensions in 8byte units */ + uint16_t padding; +}; + +struct fuse_out_header { + uint32_t len; + int32_t error; + uint64_t unique; +}; + +struct fuse_dirent { + uint64_t ino; + uint64_t off; + uint32_t namelen; + uint32_t type; + char name[]; +}; + +/* Align variable length records to 64bit boundary */ +#define FUSE_REC_ALIGN(x) \ + (((x) + sizeof(uint64_t) - 1) & ~(sizeof(uint64_t) - 1)) + +#define FUSE_NAME_OFFSET offsetof(struct fuse_dirent, name) +#define FUSE_DIRENT_ALIGN(x) FUSE_REC_ALIGN(x) +#define FUSE_DIRENT_SIZE(d) \ + FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + (d)->namelen) diff --git a/Kernel/FileSystem/FUSE/FUSEConnection.h b/Kernel/FileSystem/FUSE/FUSEConnection.h new file mode 100644 index 00000000000..5b6ab81da0d --- /dev/null +++ b/Kernel/FileSystem/FUSE/FUSEConnection.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Kernel { + +class FUSEConnection : public RefCounted { +public: + static ErrorOr> try_create(NonnullRefPtr description) + { + if (!description->is_device()) + return Error::from_errno(EINVAL); + + if (description->device()->class_name() != "FUSEDevice"sv) + return Error::from_errno(EINVAL); + + auto connection = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) FUSEConnection(description))); + + auto* device = bit_cast(description->device()); + TRY(device->initialize_instance(*description)); + + return connection; + } + + static ErrorOr> create_request(FUSEOpcode opcode, u32 nodeid, u32 unique, ReadonlyBytes request_body) + { + size_t request_length = sizeof(fuse_in_header) + request_body.size(); + auto request = TRY(KBuffer::try_create_with_size("FUSE: Request"sv, request_length)); + memset(request->data(), 0, request_length); + fuse_in_header* header = bit_cast(request->data()); + header->len = request_length; + header->opcode = to_underlying(opcode); + header->unique = unique; + header->nodeid = nodeid; + + // FIXME: Fill in proper values for these. + header->uid = 0; + header->gid = 0; + header->pid = 1; + + u8* payload = bit_cast(request->data() + sizeof(fuse_in_header)); + memcpy(payload, request_body.data(), request_body.size()); + + return request; + } + + ErrorOr> send_request_and_wait_for_a_reply(FUSEOpcode opcode, u32 nodeid, ReadonlyBytes request_body) + { + auto* device = bit_cast(m_description->device()); + + // FIXME: Send the init request from the filesystem itself right after it has been + // mounted. (Without blocking the mount syscall.) + if (!m_initialized) + TRY(handle_init()); + + auto request = TRY(create_request(opcode, nodeid, m_unique, request_body)); + auto response = TRY(device->send_request_and_wait_for_a_reply(m_description, request->bytes())); + + if (validate_response(*response, m_unique++).is_error()) + return Error::from_errno(EIO); + + return response; + } + + ~FUSEConnection() + { + auto* device = bit_cast(m_description->device()); + // Unblock the userspace daemon and tell it to shut down. + device->shutdown_for_description(m_description); + (void)m_description->close(); + } + +private: + FUSEConnection(NonnullRefPtr description) + : m_description(description) + { + } + + ErrorOr validate_response(KBuffer const& response, u32 unique) + { + if (response.size() < sizeof(fuse_out_header)) { + dmesgln("FUSE: Received a request with a malformed header"); + return Error::from_errno(EINVAL); + } + + fuse_out_header* header = bit_cast(response.data()); + if (header->unique != unique) { + dmesgln("FUSE: Received a mismatched request"); + return Error::from_errno(EINVAL); + } + + if (header->len > response.size()) { + dmesgln("FUSE: Received an excessively large request"); + return Error::from_errno(EINVAL); + } + + return {}; + } + + ErrorOr handle_init() + { + fuse_init_in init_request; + init_request.major = FUSE_KERNEL_VERSION; + init_request.minor = FUSE_KERNEL_MINOR_VERSION; + init_request.max_readahead = 512; + init_request.flags = 0; + + auto* device = bit_cast(m_description->device()); + + auto request = TRY(create_request(FUSEOpcode::FUSE_INIT, 0, 0, { &init_request, sizeof(init_request) })); + auto response = TRY(device->send_request_and_wait_for_a_reply(m_description, request->bytes())); + + if (validate_response(*response, 0).is_error()) + return Error::from_errno(EIO); + + fuse_init_out* init = bit_cast(response->data() + sizeof(fuse_out_header)); + + m_major = init->major; + m_minor = init->minor; + + m_initialized = true; + + return {}; + } + + NonnullRefPtr m_description; + bool m_initialized { false }; + u32 m_unique { 0 }; + + u32 m_major { 0 }; + u32 m_minor { 0 }; +}; + +} diff --git a/Kernel/FileSystem/FUSE/FileSystem.cpp b/Kernel/FileSystem/FUSE/FileSystem.cpp new file mode 100644 index 00000000000..7a0ef643189 --- /dev/null +++ b/Kernel/FileSystem/FUSE/FileSystem.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Kernel { + +struct [[gnu::packed]] FUSESpecificFlagsBytes { + u64 pid; + u64 device_inode; + u64 rootmode; + u64 gid; + u64 uid; +}; + +ErrorOr> FUSE::try_create(ReadonlyBytes mount_flags) +{ + auto* fuse_mount_flags = reinterpret_cast(mount_flags.data()); + + auto description = TRY(Process::current().open_file_description(fuse_mount_flags->device_inode)); + auto connection = TRY(FUSEConnection::try_create(description)); + + u64 rootmode = AK::reinterpret_as_octal(fuse_mount_flags->rootmode); + u64 gid = fuse_mount_flags->gid; + u64 uid = fuse_mount_flags->uid; + return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) FUSE(connection, rootmode, uid, gid))); +} + +ErrorOr FUSE::handle_mount_unsigned_integer_flag(Bytes mount_file_specific_flags_buffer, StringView key, u64 value) +{ + auto* fuse_mount_flags = reinterpret_cast(mount_file_specific_flags_buffer.data()); + if (key == "fd") { + fuse_mount_flags->device_inode = value; + return {}; + } + if (key == "rootmode") { + fuse_mount_flags->rootmode = value; + return {}; + } + if (key == "gid") { + fuse_mount_flags->gid = value; + return {}; + } + if (key == "uid") { + fuse_mount_flags->uid = value; + return {}; + } + return EINVAL; +} + +FUSE::FUSE(NonnullRefPtr connection, u64 rootmode, u64 gid, u64 uid) + : m_connection(connection) + , m_rootmode(rootmode) + , m_gid(gid) + , m_uid(uid) +{ +} + +FUSE::~FUSE() = default; + +ErrorOr FUSE::initialize() +{ + m_root_inode = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) FUSEInode(*this))); + m_root_inode->m_metadata.mode = m_rootmode; + m_root_inode->m_metadata.uid = m_gid; + m_root_inode->m_metadata.gid = m_uid; + m_root_inode->m_metadata.size = 0; + m_root_inode->m_metadata.mtime = TimeManagement::boot_time(); + return {}; +} + +Inode& FUSE::root_inode() +{ + return *m_root_inode; +} + +u8 FUSE::internal_file_type_to_directory_entry_type(DirectoryEntryView const& entry) const +{ + return ram_backed_file_type_to_directory_entry_type(entry); +} + +} diff --git a/Kernel/FileSystem/FUSE/FileSystem.h b/Kernel/FileSystem/FUSE/FileSystem.h new file mode 100644 index 00000000000..36b181d1317 --- /dev/null +++ b/Kernel/FileSystem/FUSE/FileSystem.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Kernel { + +class FUSEInode; + +class FUSE final : public FileSystem { + friend class FUSEInode; + +public: + virtual ~FUSE() override; + static ErrorOr> try_create(ReadonlyBytes mount_flags); + + static ErrorOr handle_mount_unsigned_integer_flag(Bytes mount_file_specific_flags_buffer, StringView key, u64); + + virtual ErrorOr initialize() override; + virtual StringView class_name() const override { return "FUSE"sv; } + + virtual Inode& root_inode() override; + +private: + virtual u8 internal_file_type_to_directory_entry_type(DirectoryEntryView const& entry) const override; + + FUSE(NonnullRefPtr connection, u64 rootmode, u64 gid, u64 uid); + + RefPtr m_root_inode; + NonnullRefPtr m_connection; + u64 m_rootmode { 0 }; + u64 m_gid { 0 }; + u64 m_uid { 0 }; +}; + +} diff --git a/Kernel/FileSystem/FUSE/Inode.cpp b/Kernel/FileSystem/FUSE/Inode.cpp new file mode 100644 index 00000000000..fd0fd66fb74 --- /dev/null +++ b/Kernel/FileSystem/FUSE/Inode.cpp @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Kernel { + +FUSEInode::FUSEInode(FUSE& fs, InodeIndex index) + : Inode(fs, index) +{ +} + +FUSEInode::FUSEInode(FUSE& fs) + : Inode(fs, 1) +{ +} + +FUSEInode::~FUSEInode() = default; + +ErrorOr FUSEInode::read_bytes_locked(off_t offset, size_t size, UserOrKernelBuffer& buffer, OpenFileDescription*) const +{ + VERIFY(m_inode_lock.is_locked()); + VERIFY(!is_directory()); + + constexpr size_t max_read_size = 0x21000 - sizeof(fuse_in_header) - sizeof(fuse_read_in); + u64 id = TRY(try_open(false, O_RDONLY)); + u32 nodeid = identifier().index().value(); + + size_t nread = 0; + size_t target_size = size; + while (target_size) { + size_t chunk_size = min(size, max_read_size); + fuse_read_in payload {}; + payload.fh = id; + payload.offset = offset + nread; + payload.size = chunk_size; + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_READ, nodeid, { &payload, sizeof(payload) })); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + u32 data_size = min(target_size, header->len - sizeof(fuse_out_header)); + if (data_size == 0) + break; + + u8* data = bit_cast(response->data() + sizeof(fuse_out_header)); + + TRY(buffer.write(data, nread, data_size)); + nread += data_size; + target_size -= data_size; + } + + TRY(try_flush(id)); + TRY(try_release(id, false)); + + return nread; +} + +ErrorOr FUSEInode::write_bytes_locked(off_t offset, size_t size, UserOrKernelBuffer const& buffer, OpenFileDescription*) +{ + VERIFY(m_inode_lock.is_locked()); + VERIFY(!is_directory()); + VERIFY(offset >= 0); + + constexpr size_t max_write_size = 0x21000 - sizeof(fuse_in_header) - sizeof(fuse_write_in); + u64 id = TRY(try_open(false, O_WRONLY)); + u32 nodeid = identifier().index().value(); + + size_t nwritten = 0; + while (size) { + size_t chunk_size = min(size, max_write_size); + auto request_buffer = TRY(KBuffer::try_create_with_size("FUSE: Write buffer"sv, sizeof(fuse_write_in) + chunk_size)); + fuse_write_in* write_header = bit_cast(request_buffer->data()); + write_header->fh = id; + write_header->offset = offset + nwritten; + write_header->size = chunk_size; + TRY(buffer.read(request_buffer->data() + sizeof(fuse_write_in), nwritten, chunk_size)); + + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_WRITE, nodeid, request_buffer->bytes())); + if (response->size() < sizeof(fuse_out_header) + sizeof(fuse_write_out)) + return Error::from_errno(EIO); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + fuse_write_out* write_response = bit_cast(response->data() + sizeof(fuse_out_header)); + + nwritten += write_response->size; + size -= write_response->size; + } + + TRY(try_flush(id)); + TRY(try_release(id, false)); + + return nwritten; +} + +InodeMetadata FUSEInode::metadata() const +{ + InodeMetadata metadata; + metadata.inode = identifier(); + u32 id = identifier().index().value(); + + fuse_getattr_in payload {}; + + auto response_or_error = fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_GETATTR, id, { &payload, sizeof(payload) }); + + if (response_or_error.is_error()) + return {}; + + auto response = response_or_error.release_value(); + fuse_out_header* header = bit_cast(response->data()); + if (header->error || response->size() < sizeof(fuse_out_header) + sizeof(fuse_attr_out)) + return {}; + + fuse_attr_out* getattr_response = bit_cast(response->data() + sizeof(fuse_out_header)); + + metadata.mode = getattr_response->attr.mode; + metadata.size = getattr_response->attr.size; + metadata.block_size = getattr_response->attr.blksize; + metadata.block_count = getattr_response->attr.blocks; + + metadata.uid = getattr_response->attr.uid; + metadata.gid = getattr_response->attr.gid; + metadata.link_count = getattr_response->attr.nlink; + metadata.atime = UnixDateTime::from_seconds_since_epoch(getattr_response->attr.atime); + metadata.ctime = UnixDateTime::from_seconds_since_epoch(getattr_response->attr.ctime); + metadata.mtime = UnixDateTime::from_seconds_since_epoch(getattr_response->attr.mtime); + metadata.major_device = major_from_encoded_device(getattr_response->attr.rdev); + metadata.minor_device = minor_from_encoded_device(getattr_response->attr.rdev); + + return metadata; +} + +ErrorOr FUSEInode::try_open(bool directory, u32 flags) const +{ + u32 id = identifier().index().value(); + + fuse_open_in payload {}; + payload.flags = flags; + + auto opcode = directory ? FUSEOpcode::FUSE_OPENDIR : FUSEOpcode::FUSE_OPEN; + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(opcode, id, { &payload, sizeof(payload) })); + if (response->size() < sizeof(fuse_out_header) + sizeof(fuse_open_out)) + return Error::from_errno(EIO); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + fuse_open_out* open_response = bit_cast(response->data() + sizeof(fuse_out_header)); + return open_response->fh; +} + +ErrorOr FUSEInode::try_flush(u64 id) const +{ + u32 nodeid = identifier().index().value(); + + fuse_flush_in payload {}; + payload.fh = id; + (void)TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_FLUSH, nodeid, { &payload, sizeof(payload) })); + + return {}; +} + +ErrorOr FUSEInode::try_release(u64 id, bool directory) const +{ + u32 nodeid = identifier().index().value(); + + fuse_release_in payload {}; + payload.fh = id; + auto opcode = directory ? FUSEOpcode::FUSE_RELEASEDIR : FUSEOpcode::FUSE_RELEASE; + (void)TRY(fs().m_connection->send_request_and_wait_for_a_reply(opcode, nodeid, { &payload, sizeof(payload) })); + + return {}; +} + +static size_t get_dirent_entry_length(size_t name_length) +{ + return name_length + FUSE_NAME_OFFSET; +} + +static size_t get_dirent_entry_length_padded(size_t name_length) +{ + return FUSE_DIRENT_ALIGN(get_dirent_entry_length(name_length)); +} + +ErrorOr FUSEInode::traverse_as_directory(Function(FileSystem::DirectoryEntryView const&)> callback) const +{ + u64 id = TRY(try_open(true, 0)); + u32 nodeid = identifier().index().value(); + + fuse_read_in payload {}; + payload.fh = id; + payload.size = 4096; + while (true) { + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_READDIR, nodeid, { &payload, sizeof(payload) })); + fuse_out_header* header = bit_cast(response->data()); + if (header->len == sizeof(fuse_out_header)) + break; + char* dirents = bit_cast(response->data() + sizeof(fuse_out_header)); + u32 total_size = header->len - sizeof(fuse_out_header); + u32 offset = 0; + while (offset < total_size) { + fuse_dirent* dirent = bit_cast(dirents + offset); + if (dirent->ino == 0) + break; + + if (dirent->namelen > NAME_MAX) + return Error::from_errno(EIO); + + TRY(callback({ { dirent->name, dirent->namelen }, { fsid(), dirent->ino }, to_underlying(ram_backed_file_type_from_mode(dirent->type << 12)) })); + offset += get_dirent_entry_length_padded(dirent->namelen); + } + payload.offset = offset; + } + + TRY(try_release(id, true)); + return {}; +} + +ErrorOr> FUSEInode::lookup(StringView name) +{ + auto name_buffer = TRY(KBuffer::try_create_with_size("FUSE: Lookup name string"sv, name.length() + 1)); + memset(name_buffer->data(), 0, name_buffer->size()); + memcpy(name_buffer->data(), name.characters_without_null_termination(), name.length()); + + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_LOOKUP, identifier().index().value(), name_buffer->bytes())); + if (response->size() < sizeof(fuse_out_header) + sizeof(fuse_entry_out)) + return Error::from_errno(EIO); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + fuse_entry_out* entry = bit_cast(response->data() + sizeof(fuse_out_header)); + + return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) FUSEInode(fs(), entry->nodeid))); +} + +ErrorOr FUSEInode::flush_metadata() +{ + return {}; +} + +ErrorOr FUSEInode::add_child(Inode&, StringView, mode_t) +{ + return ENOTIMPL; +} + +ErrorOr> FUSEInode::create_child(StringView, mode_t, dev_t, UserID, GroupID) +{ + return ENOTIMPL; +} + +ErrorOr FUSEInode::remove_child(StringView) +{ + return ENOTIMPL; +} + +ErrorOr FUSEInode::replace_child(StringView, Inode&) +{ + return ENOTIMPL; +} + +ErrorOr FUSEInode::chmod(mode_t) +{ + return ENOTIMPL; +} + +ErrorOr FUSEInode::chown(UserID, GroupID) +{ + return ENOTIMPL; +} + +ErrorOr FUSEInode::truncate_locked(u64 new_size) +{ + VERIFY(m_inode_lock.is_locked()); + VERIFY(!is_directory()); + u64 id = TRY(try_open(is_directory(), 0)); + + fuse_setattr_in setattr {}; + setattr.fh = id; + setattr.valid = FATTR_SIZE; + setattr.size = new_size; + + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_SETATTR, identifier().index().value(), { &setattr, sizeof(setattr) })); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + return try_release(id, is_directory()); +} + +ErrorOr FUSEInode::update_timestamps(Optional atime, Optional ctime, Optional mtime) +{ + MutexLocker locker(m_inode_lock); + + u64 id = TRY(try_open(is_directory(), 0)); + fuse_setattr_in setattr {}; + setattr.fh = id; + + if (atime.has_value()) { + setattr.valid |= FATTR_ATIME; + setattr.atime = atime.value().to_timespec().tv_sec; + } + if (ctime.has_value()) { + setattr.valid |= FATTR_CTIME; + setattr.ctime = ctime.value().to_timespec().tv_sec; + } + if (mtime.has_value()) { + setattr.valid |= FATTR_MTIME; + setattr.mtime = mtime.value().to_timespec().tv_sec; + } + + auto response = TRY(fs().m_connection->send_request_and_wait_for_a_reply(FUSEOpcode::FUSE_SETATTR, identifier().index().value(), { &setattr, sizeof(setattr) })); + + fuse_out_header* header = bit_cast(response->data()); + if (header->error) + return Error::from_errno(-header->error); + + return try_release(id, is_directory()); +} + +} diff --git a/Kernel/FileSystem/FUSE/Inode.h b/Kernel/FileSystem/FUSE/Inode.h new file mode 100644 index 00000000000..3f6333044da --- /dev/null +++ b/Kernel/FileSystem/FUSE/Inode.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Kernel { + +class FUSEInode final : public Inode { + friend class FUSE; + +public: + virtual ~FUSEInode() override; + + FUSE& fs() { return static_cast(Inode::fs()); } + FUSE const& fs() const { return static_cast(Inode::fs()); } + +private: + FUSEInode(FUSE&, InodeIndex); + FUSEInode(FUSE&); + + // ^Inode + virtual ErrorOr read_bytes_locked(off_t, size_t, UserOrKernelBuffer& buffer, OpenFileDescription*) const override; + virtual InodeMetadata metadata() const override; + virtual ErrorOr traverse_as_directory(Function(FileSystem::DirectoryEntryView const&)>) const override; + virtual ErrorOr> lookup(StringView name) override; + virtual ErrorOr flush_metadata() override; + virtual ErrorOr write_bytes_locked(off_t, size_t, UserOrKernelBuffer const& buffer, OpenFileDescription*) override; + virtual ErrorOr> create_child(StringView name, mode_t, dev_t, UserID, GroupID) override; + virtual ErrorOr add_child(Inode&, StringView name, mode_t) override; + virtual ErrorOr remove_child(StringView name) override; + virtual ErrorOr replace_child(StringView name, Inode& child) override; + virtual ErrorOr chmod(mode_t) override; + virtual ErrorOr chown(UserID, GroupID) override; + virtual ErrorOr truncate_locked(u64) override; + virtual ErrorOr update_timestamps(Optional atime, Optional ctime, Optional mtime) override; + + ErrorOr try_open(bool directory, u32 flags) const; + ErrorOr try_flush(u64 id) const; + ErrorOr try_release(u64 id, bool directory) const; + + InodeMetadata m_metadata; +}; + +} diff --git a/Kernel/FileSystem/VirtualFileSystem.cpp b/Kernel/FileSystem/VirtualFileSystem.cpp index c3c670663a0..e62c83f80b3 100644 --- a/Kernel/FileSystem/VirtualFileSystem.cpp +++ b/Kernel/FileSystem/VirtualFileSystem.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,7 @@ static constexpr FileSystemInitializer s_initializers[] = { { "iso9660"sv, "ISO9660FS"sv, true, true, true, ISO9660FS::try_create, {}, handle_mount_boolean_flag_as_invalid, handle_mount_unsigned_integer_flag_as_invalid, handle_mount_signed_integer_flag_as_invalid, handle_mount_ascii_string_flag_as_invalid }, { "fat"sv, "FATFS"sv, true, true, true, FATFS::try_create, {}, handle_mount_boolean_flag_as_invalid, handle_mount_unsigned_integer_flag_as_invalid, handle_mount_signed_integer_flag_as_invalid, handle_mount_ascii_string_flag_as_invalid }, { "devloop"sv, "DevLoopFS"sv, false, false, false, {}, DevLoopFS::try_create, handle_mount_boolean_flag_as_invalid, handle_mount_unsigned_integer_flag_as_invalid, handle_mount_signed_integer_flag_as_invalid, handle_mount_ascii_string_flag_as_invalid }, + { "fuse"sv, "FUSE"sv, false, false, false, {}, FUSE::try_create, handle_mount_boolean_flag_as_invalid, FUSE::handle_mount_unsigned_integer_flag, handle_mount_signed_integer_flag_as_invalid, handle_mount_ascii_string_flag_as_invalid }, }; ErrorOr VirtualFileSystem::find_filesystem_type_initializer(StringView fs_type) diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 8974c0a98ac..a9c18b98475 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -55,6 +55,7 @@ set(FILE_WATCHER_DEBUG ON) set(FILL_PATH_DEBUG ON) set(FLAC_ENCODER_DEBUG ON) set(FORK_DEBUG ON) +set(FUSE_DEBUG ON) set(FUTEX_DEBUG ON) set(FUTEXQUEUE_DEBUG ON) set(GEMINI_DEBUG ON) diff --git a/Meta/check-style.py b/Meta/check-style.py index 1e1b6904f45..2d1d090e819 100755 --- a/Meta/check-style.py +++ b/Meta/check-style.py @@ -77,6 +77,8 @@ def should_check_file(filename): return False if filename == 'Kernel/FileSystem/Ext2FS/Definitions.h': return False + if filename == 'Kernel/FileSystem/FUSE/Definitions.h': + return False return True diff --git a/Userland/Services/SystemServer/main.cpp b/Userland/Services/SystemServer/main.cpp index 6b2852c9420..44adea4c7ac 100644 --- a/Userland/Services/SystemServer/main.cpp +++ b/Userland/Services/SystemServer/main.cpp @@ -139,6 +139,7 @@ static ErrorOr prepare_bare_minimum_devtmpfs_directory_structure() TRY(Core::System::create_char_device("/dev/console"sv, 0666, 5, 1)); TRY(Core::System::create_char_device("/dev/ptmx"sv, 0666, 5, 2)); TRY(Core::System::create_char_device("/dev/tty"sv, 0666, 5, 0)); + TRY(Core::System::create_char_device("/dev/fuse"sv, 0666, 10, 229)); #ifdef ENABLE_KERNEL_COVERAGE_COLLECTION TRY(Core::System::create_block_device("/dev/kcov"sv, 0666, 30, 0)); #endif