☰
SerenityOS unveil(1) 命令指南:在受限路径白名单下安全运行任意命令
2026/10/5 22:10:31 网站建设 项目流程

SerenityOS unveil(1) 命令指南:在受限路径白名单下安全运行任意命令

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

导读

unveil是 SerenityOS 提供的一个命令行工具,它的核心作用是:在启动一个目标程序之前,预先通过unveil(2)系统调用为它设置文件系统访问白名单(veil),然后在受限环境下执行该程序。读完本文,你将掌握unveil(1)的完整语法与参数格式、五种权限字符(r/w/x/c/b)的确切语义、它的底层系统调用实现原理,以及如何在日常 Shell 操作中用它隔离那些你不完全信任的命令。

命令概览:unveil是什么

unveil(1)是一个用于"在受限路径下运行命令"的工具。手册对其功能给出了一句非常精炼的定义:

Run a command under certain path restrictions by usingunveil(2)。

也就是说,它本身并不实现任何权限判断逻辑,而是作为unveil(2)系统调用的命令行封装:先通过系统调用为即将执行的程序搭建好路径白名单,随后exec出真正的目标命令。

其基本调用形式为:

$ unveil [--path] [command...]

从实现上看,该工具位于 Userland/Utilities/unveil.cpp,其主流程非常清晰:

  1. 用Core::ArgsParser解析参数,并在遇到第一个非选项参数后停止选项解析(set_stop_on_first_non_option(true)),从而保证目标命令自身的参数不会被误当作 unveil 的选项;
  2. 校验至少提供了一个--path,否则直接报错No unveil paths were specified.;
  3. 逐个把permissions,path格式的字符串拆分为权限与路径两部分(必须恰好拆成两段,否则报Unveil path being specified is invalid.),并调用Core::System::unveil_after_exec;
  4. 最终通过Core::System::exec在 PATH 中查找并执行目标命令。

值得注意的细节是,该工具使用的是unveil_after_exec,对应内核中的UnveilFlags::AfterExec语义:这些 unveil 设置并不会立刻套用到 unveil 进程自身,而是被暂存下来,在真正execve目标程序时一并生效,从而让目标程序从一开始就运行在受限的 veil 之下。相关标志位定义可见 Kernel/API/Unveil.h(CurrentProgram与AfterExec)。

选项详解

unveil(1)的选项非常精简,只有一个核心选项:

选项长选项说明
-u--path解除(unveil)一个路径,格式为permissions,path

该选项可以重复使用任意多次,每使用一次就向白名单中追加一条路径授权。手册原文对格式的说明是:Unveil a path, with the format of permissions,path。

在源码 Userland/Utilities/unveil.cpp 中可以看到,--path的取值会被按逗号拆分为恰好两段:

  • parts[0]是权限字符串(如r、rw);
  • parts[1]是绝对路径(如/etc/timezone);

如果拆分结果不是恰好 2 段(例如缺少逗号、路径中带多余逗号),命令会直接报错退出,而不会静默忽略。

一个值得强调的约束来自底层系统调用:unveil(2)要求路径必须是绝对路径(以/开头),否则返回EINVAL。这一点在 Kernel/Syscalls/unveil.cpp 中有直接校验。因此给unveil(1)传相对路径是无法生效的。

权限字符语义:r、w、x、c、b

permissions字段由若干权限字符组成,可以一次性组合多个字符(如rwc)。五种字符的权威定义来自unveil(2)手册,与内核实现完全一致:

字符含义(手册原文)内核对应标志(Kernel/Syscalls/unveil.cpp)
rMay read a file at this path(可读取该路径下的文件)UnveilAccess::Read
wMay write to a file at this path(可写入该路径下的文件)UnveilAccess::Write
xMay execute a program image at this path(可执行该路径下的程序映像)UnveilAccess::Execute
cMay create or remove a file at this path(可在该路径下创建或删除文件)UnveilAccess::CreateOrRemove
bMay browse directories at this path(可浏览该路径下的目录)UnveilAccess::Browse

围绕这些权限,手册与实现还给出了几条关键行为约束:

  • 权限只会收紧,不会放宽:对同一个路径再次调用 unveil 时,新权限只能是之前权限的子集。如果试图追加此前没有授予的权限,系统调用会返回EPERM。在 Tests/Kernel/TestKernelUnveil.cpp 中,这一行为有非常直白的测试佐证:先unveil("/etc", "r")成功,随后再对其追加w、x、c均被拒绝。
  • unveil 不替代常规权限检查:即使进程对某路径 unveil 成功,如果它本身没有相应的 Unix 文件权限(owner/group/other),访问依然会被拒绝。veil 只是白名单,不是权限放大。
  • 目录 unveil 的递归性:unveil 一个目录,意味着进程可以访问该目录内的所有文件与子目录。
  • c权限的特例:为尚不存在的路径指定c权限是允许的(进程打算稍后自己创建该文件)。内核在路径解析遇到ENOENT时,只要请求包含CreateOrRemove权限,就会改用父目录的 custody 继续登记,而不是直接报错;这在 Tests/Kernel/TestKernelUnveil.cpp(unveil("/tmp/doesnotexist", "c")必须成功)与 Kernel/Syscalls/unveil.cpp 中均有体现。
  • 符号链接的处理:unveil 针对的是解析后的真实路径,符号链接目标同样受白名单约束。专门的符号链接测试位于 Tests/Kernel/unveil-symlinks.cpp。

