agent-Specialization/wsl2-sandbox-research.md

170 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WSL2 等价强度沙箱可行性调研报告
调研日期2026本轮。结论先行**WSL2 可以做出与 macOS sandbox-exec 等价强度的沙箱**,核心路径是「专用 WSL2 发行版 + 发行版内 user/mount/network namespaceunshare 或 bubblewrap」。因为 WSL2 跑的是真实 Linux 内核(如 6.6.x-microsoft-standard-WSL2Linux 侧的方案unshare / bwrap基本可以完全复用最大的工程成本在 Windows 侧的发行版供给、路径转换和 wsl.exe 的编码/开销问题。
---
## 1. 发行版供给:可行(编程式部署专用沙箱发行版)
### 结论
**可行**。不要依赖用户已有的发行版,而是用 `wsl --import` 导入一个官方 rootfs tar创建专用 distro`astrion-sandbox`)。这是官方支持的、完全非交互的路径。
### 具体做法
**rootfs 官方来源:**
- Ubuntu WSL 专用 rootfs`https://cloud-images.ubuntu.com/wsl/<release>/current/*-wsl.rootfs.tar.gz`。Ubuntu 官方明确提供 tar-based WSL 发行格式用于「distribute, install, and manage Ubuntu WSL instances」[Ubuntu 官方博客](https://ubuntu.com/blog/ubuntu-wsl-new-format-available))。社区实践即用 `wsl --import` 导入这些 rootfs[buildroot GettingStarted](https://github.com/TiMaMi-GmbH/buildroot-external-timami/blob/main/GettingStarted.md)、[Zenn 教程](https://zenn.dev/dozo/articles/a633794f6d7575))。
- Alpine minirootfs更轻量~3MB`https://dl-cdn.alpinelinux.org/alpine/v3.xx/releases/x86_64/alpine-minirootfs-*-x86_64.tar.gz`,官方下载页提供 MINI ROOTFS社区有完整 WSL2 导入实践([tonym.us: Porting Alpine Linux RootFS to WSL2](https://tonym.us/porting-alpine-linux-rootfs-wsl2.html)、[PowerShell-Wsl-Alpine](https://github.com/antoinemartin/PowerShell-Wsl-Alpine)、[nathanchance: 从 LXC 镜像创建 WSL2 发行版](https://nathanchance.dev/posts/wsl2-distros-from-lxc-images/))。
- 许可Ubuntu/Alpine 均为自由软件rootfs tarball 由官方公开发布供再分发与导入,无许可障碍。
**导入命令序列(官方文档,[Microsoft Learn — Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands)**
```powershell
wsl --import astrion-sandbox C:\Path\To\InstallDir ubuntu-wsl.rootfs.tar.gz --version 2
```
`--import` 完全非交互,导入的 distro 默认 root 登录无首启用户创建向导Store 版 distro 才有 OOBE。注意import 的 distro 没有 launcher exe改默认用户要靠发行版内 `/etc/wsl.conf``[user] default=`
**`wsl --install` 的交互问题:** Store 渠道安装的 distro 首启会进入交互式用户创建,且 `--install` 本身支持 `--no-launch` / `--no-distribution` 选项规避部分交互([Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/basic-commands),另见 [WSL issue #10386](https://github.com/microsoft/WSL/issues/10386) 讨论 `--no-launch` 后自行脚本化配置的做法)。但对沙箱场景,**推荐完全绕开 `--install`,用 `--import`**:可控、可重复、无 OOBE、与用户的其他发行版零耦合。
**检测并排除 docker-desktop 等不可用 distro**
- 已安装发行版记录在注册表 `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss`(每个 GUID 子键含 `DistributionName`、`BasePath`、`Version` 等)([Learning in the Open: Lxss 注册表结构](https://learningintheopen.org/tag/hkcusoftwaremicrosoftwindowscurrentversionlxss/))。
- `docker-desktop` / `docker-desktop-data` 会出现在 `wsl -l -v` 里,但它们是 Docker Desktop 的内部基础设施 distro[Super User 讨论](https://superuser.com/questions/1729811/why-are-these-multiple-wsl-distributions-on-windows))。
- 程序化排除策略(建议多层):① 名字黑名单(`docker-desktop*`);② 试运行探测 `wsl -d <name> -e true`,失败即排除;③ 最稳妥:根本不枚举用户 distro只认自己创建的 `astrion-sandbox`(先 `wsl -l -q` 检查是否已存在,存在则直接用或校验后重建)。注意 `wsl -l` 的输出是 UTF-16见第 7 节)。
---
## 2. Windows 路径访问与 drvfs 语义:可行,但要注意 9P 元数据语义
### 结论
**可行**。`E:\path` → `/mnt/e/path` 是机械转换(盘符小写、反斜杠转正斜杠)。权限语义取决于 drvfs 挂载选项WSL2 下 Windows 盘实际以 9p 协议挂载。
### 要点
- WSL2 中 Windows 盘的 mount 输出形如:`C:\ on /mnt/c type 9p (rw,...,aname=drvfs;path=C:;...)`[WSL issue #4774](https://github.com/microsoft/WSL/issues/4774)——WSL1 是 drvfs 内核驱动WSL2 是 9P over virtio语义基本对齐。
- `/etc/wsl.conf``[automount]` 可配 `options = "metadata,uid=...,gid=...,umask=...,fmask=...,dmask=...,case=off|dir|force"`[Microsoft Learn — Advanced settings configuration in WSL](https://learn.microsoft.com/en-us/windows/wsl/wsl-config))。
- **关键坑:`metadata` 默认关闭**。未开 metadata 时9p/drvfs 上 `chmod` 不生效、权限由 umask/fmask 统一伪造;开 metadata 后 chmod 结果会持久化到 NTFS 扩展属性([Super User](https://superuser.com/questions/1323645/unable-to-change-file-permissions-on-ubuntu-bash-for-windows-10)、[akr.am](https://akr.am/blog/posts/fix-wsl-file-system-behavior))。因此**不要用 chmod 作为 /mnt 下文件的只读控制手段**——权限语义不可靠。只读控制应走 mount 层(见第 3 节的 ro bind mount那是 VFS 层强制,与底层 9p 无关。
- 另一坑9p 不支持 inotify 跨 Windows 侧变更Rolldown 文档明确说明 [Windows 侧修改文件 WSL2 内 watch 收不到事件](https://rolldown.rs/reference/inputoptions.watch)),与本任务关系不大但值得知道。
- 反向保护:如果希望沙箱内根本看不到其他 Windows 盘,可以在专用发行版的 `/etc/wsl.conf` 里设 `[automount] enabled=false`再按需只挂载工作区所在盘fstab 里也支持 ro 挂载([tonym.us: Improve WSL Security with Read-Only Filesystem](https://tonym.us/improve-wsl-security-with-read-only-filesystem.html))。
---
## 3. 文件只读与写范围mount namespace / bubblewrap可行
### 结论
**可行,且是整套方案的核心**。WSL2 是真实 Linux 内核user/mount/network namespace 均可用;`unshare --mount` + ro bind 链条成立;**bubblewrap 可以直接装进 WSL2 使用**Linux 方案可完全复用。
### 证据
- OpenAI Codex CLI 的 Linux 沙箱就是 bubblewrap其在 WSL2 上实际运行([codex issue #22469](https://github.com/openai/codex/issues/22469):环境明确为 `WSL2 (kernel 6.6.114.1-microsoft-standard-WSL2)`,问题只是 bwrap 的 `--dev /dev` 没暴露 `/dev/dxg` GPU 节点——说明 bwrap 沙箱本体在 WSL2 工作正常)。
- WSL2 内核支持 user namespace 与 network namespace内核 6.6.87.2-microsoft-standard-WSL2 上 `unshare(CLONE_NEWUSER|CLONE_NEWNET)` 可用([CVE-2026-43284 PoC 的实测环境记录](https://sploitus.com/exploit?id=9B3D53CA-E954-5110-8888-1957B931A61B)`unshare -Urm` + bind + remount ro 的完整链条在 codex issue #9254 中有逐命令验证([链接](https://github.com/openai/codex/issues/9254))。
- **WSL1 不可用**WSL1 内核不支持 user namespace`unshare -Ur` 直接 EINVALbwrap 报 "kernel does not support user namespaces"[codex issue #16076](https://github.com/openai/codex/issues/16076))。另外 WSL1 有 ro bind mount 在有可写 fd 时 EBUSY 的 bug[WSL issue #3549](https://github.com/microsoft/WSL/issues/3549))。**方案必须强制 WSL2**(导入时 `--version 2`,启动前 `wsl -l -v` 校验)。
- 已知小限制:`unshare --time` 在 WSL 上不支持([WSL issue #9223](https://github.com/microsoft/WSL/issues/9223)),与本需求无关。
### 推荐命令链发行版内root 或 userns 均可)
方案 A纯 unshare无需装包
```bash
wsl -d astrion-sandbox -u root -- bash -c '
unshare --mount --propagation private bash -c "
mount --make-rprivate / # 关键:先切断 shared 传播,避免污染宿主/其他会话
mount --bind / / # 如需整树 ro先 bind 再 remount,robind+ro 一步到位有内核老 bug必须两步
mount -o remount,bind,ro /
mount --bind /mnt/e/workspace /mnt/e/workspace
mount -o remount,bind,rw /mnt/e/workspace # 工作区 rw 放回去
mount --bind /tmp /tmp && mount -o remount,bind,rw /tmp
exec sudo -u sandboxuser bash # 降权后 exec 目标 shell
"
'
```
方案 Bbubblewrap推荐与 macOS/Linux 共用同一套策略描述):
```bash
bwrap \
--ro-bind / / \
--bind /mnt/e/workspace /mnt/e/workspace \
--bind /tmp /tmp \
--unshare-net \
--die-with-parent \
-- bash
```
bwrap 的 `--unshare-net` 会在新 netns 里创建 loopback[fromager issue #472](https://github.com/python-wheel-build/fromager/issues/472)),天然实现「禁网但保留 lo」。
### 两个必须注意的工程细节
1. **mount 传播**WSL2 的挂载默认是 shared propagation。在新 mount namespace 里 remount ro 之前如果不 `mount --make-rprivate /`ro 事件可能传播回初始 namespace影响同 VM 里的其他会话/发行版。先私有化再操作是标准做法。
2. **bind ro 要两步**`mount --bind` 本身不继承 ro 选项,必须 `mount -o remount,bind,ro`(经典内核行为,[Unix.SE](https://unix.stackexchange.com/questions/128336/why-doesnt-mount-respect-the-read-only-option-for-bind-mounts)、[LWN: Read-only bind mounts](https://lwn.net/Articles/281157/))。
3. 只读是 VFS 层强制9p 后端无法绕过(进程在 Linux 侧的一切写都被 VFS 挡掉);但**从 Windows 侧的直接写入不受此约束**——沙箱只防 WSL 内的不可信进程。
---
## 4. 网络隔离:可行(三档都能实现)
### 结论
**可行**。三档映射:
- **禁网**`unshare --net`(新 netns 无任何外部接口)或 `bwrap --unshare-net`
- **仅回环**`unshare --net` 后 `ip link set lo up`(在新 ns 内有 CAP_NET_ADMIN可用或直接用 bwrap自动带 lo
- **全开**:不加 net namespace。
### 依据与细节
- `unshare -rn` 创建无外部连通性的 netns是构建工具的常用隔离手段[fromager issue #472](https://github.com/python-wheel-build/fromager/issues/472));进程隔离教学也覆盖 `ip netns`/`unshare` 路径([oneuptime 教程](https://oneuptime.com/blog/post/2026-03-20-isolate-process-network-namespace/view))。
- 新 netns 里 lo 存在但初始 DOWN需要 `ip link set lo up`bwrap 较新版本自动完成这步。个别环境 bwrap 的 loopback 配置会失败RTM_NEWADDR EPERM[codex issue #15982](https://github.com/openai/codex/issues/15982))——若在目标环境复现,降级为 `unshare --net` + 手动 `ip link set lo up`
- 发行版级 iptables/nftables 也可行WSL2 是完整内核,实例内 root 有完整 netfilter 能力),但 netns 方案更彻底(连路由表都不存在,无法绕过),优先推荐。
- **localhostForwarding 的影响**:默认 `localhostForwarding=true`[Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/wsl-config)Windows 侧可通过 localhost 访问 WSL 内监听的端口。对隔离的含义:① 沙箱内进程监听端口可能被 Windows 宿主触达(本地跨进程攻击面);② NAT 模式下 WSL 内可经宿主网关 IP 访问 Windows 上的 localhost 服务(如代理 7890 端口)。**`unshare --net` 把这两条路一起断掉**(新 ns 里根本没有到宿主的链路),所以 netns 方案天然覆盖该问题;若走 iptables 方案则必须额外阻断到宿主网关 IP 的流量。
- 另可在 `.wslconfig``networkingMode=none` 做 VM 级断网,但它是全局开关、影响所有发行版,不适合按需沙箱。
---
## 5. 交互式 shell持久会话可行
### 结论
**可行**。namespace 隔离是进程树属性,把沙箱包装器作为长驻 shell 的父进程即可,对一次性命令和持久终端同样成立。
### 做法
- 一次性命令:`wsl -d astrion-sandbox -u root -- bwrap <flags> -- bash -lc "cd ... && cmd"`。
- 持久交互终端:把同样的包装器包在交互 shell 外面——`wsl -d astrion-sandbox -- bwrap <flags> -- bash`(或先 `unshare` 进 namespace 再 exec bash。bwrap 是 exec 语义,整个会话生命周期都活在沙箱里;加 `--die-with-parent` 保证 wsl.exe 断开时沙箱内进程树随之回收。
- 会话内多条命令复用同一沙箱:沙箱内起 tmux 或直接复用该 bash 的 stdin/stdout 管道host 侧通过持有 wsl.exe 进程的 stdio 持续下发命令。
- 若要「先配置一次隔离、后续多次进入」,也可以 `unshare --mount --net --fork` 后用 `nsenter -t <pid> -m -n` 反复进入同一组 namespacensenter 是标准 Linux 能力WSL2 可用)。
---
## 6. 性能:有限(每次起 wsl.exe 有数百 ms 开销,必须做进程复用)
### 结论
**可行但需要工程优化**。实测/社区数据一致指向wsl.exe 每次调用有数百毫秒级固定开销,高频命令场景必须保活 + 复用长驻进程。
### 数据点
- 从 Windows 侧经 wsl.exe 调用 Linux 程序,单次约 500800ms[Super User #1663063](https://superuser.com/questions/1663063/slow-bash-under-wsl))。
- 极端病态案例wsl.exe 会话内命令 >5s 而 SSH 进同一 WSL <0.1s[WSL issue #4712](https://github.com/microsoft/WSL/issues/4712)说明 wsl.exe 通道本身可能是瓶颈
- 第三方 cold boot 基准基于 WSL2 agent 运行时冷启动约 3500ms[ZeroClaw 对比基准](https://skywork.ai/skypage/en/openclaw-windows-compatibility-guide/2048651699598520321)社区数据仅供参考量级)。
- 对照Linux 原生 bwrap/容器级沙箱开销约 300ms 以内[Julia Evans benchmark](https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/)即大部分开销在 wsl.exe 桥接层而非沙箱本身
### 优化手段(都已验证可行)
1. **防 VM 休眠**`.wslconfig` `vmIdleTimeout=-1`默认 60000ms 空闲即关 VM[Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/wsl-config)新版 WSL 还有 `[general] instanceIdleTimeout`默认 15000ms[GreenGorych .wslconfig 参考](https://greengorych.io/blog/complete-wslconfig-reference-and-template/?q=))。
2. **保活**后台跑一个无害长驻命令 `wsl --exec dbus-launch true`[知乎实践](https://www.zhihu.com/question/662699218/answer/3578847718))。
3. **复用**host 侧持有一条长驻的 `wsl -d astrion-sandbox -- bwrap ... bash` 进程通过其 stdin/stdout 反复下发命令推荐或沙箱内起一个小的命令服务FIFO/socket后续命令经 wsl.exe -e 投递
---
## 7. 编码与输出污染:有限(有开关,但需逐项处理)
### UTF-16 输出
- **结论可控。** wsl.exe 自身输出 `--list` UTF-16LE BOM不尊重系统代码页[WSL issue #4607](https://github.com/microsoft/WSL/issues/4607))。
- **解法设置环境变量 `WSL_UTF8=1`**wsl.exe 即改用 UTF-8 输出微软官方支持的开关VS Code 曾因此产生解析 bug反向证实了该行为[vscode issue #276253](https://github.com/microsoft/vscode/issues/276253))。host 侧启动 wsl.exe 子进程时统一注入 `WSL_UTF8=1` 即可
### localhost 代理警告
- **结论可抑制但最佳路径建议实测确认。** 警告 `wsl: A localhost proxy configuration was detected but not mirrored into WSL. WSL in NAT mode does not support localhost proxies.` wsl.exe 在每次启动时打印[WSL issue #13456](https://github.com/microsoft/WSL/issues/13456)、[MathWorks 论坛](https://fr.mathworks.com/matlabcentral/answers/2174804-wsl-proxy-issue-affecting-compiler-package-microservicedockerimage-in-matlab)、[kevinskii.dev](https://kevinskii.dev/posts/wsl-networking/))。触发条件是 `autoProxy=true`默认+ Windows 配了监听 127.0.0.1 的代理 + NAT 模式
- 抑制手段
1. `.wslconfig` `[wsl2] autoProxy=false`官方文档确认该键控制Enforces WSL to use Windows' HTTP proxy information」,[Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/wsl-config)社区模板即推荐关代理时设 false[feamcor gist](https://gist.github.com/feamcor/46687e86513c106c438bfa2715249609))。逻辑上检测关闭则警告不再产生——**建议落地前实测一轮**因为微软没有单独文档承诺该警告由 autoProxy 开关控制」。
2. 或切 `networkingMode=mirrored`镜像模式下 localhost 代理天然可用警告消失但会改变整体网络行为并存在与代理工具冲突后回退 NAT 的已知问题[issue #13456](https://github.com/microsoft/WSL/issues/13456)、[issue #10794](https://github.com/microsoft/WSL/issues/10794))。
3. 兜底该警告走 stderr可在 host 侧对 wsl.exe stderr 做已知模式过滤不影响子命令的退出码和 stdout)。
- `.wslconfig` 修改后需 `wsl --shutdown` 重启 VM 生效8 秒规则[Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/wsl-config))。WSLENV 是环境变量透传机制仅传白名单变量[Zenn 实践](https://zenn.dev/kwrkb/articles/d927a0eac72d3d?locale=en)),与该警告无关。
---
## 总体评估矩阵
| 能力 | 结论 | 关键手段 |
|---|---|---|
| 专用沙箱发行版供给 | 可行 | `wsl --import` + Ubuntu cloud WSL rootfs / Alpine minirootfs注册表 Lxss + 名字黑名单 + 试运行探测排除 docker-desktop推荐只认自建 distro |
| Windows 路径访问 | 可行 | 盘符机械转换权限控制走 mount 层而非 chmod9p metadata 默认关 |
| 只读/写范围mount ns | 可行 | `unshare --mount` + `make-rprivate` + 两步 ro bind + 工作区 rw 放回 bubblewrap必须 WSL2WSL1 userns |
| 网络三档 | 可行 | `bwrap --unshare-net` / `unshare --net` + `ip link set lo up`localhostForwarding 风险被 netns 天然覆盖 |
| 持久交互 shell | 可行 | 沙箱包装器 exec 长驻 bash nsenter 重入 |
| 性能 | 有限 | wsl.exe 单次数百 ms必须 VM 保活 + 长驻进程复用 |
| 编码/输出污染 | 有限 | `WSL_UTF8=1` 解决编码`autoProxy=false` 抑制代理警告建议实测stderr 模式过滤兜底 |
**落地建议优先级**专用 distro--import Alpine/Ubuntu rootfs)→ 发行版内 bubblewrap 统一沙箱ro-bind / + 工作区 bind + --unshare-net 三档)→ 长驻 bwrap bash 会话复用 `WSL_UTF8=1` + `autoProxy=false`这样 Linux Windows(WSL2) 可共用几乎同一份沙箱策略代码