SerenityOS 的 pledge(2) 系统调用:基于“承诺“机制的进程自我沙箱化
2026/9/10 6:28:30 网站建设 项目流程

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); // nullptr:execpromises 不变 // 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_execPROT_EXECmmap(2) 与mprotect(2)
map_fixedMAP_FIXEDMAP_FIXED_NOREPLACEmmap(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拷贝参数,再把两个字符串拷入FixedStringBuffer<all_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("一个或多个无效承诺")。由于Pledgeu32枚举,每个承诺对应掩码中的一个位,整个承诺集合因此可以紧凑地存放在一个 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/promiseshas_execpromises/execpromises四个字段中,由进程级锁保护。has_*标志区分"从未调用过 pledge"与"调用过但承诺为空"——前者按手册页规定不受任何限制("从未调用过 pledge 的进程视为未作任何承诺")。

五、违例如何被处置

内核各系统调用点通过两个辅助函数(Process.cpp)实施承诺:

  • require_promise(Pledge promise):若进程未作任何承诺则直接放行;若已承诺且包含该承诺则放行;否则打印Process has not pledged '<promise>'诊断、把线程标记为set_promise_violation_pending(true),并返回内部错误码EPROMISEVIOLATION
  • require_no_promises():语义相反,用于"一旦做过 pledge 就禁止"的操作。

违例时内核还会调用try_set_coredump_property("pledge_violation"sv, to_string(promise)),把触发违例的具体承诺名写入核心转储属性——这为事后诊断"到底违反了哪条承诺"留了可验证的痕迹,也对应手册页"进程立即被终止"的运行时表现。

此外,承诺状态对系统可见:ProcFS 的进程信息在 ProcessExposed.cpp 中通过has_promises()判断是否向/proc暴露该进程的承诺信息。

六、错误码汇总

手册页给出的错误返回(与内核实现一一对应):

错误码含义源码依据
EFAULTpromises和/或execpromises非空但不在可读用户内存中用户态字符串拷贝阶段
EINVAL指定了一个或多个无效承诺pledge.cpp 的解析失败路径
EPERM尝试提升能力被拒绝pledge.cpp 的位掩码提升检查(no_error承诺下转为忽略)
E2BIGpromisesexecpromises字符串长于所有已知承诺串总长固定长度缓冲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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询