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 using
unveil(2)。
也就是说,它本身并不实现任何权限判断逻辑,而是作为unveil(2)系统调用的命令行封装:先通过系统调用为即将执行的程序搭建好路径白名单,随后exec出真正的目标命令。
其基本调用形式为:
$ unveil [--path] [command...]从实现上看,该工具位于 Userland/Utilities/unveil.cpp,其主流程非常清晰:
- 用
Core::ArgsParser解析参数,并在遇到第一个非选项参数后停止选项解析(set_stop_on_first_non_option(true)),从而保证目标命令自身的参数不会被误当作 unveil 的选项; - 校验至少提供了一个
--path,否则直接报错No unveil paths were specified.; - 逐个把
permissions,path格式的字符串拆分为权限与路径两部分(必须恰好拆成两段,否则报Unveil path being specified is invalid.),并调用Core::System::unveil_after_exec; - 最终通过
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) |
|---|---|---|
r | May read a file at this path(可读取该路径下的文件) | UnveilAccess::Read |
w | May write to a file at this path(可写入该路径下的文件) | UnveilAccess::Write |
x | May execute a program image at this path(可执行该路径下的程序映像) | UnveilAccess::Execute |
c | May create or remove a file at this path(可在该路径下创建或删除文件) | UnveilAccess::CreateOrRemove |
b | May 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 -efps同样需要读取时区、动态库(/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. | 没有提供任何--path | Userland/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),仅供参考