第 3 卷
先看模式。它从哪来?优先级从高到低是:
- 显式批准的模式覆盖,比如用户批准了一次提权重试,那次调用就用更宽的模式;
- 会话最近一条 sandbox/mode 事件,这一轮会话被切到了什么模式;
- 部署配置的默认值,没人特别指定时的兜底。
工作区根目录也在这里定:有会话时,取发起这次调用的会话的工作目录,也就是进程当前所在的那个目录;没有会话时,比如脚本直接发起的调用,没有会话信息可用,就退回部署时配
好的默认目录。
注:Windows 上还要多切一层:工作区的写入授权按工作区共享、长期保留,而每个“会话 / 工作区”另外拿到一个随机的私有临时目录——于是共享同一工作区的多个会话能共享
写权限,却不继承彼此的临时目录权限。
定下来之后还要把它解析成实际路径,原因和上一节那个 /tmp 一模一样:路径里一旦有软链接,字面写法和实际到达的地方就不是一回事。比如当前目录是 /ws/link/..,其中
link 指向别处:按字面消掉 .. 得到 /ws,可进程真正 chdir 进去的是 link 指向的目录,再往上退一级,落到的是另一个地方。所以先把软链接解开、看它真正指向哪儿,再消掉
字面上的 ..——这样这个根目录标识的才是进程真正会到达的目录。
这跟上一节 /tmp 变成 /private/tmp 是同一套路径解析,只是用处不同:那边是让白名单对得上进程真正去写的那个位置,这边是让工作区根目录正好是进程真正会去的那个目录。
每次调用各算各的,带来一个很实用的性质:两个消费方可以在同一瞬间套着不同的策略跑。
bash 用 read-only,而另一个 agent 需要往自己的状态目录写东⻄——两者同时向同一个提供方要不同的边界,提供方不需要为谁改变状态。获批的提权重试也一样,它不是“把提供
方放宽”,而是用更宽的策略发起一次新调用。
入拦下来。注意平台与机制不是一一对应的:Linux 独占两套,一主一备,所以才叫三个平台、四套机制。
Linux:bwrap 主用,不行则退到 Landlock(两个候选按序探测,结论在 provider 生命周期内缓存)
bwrap 是“隔离式”:用 Linux 的 mount namespace 给进程单独挂一套目录视图,把该保护的位置换成只读挂载,路径一个没变,写进去的动作会被内核挡下。打个比方:把
你放进一间和真房间一模一样的样板间,家具摆设都在,抽屉却全封死了;你以为在自己家,其实什么都放不进去。
Landlock 是“检查式”:bwrap 的备胎。它不另建视图,进程看到的还是真实文件系统,只是内核在每次文件访问时按规则集逐条查一遍,不合规则就拒绝。打个比方:房
间还是那间房,但每扇门都装了刷卡机——你走原来的走廊、开原来的门,每开一扇都要刷一次卡,刷不过就“嘀”一声弹开。老版本的内核接口(ABI)管不全所有动作,也报
partial。
macOS:sandbox-exec
Seatbelt 是“清单式”:sandbox-exec -p 把一段 allow/deny 规则(profile)交给 macOS 内核的策略引擎,由内核照单判定——默认允许、只拒写、只放行
/dev/null。打个比方:给进程配一个照单办事的管家,清单上写着"可以读、不准写、废纸只能扔这个桶";进程每做一件事,管家就对着清单核一遍,没写的不让做。
Windows:ACL 受限令牌 runner
受限令牌是“令牌式”:它既不改视图也不逐次检查,而是给进程换一个降级后的身份, restricting SID 只列出能写的目录,内核查 ACL(访问控制列表,每个文件上都挂着一
张“谁能在它上面做什么”的表)时只认这个受限身份。打个比方:把工牌换成打折版,上面只印着“能进这几间房”,别处一律刷不开。只是启动时 Everyone 必须保留,等于
工牌上还盖了个通用章,有些门它照样开得开——所以它报的是 partial。
四套机制,四种拦法——换视图、每次查、照单判、换身份。手段差别这么大,落点却是同一
| 平台(process.platform的取值) | runner | enforcement | 方言 |
|---|---|---|---|
| macOS(darwin) | sandbox-exec | full | operat |
| Linux(linux) | 抛SANDBOX_UNAVAILABLE | — | — |
| Windows(win32) | node runner.js | partial | access |
注意,linux 那行别读反了:抛异常不是 Linux 沙箱坏了,而是这台演示机是 macOS,上面压根没有 Linux 的 runner(bwrap、Landlock 都不存在)。换一台真装了 bubblewrap 的
Linux,就会看到 bwrap 被选中、enforcement 为 full,这就是前面说的失败关闭:没有可用提供方就抛异常,绝不裸跑。
脚本能在一台机器上把三个平台都跑一遍,靠的是 internals.platform 这个测试钩子临时换掉 process.platform;真机上用不着这一手。表里 darwin 是 macOS 在
Node.js 中的平台标识,win32 是 Windows 的历史叫法,64 位 Windows 返回的也是它。
三个平台摆在一起,差异一目了然:runner 不同、enforcement 不同、被拒时 stderr 里的字样也不同。可对消费方来说,这些差异根本看不见,它只认 ctx.sandbox 这一份契约,
平台差异全被提供方吃掉了。这,就是 seam(能力接口)的价值:换一个提供方实现,消费方一行代码都不用改。
安全边界:把拦住写入交给操作系统
最后我们来回答最关键的问题:这个边界到底有多牢靠?答案藏在它的设计理念里——dsh 自己并不拦,而是把命令交给操作系统内核:
复制代码