第 2 卷
复制代码
| 1 | constpolicy = {mode:'read-only',workspaceRoot: process.cwd() } |
|---|---|
| 2 | constconfined = ctx.sandbox.confine(['bash','-c','echo hi > /tmp/out.txt'], po |
| 3 | constresult =spawnSync(confined.argv[0], confined.argv.slice(1)) *// spawnSync |
包装后的 argv 是这个样子:
复制代码 1sandbox-exec -p <profile> -- bash -c <命令></profile>
从左到右认识一下这几个部分:
sandbox-exec:这一层的 runner,在 macOS 上正是沙箱机制 Seatbelt 的命令行工具。
-p:告诉它:策略以字符串的形式直接跟在后面。
<profile></profile>:那段策略本身。下面马上把它单独打出来看。
--:分隔符,前面归沙箱,后面才是真正要跑的命令。
bash -c <命令>:你原本要跑的那条命令,一个字没动,只是被挪到了后面。
返回值有四样东⻄,每样都有用(先认个词:runner,平台上真正去包装、启动命令的那个程序,Linux 是 bwrap、macOS 是 sandbox-exec,后面细讲):
包装后的 argv——拿它去启动进程,替换掉你原本要跑的那条命 argv 令
跑 npm run start:ch07,脚本会连着跑五组对照。这里挑三组讲:①② 分别看“拦得住吗”和“放宽到哪儿”,⑤ 看“不套会怎样”,剩下 ③④ 的结论顺着这两组就能推出来,脚本里
都跑得到,就不占篇幅了。
先看第 ① 组:read-only 模式下往家目录写文件(家目录在工作区外,属于“不该被写”的地方):
复制代码 1--- ① read-only:往工作区外写 ---
2退出码: 1 | stderr: bash: /Users/.../_dsh_should_not_exist.txt: Operatio
被拒了。凭什么拒?答案就在上面那行 argv 里,跟在 -p 后面的 <profile></profile> 就是那段策略。把它单独打出来、去掉 JSON 转义,是这样:
复制代码 1(version 1) (allow default) (deny file-write*) (allow file-write* (literal "/dev/
这三句话就是 read-only 的全部语义:默认允许,只拒绝写,且只放行 /dev/null 这一个字面路径。留这么一个“下水道”,是让命令能照常往里丢弃输出,不至于因为没地方写而报
错。
第 ② 组换成 workspace-write,profile 末尾多出一段白名单,额外放行三个可写位置(同样去掉了 JSON 转义;$WORKSPACE、$TMPDIR 是脚本把真实长路径换成的别名):
三个位置各是什么:
$WORKSPACE 是工作区根目录,你要改的代码在这儿;
$TMPDIR 是这次调用拿到的私有临时目录;
/private/tmp:注意,你写的是 /tmp,但 Seatbelt(就是前面那个 sandbox-exec)
匹配的是解析后的路径,而 /tmp 在 macOS 上只是 /private/tmp 的符号链接。
最后这条值得停一下:正因如此,每个根目录都要先解析出实际路径、再整理字面写法;否则白名单写了一堆,实际一个也没匹配上。
注:中间 ③④ 两组正如上面所说的能推出来:③ 是 workspace-write 下往工作区外写、照样被拒,白名单只放行上面这三处;④ 是 read-only 下只读命令、不被拦,只拒
写、不拒读。
最后看第 ⑤ 组,不套沙箱的对照组,和 ① 同一个命令,退出码 0,文件真的写出了:
复制代码 1--- ⑤ 同一个命令,不套沙箱 ---
2退出码: 0(对比 ① 的 1:没被拦住,文件真的写出了)
五组看下来就一句:拦住这次写入的,不是什么隔离环境,就是命令前面被加上的那半截—— sandbox-exec -p <profile> --</profile>。你原本那条命令一个字没改,只是前面
多了这一串,写入就被管住了。
沙箱策略怎么定:每次各算各的