完整使用示例

手册给出了两个可直接复制的实战示例,以下完整保留并逐条解读。

示例一:在受限路径下运行ls -la /sys/kernel

$ unveil --path=r,/etc/timezone --path=r,/usr/lib --path=r,/sys/ --path=r,/etc/passwd --path=r,/etc/group ls -la /sys/kernel

该命令仅向白名单中登记了五个只读路径:/etc/timezone、/usr/lib、/sys/、/etc/passwd、/etc/group。由于目标ls需要读取这些系统文件与目录才能正常完成对/sys/kernel的列目录操作,这些授权刚好覆盖了它的最小需求;而/sys/kernel之外的绝大多数文件系统区域对ls而言都是不可见的。

示例二:在受限路径下运行ps -ef

$ unveil --path=r,/etc/timezone --path=r,/usr/lib --path=r,/sys/ --path=r,/etc/passwd --path=r,/etc/group ps -ef

ps同样需要读取时区、动态库(/usr/lib)、系统信息(/sys/)以及用户账户映射(/etc/passwd、/etc/group)才能正确解析进程信息并打印完整格式输出,因此白名单与示例一保持一致。

两个示例的共同模式是:先穷举目标程序正常运行所需的全部路径,再以只读(r)方式逐条登记,最后才执行命令。如果白名单漏掉某个路径,目标程序访问该路径时会直接失败——这正是 unveil 作为"能力最小化"沙箱工具的核心用法。

底层原理:sys$unveil与 veil 状态机

unveil(1)的价值建立在unveil(2)系统调用的实现之上,其内核实现位于 Kernel/Syscalls/unveil.cpp。理解以下几个实现细节,有助于判断何时该用、何时不该用这个工具。

1. 白名单以"树"组织:内核为每个进程维护一棵 unveil 路径树(UnveilData::paths),路径按目录层级逐段插入。插入时通过update_intermediate_node_permissions把父节点的权限继承给中间节点,并标记explicitly_unveiled的节点,从而实现"unveil 目录即覆盖其下所有子路径"的语义。

2. 状态机:Dropped → Locked → LockedInherited:进程的 veil 状态由VeilState描述。一旦使用unveil(nullptr, nullptr)锁定(见 Kernel/Syscalls/unveil.cpp),后续任何 unveil 调用都会返回EPERM。若已锁定的状态通过execve序列被继承,则进入LockedInherited状态,此时新进程对 unveil 请求会静默忽略,防止绕过。

3. fork/exec 语义:fork()会把 unveil 状态完整复制给子进程;而execve()成功后 veil 状态会被重置。unveil(1)之所以能用unveil_after_exec精准控制目标程序,正是依赖这套"exec 时套用"的机制。

4. 参数校验严格:权限字符串长度超过 5 个字符返回E2BIG;包含未知字符(如f)返回EINVAL;路径为空或非绝对路径返回EINVAL;path/permissions二选一为 null 返回EINVAL。这些边界全部被 Tests/Kernel/TestKernelUnveil.cpp 的test_argument_validation用例逐一覆盖。

常见错误与排查

当unveil(1)包裹的命令运行异常时,可以按以下线索快速定位:

现象可能原因依据
目标程序访问某文件失败白名单遗漏该路径,或路径未以绝对路径形式登记权限校验见 Kernel/Syscalls/unveil.cpp
Unveil path being specified is invalid.--path值没有恰好拆成permissions,path两段Userland/Utilities/unveil.cpp
No unveil paths were specified.没有提供任何--pathUserland/Utilities/unveil.cpp
目标程序需要写权限但被拒绝只授予了r,或权限字符串不含w/c权限集约束见 man2 与测试test_failures

另外要记住:veil 是进程级白名单,只影响目标进程的文件系统可见性,不会降低目标进程自身的 Unix 权限(如 root 身份),也不会拦截网络、信号等其他资源。若需要更全面的进程级能力约束,应与pledge(2)或系统整体缓解机制(见Mitigations(7))配合使用。

相关参考

  • unveil(1):本文介绍的命令行工具手册(即本指南对应的原始文档)
  • unveil(2):系统调用手册,含完整错误码与 C 语言示例
  • Userland/Utilities/unveil.cpp:unveil(1)的命令行实现
  • Kernel/Syscalls/unveil.cpp:sys$unveil内核实现
  • Kernel/API/Unveil.h:UnveilFlags标志位定义
  • Tests/Kernel/TestKernelUnveil.cpp:参数校验与权限收紧的测试
  • Tests/Kernel/unveil-symlinks.cpp:符号链接场景测试

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询