Serenity OS remount(2) 指南:动态调整挂载标志以约束文件系统的安全行为 Serenity OS remount(2) 指南动态调整挂载标志以约束文件系统的安全行为【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity本文基于 Serenity OS 的remount(2)手册页Base/usr/share/man/man2/remount.md系统讲解如何通过remount()系统调用为一个已挂载的文件系统更换挂载标志mount flags并结合用户态LibCore封装与内核VirtualFileSystem的源码实现说明每个标志在内核打开/创建路径中的具体生效位置、权限与 Pledge 约束以及 remount 语义上的常见陷阱已打开的文件描述符不会继承新标志。读完本文你可以在 Serenity OS 中安全地以MS_NODEV、MS_NOEXEC、MS_RDONLY等标志收紧或放松某个挂载点的安全策略并理解其背后的内核调用链与错误处理逻辑。1. 接口定义与功能定位remount()的作用非常单一对已经挂载在target路径上的文件系统整体替换其挂载标志。它不挂载新文件系统也不改变挂载点位置只负责修改该挂载点的行为约束。手册页给出的原型为#include LibCore/System.h ErrorOrvoid remount(StringView target, int flags);实际的用户态封装位于 Userland/Libraries/LibCore/System.cpp 中的Core::System::remount()其参数列表比手册页多了一个可选的 VFS 根上下文 ID用于 Serenity 的多 VFS 根上下文机制ErrorOrvoid remount(Optionali32 vfs_context_id, StringView target, int flags) { if (target.is_null()) return Error::from_errno(EFAULT); Syscall::SC_remount_params params { vfs_context_id.value_or(-1), { target.characters_without_null_termination(), target.length() }, flags }; int rc syscall(SC_remount, params); HANDLE_SYSCALL_RETURN_VALUE(remount, rc, {}); }从源码结构看vfs_context_id缺省为-1表示使用调用进程自己的 VFS 根上下文target为空指针时直接返回EFAULT。一个便于记忆的细节同一个LibCore中的mount()封装内部会把传统 POSIX 风格的MS_REMOUNT标志转发给remount()见 System.cpp 中mount()对flags MS_REMOUNT的分支因此老代码调用mount(MS_REMOUNT)在新 API 下仍然等价于调用remount()。2. 支持的挂载标志及其位值手册页列出remount()接受的标志集合这些标志的定义与位值可在内核 API 头文件 Kernel/API/POSIX/unistd.h 中确认标志位值含义MS_NODEV1 0禁止从该文件系统打开任何设备文件MS_NOEXEC1 1禁止执行该文件系统上的任何可执行文件MS_NOSUID1 2忽略该文件系统上可执行文件的 set-user-id 位MS_RDONLY1 4将该文件系统以只读方式挂载MS_WXALLOWED1 6允许绕过该文件系统上可执行文件的 W^X 保护MS_AXALLOWED1 7允许该文件系统上可执行文件进行匿名可执行映射MS_NOREGULAR1 8禁止打开该文件系统上的任何常规文件手册页同时明确指出这些标志可以被用作安全措施security measure限制挂载文件系统可能被滥用的途径。典型用法例如给一个存放用户上传内容的挂载点加上MS_NODEV | MS_NOEXEC | MS_NOSUID防止其中的设备节点被打开、可执行文件被运行、SUID 位被利用MS_WXALLOWED与MS_AXALLOWED则属于放宽方向的标志用于允许特定场景下绕过 W^X 类限制。3. 内核实现sys$remount 的权限检查与标志替换系统调用入口是内核中的Process::sys$remount实现于 Kernel/Syscalls/mount.cppErrorOrFlatPtr Process::sys$remount(UserspaceSyscall::SC_remount_params const* user_params) { VERIFY_NO_PROCESS_BIG_LOCK(this); TRY(require_promise(Pledge::mount)); auto credentials this-credentials(); if (!credentials-is_superuser()) return EPERM; auto params TRY(copy_typed_from_user(user_params)); if (params.flags MS_REMOUNT) return EINVAL; if (params.flags MS_BIND) return EINVAL; auto target TRY(try_copy_kstring_from_user(params.target)); auto current_vfs_root_context Process::current().vfs_root_context(); auto mount_target_context TRY(context_for_mount_operation(params.vfs_root_context_id, target-view())); TRY(VirtualFileSystem::remount(mount_target_context.vfs_root_context, mount_target_context.custody, params.flags)); return 0; }由此可以提炼出调用remount()的三条硬约束Pledge 约束TRY(require_promise(Pledge::mount))要求调用进程必须持有mount承诺否则系统调用直接失败。超级用户约束非超级用户进程返回EPERM与手册页 Errors 一节一致。禁止携带MS_REMOUNT/MS_BINDremount是专职系统调用传入这两个标志会返回EINVAL。这与手册页flags 值包含已废弃标志如MS_REMOUNT或MS_BIND时返回EINVAL完全对应——在这套 API 中它们不再是mount()的分发开关而是被显式拒绝的输入。随后内核通过context_for_mount_operation()定位target所在挂载点找不到挂载点即对应手册页所列的ENODEV错误最终调用VirtualFileSystem::remount()完成标志替换。4. 标志如何被替换VirtualFileSystem::remount真正的标志写入逻辑在 Kernel/FileSystem/VirtualFileSystem.cppErrorOrvoid VirtualFileSystem::remount(VFSRootContext context, Custody mount_point, int new_flags) { dbgln(VirtualFileSystem: Remounting inode {}, mount_point.inode().identifier()); TRY(context.apply_to_mount_for_host_custody(mount_point, new_flags { mount.set_flags(new_flags); })); return {}; }注意其语义是整体替换mount.set_flags(new_flags)用new_flags覆盖挂载点当前的标志位而不是与旧标志做位或/位与运算。因此调用方必须一次传齐所有希望保留的标志遗漏某个标志等于关闭对应约束。apply_to_mount_for_host_custody()以挂载点 inode 的 Custody 为锚点找到宿主挂载对象并施加回调这也解释了为什么target必须是该文件系统挂载点所在路径——内核以target解析出的 Custody 反查其所属挂载。5. 各标志在打开路径中的实际生效点标志只是意图真正的执行发生在 VFS 的打开/创建路径中。在 Kernel/FileSystem/VirtualFileSystem.cpp 的open()实现中可以看到各标志的检查点MS_NOREGULAR当目标元数据是常规文件时直接返回EACCESmetadata.is_regular_file() (custody.mount_flags() MS_NOREGULAR)创建常规文件时同样检查该标志并返回EACCES见 VirtualFileSystem.cpp 的create()路径MS_NOEXEC在带O_EXEC的打开请求中即使文件本身允许执行挂载标志位也会使打开返回EACCES!metadata.may_execute(credentials) || (custody.mount_flags() MS_NOEXEC)MS_NODEV目标是设备 inode 时检查custody.mount_flags() MS_NODEV命中即返回EACCES设备对象根本不会被查询和打开MS_RDONLY在 Custody 层面生效Kernel/FileSystem/Custody.cpp 的Custody::is_readonly()只要挂载标志含MS_RDONLY即视为只读后续create()等写操作路径会据此返回EROFS。这些检查都基于 Custody 上缓存的挂载标志位custody.mount_flags()这正是下一节已打开文件不受 remount 影响这一语义的根源。6. 错误码汇总手册页 Errors 一节与内核实现相互印证EINVALflags中包含已废弃标志MS_REMOUNT或MS_BINDKernel/Syscalls/mount.cpp 显式检查EPERM当前进程不具备超级用户权限Kernel/Syscalls/mount.cppENODEVtarget路径上未找到挂载点其余常规的路径解析错误也可能发生例如target分量不存在时的ENOENT等。此外用户态封装在target为空指针时返回EFAULTUserland/Libraries/LibCore/System.cpp。7. remount 的语义边界只对未来操作生效这是使用remount()时最容易踩的坑配套的 mount(2) 手册页 中Remounting一节给出了精确说明remount 只影响此后对文件系统的操作不影响已经打开的文件。例如你先打开了一个挂载在MS_NODEV文件系统上的目录然后把该文件系统 remount 为允许打开设备此后通过该目录描述符做openat()打开设备仍然会失败因为 Custody/文件描述符持有的是打开时刻的挂载标志快照已运行进程的当前工作目录和根目录同理它们不会自动感知底层挂载标志的变化。若想在 remount 之后让工作目录采用新标志进程需要调用chdir()重新chdir到同一路径以刷新。从源码结构看这与第 5 节一致标志检查读取的是 Custody 上的m_mount_flags而VirtualFileSystem::remount()只更新挂载对象上的标志mount.set_flags(new_flags)不会去修改已经存在于进程表、目录项缓存或 fd 表中的旧 Custody 副本。8. 与 mount(2)、bindmount(2) 的关系mount(2)负责建立新挂载支持Ext2FS、ProcFS、DevPtsFS、RAMFS、Plan9FS等类型且支持MS_BIND绑定挂载标志集合与remount()完全相同可参见 Base/usr/share/man/man2/mount.mdbindmount(2)是mount(MS_BIND)的专职版本remount()同样是mount(MS_REMOUNT)的专职版本——三者的MS_REMOUNT/MS_BIND交叉使用都会得到EINVALmount(2)手册页还定义了MS_IMMUTABLE不可变挂载等remount()文档未列出的标志remount 时传入未列标志的行为请以 Kernel/API/POSIX/unistd.h 的完整标志定义与内核实现为准。9. 小结remount(2)是 Serenity OS 挂载体系中变更策略的最小入口调用方需持有mountPledge 且为超级用户通过SC_remount系统调用将一组完整的挂载标志整体替换到目标挂载点上MS_NODEV、MS_NOEXEC、MS_NOSUID、MS_RDONLY、MS_NOREGULAR等标志随后在 VFS 的 open/create 路径中逐点强制执行。理解标志是整体替换而非增量修改以及已打开的描述符不受影响这两点是正确编写依赖 remount 的安全策略代码的关键。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考