软件沙箱指一个受控执行环境,能阻止潜在恶意代码访问未授权的系统资源,2009 年一场安全会议给出的三条定义是,能以编程方式限制进程权限,不需要机器上的管理员权限,并且是进程自己降权。对要给自己的程序加沙箱的工程师来说,落地顺序是先拆进程,再用 actor 模型组织通信,最后把文件描述符当能力用,平台层面 FreeBSD 用 Capsicum,Linux 首选 Landlock 配合 seccomp,macOS 有自己的沙箱配置机制,三者都不要依赖用户命名空间做通用沙箱。
三个常见误区为什么都要避开
第一个误区是用 setuid 辅助程序配 chroot 搭沙箱,setuid 等于把完整管理员权限临时交出去,任何程序都能装 setuid 二进制会让安全措施失效,权限只应减少不应增加。第二个误区是把 Linux 命名空间当通用沙箱接口,Docker 热潮让命名空间流行,但在嵌套用户命名空间里进程扮演超级用户,原本只有超级用户能走的内核代码路径对每个用户开放,这些内核代码有十几年历史,当初不是按这个前提写的,已经出过安全问题,有内核开发者公开说过能用 CLONE_NEWUSER 拿到任意网络命名空间上的 CAP_NET_ADMIN 从而操作网络配置 API 是巨大风险,命名空间适合交给受信任的容器化工具用,不适合当软件沙箱接口,新一代接口如 Landlock 的设计目标就是不成倍扩大内核攻击面。
线程级降权为什么走不通
主流操作系统的权限边界在进程级,凭据挂在进程上由内核检查。Linux 把凭据挂在线程上,这条路走不通,GNOME 按线程级设计被 CVE-2023-43641 证伪,glibc 还要额外在线程间同步凭据。有人提出每个不可信线程配一个可信辅助线程代发系统调用,可信代码只能信寄存器,所有内存按敌意处理,关键代码得用手写汇编,理论上可行,实际成本高到不会成为主流。工程上的结论很直接,降权粒度按进程设计,不要按线程设计。
用 actor 模型和文件描述符搭沙箱的步骤
落地第一步是拆进程,把程序按功能拆成多个进程,每个进程一份权限。FreeBSD 的 Capsicum 团队把隔间化应用开发类比成分布式应用开发,组件跑在不同进程里靠消息传递通信。第二步用 actor 模型组织通信,actor 管自己的状态,能创建新 actor,能发消息,能把地址放进消息,actor 之间不共享内存,也不会和自己并行,落到具体框架是创建、发送、接收三个函数,用 UNIX 域套接字做 actor 间消息,套接字就是 actor 地址,文件描述符可以通过 UNIX 域套接字传递,收件箱描述符从不外发。第三步把文件描述符当能力用,可传递的资源包括文件、目录、管道、套接字、设备节点、共享内存、进程句柄、同步对象和 eBPF 程序,能力同时是资源引用和访问权凭证,有了这套模型才能回答 actor A 是否可能实际拿到资源 X,以及怎么设计布局让沙箱 actor 不可能同时拿到文件和套接字,经验法则是少用 ioctl。
各平台机制怎么选
FreeBSD 用 Capsicum,Linux 首选 Landlock 配合 seccomp,macOS 用自己的沙箱配置机制,三者都不要依赖用户命名空间做通用沙箱。降权和管理员策略是互补关系,降权管进程能碰什么,管理员策略管谁允许跑,少一个都不完整。
可执行的落地顺序
- 列出程序要访问的全部资源,包括文件、网络、设备和进程间通信。
- 按资源把功能拆成进程,画出通信拓扑,标出每个进程的输入输出。
- 为每个进程声明最小权限,先按最宽写,再逐项收紧。
- 用 UNIX 域套接字建立通道,按需传递描述符,收件箱描述符从不外发。
- 先跑通再收紧,每收紧一项就跑一次完整流程。
- 用 strace 或等价工具核对系统调用面,确认和声明的权限一致。
FAQ
Q 沙箱能替代容器吗。A 不能,容器管部署隔离,沙箱管进程权限,两者解决的问题不同,可以叠加用。
Q Landlock 和 seccomp 怎么分工。A Landlock 管文件系统访问路径,seccomp 管系统调用白名单,两者配合才能覆盖大部分攻击面。
Q 旧程序改动成本太高怎么办。A 先拆出处理不可信输入的那一个进程,只给这个进程上沙箱,收益最大,改动最小。