1. 沙箱到底在防什么为什么大家总觉得开了就万事大吉很多人第一次接触“沙箱”这个词是在浏览器里。Chrome 每个标签页一个进程崩了一个不影响其他这叫进程隔离再往后一点做后端或者做安全的朋友会接触到代码沙箱、支付沙箱、容器沙箱。名字都叫沙箱但此沙箱和彼沙箱防护的目标完全不是一回事。我见过太多团队在评审会上拍胸脯说“我们代码已经在沙箱里跑了隔离没问题”结果一问细节沙箱跑在宿主机的一个普通用户下共享着同一个内核网络走的是宿主机的网卡文件系统挂的是宿主机的目录只是加了个chroot或者seccomp规则。这种配置在 Linux 单机上确实能挡住一部分越权操作但一旦你的业务要跨 Linux、Windows、macOS 三端跑或者你的沙箱要处理来自不可信来源的代码这套东西的边界就非常脆弱了。先把概念掰开。沙箱本质上是一种“限制程序行为范围”的机制它回答的问题是这段代码能碰什么、不能碰什么。而跨平台主机隔离回答的是一个更底层的问题这段代码运行的环境和真实主机之间到底有没有一条不可逾越的边界。前者是策略层面的约束后者是架构层面的切割。两者经常被混为一谈是因为在单机单用户的简单场景下它们的效果看起来差不多——反正代码跑完就删能出什么事问题就出在“跨平台”这三个字上。Linux 有 namespace 和 cgroupWindows 有 Job Object 和 AppContainermacOS 有 sandbox_init 和 Seatbelt 策略。这三套机制的抽象层次、能力边界、默认行为都不一样。你在 Linux 上用 seccomp 拦掉的系统调用在 Windows 上根本没有对应的概念你在 macOS 上依赖的 entitlement 签名在 Linux 上完全不存在。所以当你把一套“沙箱方案”平移到另一个平台时你以为自己开了沙箱实际上可能只是换了个用户跑程序而已。这篇文章想聊的就是这件事为什么“已经开了沙箱”这句话在跨平台场景下几乎等于没说。我会从隔离层级的拆解讲起把 Linux、Windows、macOS 三端各自的隔离机制摊开对比然后给出一个可落地的跨平台隔离检查清单最后分享几个我在实际项目里踩过的坑。不管你是做代码沙箱、支付沙箱还是做多端桌面应用的运行时隔离这套思路都能直接拿去对照。2. 把“隔离”拆成四层才知道自己缺了哪一层2.1 进程级隔离最容易被误认为“够了”的那一层进程级隔离是最基础的形态。Linux 上 fork 一个子进程Windows 上 CreateProcessmacOS 上 posix_spawn本质上都是让代码在一个独立的执行单元里跑。很多沙箱方案到这一步就停了理由是“进程之间内存不共享崩了也不影响主进程”。这个理由在功能层面成立在安全层面不成立。同一用户下的进程默认可以互相 ptrace、可以读/proc/pid/mem、可以发信号、可以通过共享内存通信。Linux 上只要ptrace_scope没收紧一个沙箱进程完全可以 attach 到主进程上读内存。Windows 上同一 session 内的进程可以通过OpenProcess拿到句柄除非你显式设置了 DACL。macOS 上虽然 SIP 保护了一部分系统进程但用户态进程之间的互操作依然宽松。所以进程级隔离只能算“故障隔离”不能算“安全隔离”。它的价值在于稳定性不在于防御。如果你的沙箱要跑的是不可信代码这一层远远不够。2.2 文件系统隔离chroot 不是银弹文件系统隔离的常见做法是 chroot、bind mount、overlayfs。Linux 上chroot加pivot_root能做出一个看起来独立的根目录但 chroot 有一个经典逃逸手法如果进程有 CAP_SYS_CHROOT 能力或者能拿到一个 chroot 外的文件描述符就能跳出去。容器时代大家改用 mount namespace效果好了很多但前提是你真的用了 namespace而不是只调了个 chroot。Windows 上没有 chroot 的等价物最接近的是文件系统虚拟化如 Detours 重定向或者 AppContainer 的文件访问限制。前者是 API hook绕过方式很多后者依赖 capability 声明配置复杂且容易漏。macOS 的 sandbox 策略可以限制文件读写路径但策略文件写错一个通配符整个隔离就形同虚设。跨平台做文件隔离最稳的思路不是“在每个平台找等价物”而是把工作目录做成一次性的、挂载进来的、生命周期和沙箱进程绑定的临时卷。沙箱启动时创建退出时销毁宿主机的真实路径永远不暴露给沙箱进程。这样无论底层是 namespace、Job Object 还是 Seatbelt隔离效果都由“卷的边界”保证而不是由平台机制保证。2.3 网络隔离最常被忽略的一层我见过不少沙箱方案文件隔离做得挺细网络直接放行。理由是“沙箱里的代码需要下载依赖”。这个理由在开发场景成立在生产场景是灾难。一个能自由发起网络连接的沙箱等于给攻击者开了一条数据外传通道也等于给内网扫描开了一扇门。Linux 上网络隔离靠 network namespace配合 veth pair 和 iptables 规则可以把沙箱的网络限制到只有一个 loopback或者只允许访问特定的代理地址。Windows 上可以用 Windows Filtering Platform 做进程级网络策略但配置起来比 Linux 麻烦得多而且很多开发者不熟悉。macOS 的 sandbox 策略可以限制网络访问但默认策略往往允许 outgoing 连接。跨平台网络隔离的实用做法是在沙箱进程启动前就把网络能力收掉。Linux 用unshare -nWindows 用 AppContainer 的 network capability 不授予macOS 用 sandbox 策略里的deny network*。如果业务确实需要网络走一个受控的代理代理层做域名白名单和流量审计。这样即使沙箱被突破攻击者也拿不到原始网络栈。2.4 内核级隔离真正的边界在这里前面三层都是“策略”内核级隔离才是“边界”。Linux 的 namespace 把进程看到的 PID、网络、挂载、IPC、UTS、用户都做了视图隔离cgroup 限制了资源用量seccomp 限制了系统调用集合。这三者组合起来才构成一个相对完整的隔离单元。缺了 namespacechroot 可以被逃逸缺了 seccomp内核漏洞的利用面就大缺了 cgroup一个 fork 炸弹就能拖垮宿主机。Windows 的对应物是 Job Object资源限制 AppContainer能力限制 虚拟化安全VBS/HVCI硬件级隔离。但 Windows 的 AppContainer 对传统 Win32 程序支持有限很多老程序跑不起来所以实际项目里经常退化成“低完整性级别进程”隔离强度大打折扣。macOS 的 Seatbelt 是策略引擎不是资源隔离机制。它能限制文件、网络、IPC但不限制 CPU 和内存。真正的资源隔离要靠 launchd 的 resource limit 或者外部的 cgroup 等价物而 macOS 在这方面一直比较弱。所以当你听到“我们开了沙箱”第一反应应该是追问哪一层如果对方答不上来那大概率只是进程级隔离跨平台场景下基本等于没做。3. 三端隔离机制横向对比别指望一套配置走天下3.1 Linux能力最强但配置最碎Linux 的隔离能力是三者里最完整的但也是最需要手工拼装的。一个典型的强隔离沙箱配置大概长这样# 创建隔离的命名空间 unshare --pid --net --mount --ipc --uts --user --map-root-user \ --fork \ # 限制资源 bash -c ulimit -v 1048576; ulimit -u 64; exec /path/to/sandboxed_program再配合 seccomp 过滤系统调用cgroup 限制 CPU 和内存AppArmor 或 SELinux 做路径级访问控制。这套东西配好了隔离强度很高。但问题在于每一层都要单独配漏一层就有一个缺口。我见过只配了 namespace 没配 seccomp 的也见过配了 seccomp 但规则写得太宽比如允许ptrace的。还有一个容易被忽略的点Linux 的 user namespace 在部分发行版上默认关闭或者需要sysctl kernel.unprivileged_userns_clone1。如果你的沙箱依赖 user namespace 做 root 映射部署到新机器上可能直接起不来。这个坑我在国产 Linux 发行版上踩过不止一次不同发行版的内核配置差异很大不能假设“Linux 都一样”。3.2 Windows抽象层次高但可控性差Windows 的隔离机制走的是另一条路。它不给你 namespace 这种底层原语而是给你 AppContainer、Job Object、Integrity Level 这些更高层的抽象。好处是配置相对简单坏处是你能控制的东西少。AppContainer 是 Windows 上最接近“沙箱”的机制。它给进程一个独立的 SID默认没有文件系统和注册表访问权网络能力也要显式声明。但 AppContainer 对 Win32 程序的支持一直是个痛点很多依赖 COM 或者特定 DLL 的程序跑不起来。实际项目里大家经常退而求其次用“低完整性级别进程”这个隔离强度就弱很多了低完整性进程仍然能读大部分用户文件。Job Object 主要用来做资源限制比如限制进程数、内存、CPU 时间。它不提供文件或网络隔离。所以 Windows 上的完整隔离方案通常是 AppContainer Job Object 网络策略的组合配置复杂度不比 Linux 低只是碎的地方不一样。还有一个现实问题Windows 的很多安全机制依赖代码签名和 WDAC 策略在开发机上配好了到客户环境可能因为组策略限制跑不起来。这个在跨平台产品交付时特别容易翻车。3.3 macOS策略严格但资源隔离薄弱macOS 的 Seatbelt 策略引擎其实设计得很优雅。你用一段类似 Scheme 的 DSL 描述“允许什么、拒绝什么”系统在内核层执行。比如(version 1) (deny default) (allow file-read* (subpath /usr/lib)) (allow process-exec (subpath /path/to/sandbox)) (deny network*)这段策略的意思是默认拒绝一切只允许读/usr/lib只允许执行指定路径的程序禁止一切网络。看起来很美但实际用起来有几个坑。第一策略文件写错不会报错只会静默拒绝或者静默允许。调试起来很痛苦得靠log stream看内核日志。第二Seatbelt 不限制资源一个沙箱进程可以吃满 CPU 和内存。第三macOS 的 SIP 和 TCC 会在策略之上再加一层限制有时候你的策略允许了TCC 还是拦着报错信息还不明确。跨平台项目里macOS 这一端经常是最后才适配的因为开发者主力机是 Linux 或 WindowsmacOS 的测试环境不好搭。结果就是 macOS 端的隔离配置最粗糙往往只做了个sandbox_init调用策略文件用的是最宽松的模板。3.4 三端对比速查表隔离维度LinuxWindowsmacOS进程隔离PID namespaceJob Object进程组 Seatbelt文件隔离mount namespace chrootAppContainer 文件虚拟化Seatbelt 文件策略网络隔离network namespace iptablesWFP AppContainer 能力Seatbelt 网络策略资源限制cgroup v2Job Objectlaunchd 限制弱系统调用过滤seccomp-bpf无直接等价物Seatbelt 操作级策略用户映射user namespaceAppContainer SID无直接等价物默认强度取决于配置中等策略依赖这张表想说明一件事三端的隔离原语没有一一对应关系。你在 Linux 上用 seccomp 拦掉openat在 Windows 上找不到等价物你在 macOS 上用 Seatbelt 限制文件路径在 Linux 上得用 AppArmor 或者手动 mount。所以“跨平台主机隔离”不是“在每个平台开个沙箱”那么简单而是要针对每个平台的能力边界设计不同的隔离策略同时保证对外行为一致。4. 一套可落地的跨平台隔离检查清单4.1 启动前的静态检查在沙箱进程启动之前有几件事必须确认。这些检查不依赖运行时纯配置层面就能做适合放进 CI 流程。第一确认隔离机制真的生效了。Linux 上检查/proc/pid/ns/下的 namespace inode 是否和宿主机不同Windows 上检查进程 token 里有没有 AppContainer SIDmacOS 上检查sandbox_check的返回值。这一步很多团队跳过结果沙箱配置写错了也没人发现。第二确认工作目录是临时卷。沙箱的工作目录不应该指向宿主机的任何持久化路径。Linux 上用 tmpfsWindows 上用内存映射文件或者临时目录加 ACLmacOS 上用NSTemporaryDirectory加沙箱策略限制。目录的生命周期必须和沙箱进程绑定。第三确认网络策略是默认拒绝。不管业务需不需要网络默认策略都应该是 deny需要时再开白名单。这个原则在安全领域叫“默认安全”但实际项目里经常反过来做——默认允许出事了再加规则。第四确认资源上限已设置。CPU、内存、进程数、文件描述符数这四项必须有上限。没有上限的沙箱一个死循环就能拖垮宿主机。Linux 用 cgroupWindows 用 Job ObjectmacOS 用setrlimit加 launchd 配置。4.2 运行时的动态监控静态检查只能保证配置正确运行时还得盯着。我一般会在沙箱进程外面套一个监控层定期采样几个指标系统调用频率突然飙升可能是 fork 炸弹或者扫描行为网络连接数非白名单地址的连接尝试要告警文件写入量异常大的写入可能是数据外传内存增长曲线持续增长不回落可能是内存泄漏或者挖矿这些指标不需要很精确趋势比绝对值重要。Linux 上可以用 eBPF 做无侵入采样Windows 上用 ETWmacOS 上用 EndpointSecurity 框架。如果不想搞这么重至少用ps或者tasklist定期抓一下资源占用超过阈值就 kill。4.3 退出后的清理验证沙箱退出后的清理经常被忽略。临时目录删了吗网络规则回收了吗cgroup 销毁了吗AppContainer 的 profile 注销了吗这些如果没做干净下一次沙箱启动可能复用残留状态导致隔离失效。我的做法是写一个清理检查脚本沙箱退出后跑一遍确认所有临时资源都已释放。这个脚本在 CI 里跑防止配置改动引入资源泄漏。Linux 上检查/sys/fs/cgroup/下对应的目录是否还在Windows 上检查 Job Object 句柄是否关闭macOS 上检查临时目录是否为空。5. 实操中踩过的坑与排查思路5.1 Linux 上 namespace 创建成功但隔离没生效有一次在某个国产 Linux 发行版上unshare --pid --fork返回成功但沙箱进程里ps aux还是能看到宿主机的所有进程。排查了半天发现是/proc没有重新挂载。PID namespace 只隔离了进程 ID 空间/proc还是宿主机的所以看到的是宿主机的进程列表。解决办法是在 unshare 之后、执行目标程序之前重新挂载/procunshare --pid --mount --fork bash -c mount -t proc proc /proc exec /path/to/program 这个坑的教训是namespace 隔离的是内核视图不是文件系统内容。/proc、/sys、/dev这些虚拟文件系统如果不重新挂载会泄漏宿主机信息。5.2 Windows 上 AppContainer 程序启动失败AppContainer 对 DLL 加载路径有严格限制。如果沙箱程序依赖的某个 DLL 不在 AppContainer 允许的路径下程序会直接启动失败错误码还很不直观。排查方法是看 Event Viewer 里的 AppContainer 相关日志或者用Process Monitor过滤ACCESS DENIED。解决办法有两个要么把依赖 DLL 复制到 AppContainer 允许的目录要么在 AppContainer 的 capability 里声明文件访问权限。前者简单但增加部署复杂度后者灵活但配置容易漏。我一般先用前者快速验证跑通后再优化成后者。5.3 macOS 上 Seatbelt 策略静默失效macOS 的 sandbox 策略如果语法有误系统不会报错而是按默认策略执行。默认策略往往是允许的所以你以为隔离开了实际上没有。验证方法是故意在沙箱里做一个应该被拒绝的操作比如读/etc/passwd看是否真的被拦。还有一个坑是策略的继承。如果沙箱进程 fork 了子进程子进程默认继承父进程的 sandbox 策略。但如果子进程 exec 了一个带 setuid 位的程序策略可能会被重置。这个行为在文档里写得很隐晦实际遇到时排查了很久。5.4 跨平台常见问题速查表现象可能原因排查方向Linux 沙箱能看到宿主机进程/proc 未重新挂载检查 mount namespace 和 /proc 挂载Windows 沙箱程序启动即退出AppContainer DLL 路径限制查 Event Viewer 和 ProcMonmacOS 沙箱策略不生效策略语法错误静默失败用 sandbox_check 验证沙箱内网络意外可达网络策略默认允许检查 iptables/WFP/Seatbelt 规则沙箱退出后资源残留清理逻辑不完整检查 cgroup/Job Object/临时目录跨平台行为不一致各端隔离原语差异对照三端对比表逐项核对6. 从“开了沙箱”到“隔离可信”还差什么回到标题那个问题。为什么说“已经开了沙箱”不等于跨平台主机隔离做好了因为沙箱是一个动作隔离是一个结果。你调了sandbox_init、创建了 namespace、设置了 AppContainer这些都是动作。动作是否达成了隔离结果取决于配置是否正确、边界是否完整、跨平台是否一致。我个人的经验是判断一个跨平台隔离方案是否可信看三个指标就够了。第一默认拒绝沙箱启动时除了明确允许的能力其他全部拒绝。第二边界可验证每个隔离维度都有对应的验证方法不依赖“应该没问题”这种假设。第三跨端一致三端的隔离强度不能差太多不能 Linux 端做了 seccomp 而 Windows 端只跑了个低完整性进程。最后分享一个实用技巧。如果你不确定自己的沙箱隔离到底做到了什么程度写一个“逃逸测试用例集”包含文件读取、网络连接、进程枚举、系统调用、资源耗尽这几类操作在三端各跑一遍记录哪些被拦、哪些放行。这个测试集不需要很复杂但能给你一个客观的隔离强度基线。每次隔离配置改动后重跑一遍防止回归。这个测试集我在多个项目里用过最有用的一次是发现 macOS 端的网络策略写错了通配符导致沙箱可以访问任意地址。如果只靠人工检查这个错误可能到上线都发现不了。