资讯动态

SerenityOS 的 pledge(2) 系统调用:基于“承诺“机制的进程自我沙箱化

发布时间:2026/9/10 6:28:40 来源:尧图企业网站定制
SerenityOS 的 pledge(2) 系统调用基于承诺机制的进程自我沙箱化【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity本文以 SerenityOS 手册页Base/usr/share/man/man2/pledge.md为主体深入讲解pledge()系统调用的语义、完整承诺promises列表与错误码并结合内核源码剖析其位掩码解析、能力单调收缩、fork/exec 继承以及违例终止的完整实现链路帮助读者掌握在 SerenityOS 上为进程做自我沙箱化的原理与落地方式。一、pledge() 是什么pledge()是 SerenityOS 中用于收缩进程能力的系统调用。它的核心思想是进程主动向内核作出承诺promise声明从此刻起只使用系统功能的一个子集功能被划分成一组经过筛选的承诺promises程序可以用空格分隔的列表把它们组合起来覆盖自身需求。一旦进程尝试使用它曾经承诺不再使用的系统功能该进程会立即被终止。这一机制源自 OpenBSD 的同名系统调用用于限制漏洞利用的影响范围或在故意执行不可信代码之前预先收窄权限。手册页中明确写道SerenityOS 的实现与 OpenBSD 存在多处差异且并非最终版本因此本文所有细节均以当前仓库代码为准。二、接口与调用语义手册页给出的原型来自 pledge 手册页#include unistd.h int pledge(const char* promises, const char* execpromises);两个参数都是空格分隔的承诺列表语义上有明确分工promises作用于当前进程并且会被 [fork(2)] 创建的子进程继承execpromises仅当进程通过exec系列调用创建新进程映像时才生效任一参数为NULL时对应的值保持不变不会被清除可以反复调用pledge()以移除此前已授予的承诺但能力一旦失去就永远无法恢复能力只减不增。从源码可以印证这套继承语义在 fork 实现 中子进程直接复制父进程的has_promises状态而在 execve 实现 中进程映像切换完成后会执行protected_data.has_promises protected_data.has_execpromises;即用预先登记的 execpromises 取代当前承诺——这正是execpromises 在 exec 时才生效的内核侧实现。一个符合该语义的典型用法模式按手册页描述的规则组织// 1. 先声明当前阶段需要的全部能力并预告 exec 后的承诺 pledge(stdio rpath inet exec, stdio rpath); // 2. 完成网络读取等需要更多能力的工作之后主动收缩 pledge(stdio rpath, nullptr); // nullptrexecpromises 不变 // 3. 此后任何对 socket 的操作都会触发违例终止三、完整承诺promises列表手册页定义了一个经过筛选的承诺集合。下表完整继承原文档的 29 个承诺及其含义带(\*)标注的是SerenityOS 特有扩展原 OpenBSDpledge()不支持承诺说明SerenityOS 扩展stdio基础 I/O、内存分配、关于自身的信息、若干非破坏性系统调用threadPOSIX 线程 API是id修改 UID/GID 的能力tty与 TTY 相关的功能proc进程与调度相关的功能execexec系统调用unixUNIX 本地域套接字inetIPv4 域套接字accept允许在已监听的套接字上调用accept(2) 接受连接是rpath文件系统读访问wpath文件系统写访问cpath文件系统创建访问dpath创建新的设备文件chown修改文件属主/属组fattr修改文件属性/权限video允许对帧缓冲视频设备使用ioctl(2) 与mmap(2)settime修改系统时间与日期setkeymap修改系统键盘布局是sigaction修改信号处理函数与处置方式是sendfd通过本地套接字发送文件描述符recvfd通过本地套接字接收文件描述符ptraceptrace(2) 系统调用是prot_exec带PROT_EXEC的mmap(2) 与mprotect(2)map_fixed带MAP_FIXED或MAP_FIXED_NOREPLACE的mmap(2)是mountmount(2) 及各类文件系统挂载相关系统调用是unshare各类 unshare 专属系统调用是no_error向后忽略提升承诺的请求请求不会被授予只是被忽略便于在子进程想提前申请更多能力时强制执行 execpromises效果类似于 OpenBSD 的error承诺值得注意的一个实现细节内核 Process.h 中的Pledge枚举enum class Pledge : u32通过ENUMERATE_PLEDGE_PROMISES宏表生成还额外包含一个getkeymap枚举项而手册页的承诺列表中并未列出它。从源码结构看这是一个内核内部枚举与用户可见手册页之间暂时不同步的点面向用户编程时仍应以手册页所列集合为准。四、内核实现解析、校验与单调收缩pledge()的内核实现位于 Kernel/Syscalls/pledge.cpp整段逻辑可以拆成四步1. 拷贝与固定长度缓冲。系统调用入口Process::sys$pledge先经copy_typed_from_user拷贝参数再把两个字符串拷入FixedStringBufferall_promises_strings_length_with_spaces。这个编译期常量在 Process.h 中由宏展开累加每个承诺字符串的长度得到并附带static_assert(all_promises_strings_length_with_spaces 1024)的健全性检查——它同时构成了E2BIG错误的判定边界承诺字符串超过所有已知承诺串拼接起来的总长即视为非法。2. 空格分隔解析为位掩码。parse_pledge对字符串做for_each_split_view( , SplitBehavior::Nothing)拆分每个 token 与宏展开出的承诺名逐一比较命中则mask | (1u (u32)Pledge::x)。任一分片不匹配即返回EINVAL一个或多个无效承诺。由于Pledge是u32枚举每个承诺对应掩码中的一个位整个承诺集合因此可以紧凑地存放在一个 32 位整数里。3. 单调收缩校验与no_error语义。核心检查是if (promises_provided) { if (protected_data.has_promises (new_promises ~protected_data.promises)) { if (!(protected_data.promises (1u (u32)Pledge::no_error))) return EPERM; new_promises protected_data.promises; } }即新承诺中出现任何当前掩码中不存在的位能力提升时默认返回EPERM但如果当前承诺中已包含no_error则不报错而是用new_promises protected_data.promises把多申请的位静默截掉——这与手册页no_error只是忽略提升请求、并不授予它们的描述逐字对应。execpromises走完全对称的检查。4. 先全部校验、后原子落盘。源码中有一条明确注释所有校验通过后才写入承诺避免先应用了 promises、之后解析 execpromises 又失败这类把调用方留在非预期状态的逻辑 bug。承诺状态保存在Process::ProtectedValues见 Process.h的has_promises/promises与has_execpromises/execpromises四个字段中由进程级锁保护。has_*标志区分从未调用过 pledge与调用过但承诺为空——前者按手册页规定不受任何限制从未调用过 pledge 的进程视为未作任何承诺。五、违例如何被处置内核各系统调用点通过两个辅助函数Process.cpp实施承诺require_promise(Pledge promise)若进程未作任何承诺则直接放行若已承诺且包含该承诺则放行否则打印Process has not pledged promise诊断、把线程标记为set_promise_violation_pending(true)并返回内部错误码EPROMISEVIOLATIONrequire_no_promises()语义相反用于一旦做过 pledge 就禁止的操作。违例时内核还会调用try_set_coredump_property(pledge_violationsv, to_string(promise))把触发违例的具体承诺名写入核心转储属性——这为事后诊断到底违反了哪条承诺留了可验证的痕迹也对应手册页进程立即被终止的运行时表现。此外承诺状态对系统可见ProcFS 的进程信息在 ProcessExposed.cpp 中通过has_promises()判断是否向/proc暴露该进程的承诺信息。六、错误码汇总手册页给出的错误返回与内核实现一一对应错误码含义源码依据EFAULTpromises和/或execpromises非空但不在可读用户内存中用户态字符串拷贝阶段EINVAL指定了一个或多个无效承诺pledge.cpp 的解析失败路径EPERM尝试提升能力被拒绝pledge.cpp 的位掩码提升检查no_error承诺下转为忽略E2BIGpromises或execpromises字符串长于所有已知承诺串总长固定长度缓冲all_promises_strings_length_with_spaces的容量边界七、历史与相关机制pledge()系统调用最初由 OpenBSD 引入手册页同时声明 SerenityOS 的实现在许多方面有所不同并且绝非最终形态——本文第三、四节所列的*扩展承诺与no_error的忽略而非拒绝语义正是两处可见的分歧点。在 SerenityOS 的安全机制体系中pledge(2)常与unveil(2)配套使用前者收缩系统调用能力后者收窄路径可见性二者均属于内核层面的缓解措施Mitigations。需要强调的是pledge()是一种协作式机制——它依赖程序自愿调用内核不会强制任何进程作出承诺因此适合作为应用自我沙箱化的手段而非对外部进程的强制隔离。参考路径手册页Base/usr/share/man/man2/pledge.md系统调用实现Kernel/Syscalls/pledge.cpp承诺枚举与位掩码基础设施Kernel/Tasks/Process.h违例处置Kernel/Tasks/Process.cpp承诺继承Kernel/Syscalls/fork.cpp、Kernel/Syscalls/execve.cpp【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价