agent-Specialization/wsl2-sandbox-research.md

18 KiB
Raw Blame History

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创建专用 distroastrion-sandbox)。这是官方支持的、完全非交互的路径。

具体做法

rootfs 官方来源:

导入命令序列(官方文档,Microsoft Learn — Basic commands for WSL

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,另见 WSL issue #10386 讨论 --no-launch 后自行脚本化配置的做法)。但对沙箱场景,推荐完全绕开 --install,用 --import:可控、可重复、无 OOBE、与用户的其他发行版零耦合。

检测并排除 docker-desktop 等不可用 distro

  • 已安装发行版记录在注册表 HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss(每个 GUID 子键含 DistributionNameBasePathVersion 等)(Learning in the Open: Lxss 注册表结构)。
  • docker-desktop / docker-desktop-data 会出现在 wsl -l -v 里,但它们是 Docker Desktop 的内部基础设施 distroSuper User 讨论)。
  • 程序化排除策略(建议多层):① 名字黑名单(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——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)。
  • 关键坑:metadata 默认关闭。未开 metadata 时9p/drvfs 上 chmod 不生效、权限由 umask/fmask 统一伪造;开 metadata 后 chmod 结果会持久化到 NTFS 扩展属性(Super Userakr.am)。因此不要用 chmod 作为 /mnt 下文件的只读控制手段——权限语义不可靠。只读控制应走 mount 层(见第 3 节的 ro bind mount那是 VFS 层强制,与底层 9p 无关。
  • 另一坑9p 不支持 inotify 跨 Windows 侧变更Rolldown 文档明确说明 Windows 侧修改文件 WSL2 内 watch 收不到事件),与本任务关系不大但值得知道。
  • 反向保护:如果希望沙箱内根本看不到其他 Windows 盘,可以在专用发行版的 /etc/wsl.conf 里设 [automount] enabled=false再按需只挂载工作区所在盘fstab 里也支持 ro 挂载(tonym.us: Improve WSL Security with Read-Only Filesystem)。

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:环境明确为 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 的实测环境记录unshare -Urm + bind + remount ro 的完整链条在 codex issue #9254 中有逐命令验证(链接)。
  • WSL1 不可用WSL1 内核不支持 user namespaceunshare -Ur 直接 EINVALbwrap 报 "kernel does not support user namespaces"codex issue #16076)。另外 WSL1 有 ro bind mount 在有可写 fd 时 EBUSY 的 bugWSL issue #3549)。方案必须强制 WSL2(导入时 --version 2,启动前 wsl -l -v 校验)。
  • 已知小限制:unshare --time 在 WSL 上不支持(WSL issue #9223),与本需求无关。

推荐命令链发行版内root 或 userns 均可)

方案 A纯 unshare无需装包

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 共用同一套策略描述):

bwrap \
  --ro-bind / / \
  --bind /mnt/e/workspace /mnt/e/workspace \
  --bind /tmp /tmp \
  --unshare-net \
  --die-with-parent \
  -- bash

bwrap 的 --unshare-net 会在新 netns 里创建 loopbackfromager issue #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.SELWN: Read-only bind mounts)。
  3. 只读是 VFS 层强制9p 后端无法绕过(进程在 Linux 侧的一切写都被 VFS 挡掉);但从 Windows 侧的直接写入不受此约束——沙箱只防 WSL 内的不可信进程。

4. 网络隔离:可行(三档都能实现)

结论

可行。三档映射:

  • 禁网unshare --net(新 netns 无任何外部接口)或 bwrap --unshare-net
  • 仅回环unshare --netip link set lo up(在新 ns 内有 CAP_NET_ADMIN可用或直接用 bwrap自动带 lo
  • 全开:不加 net namespace。

依据与细节

  • unshare -rn 创建无外部连通性的 netns是构建工具的常用隔离手段fromager issue #472);进程隔离教学也覆盖 ip netns/unshare 路径(oneuptime 教程)。
  • 新 netns 里 lo 存在但初始 DOWN需要 ip link set lo upbwrap 较新版本自动完成这步。个别环境 bwrap 的 loopback 配置会失败RTM_NEWADDR EPERMcodex issue #15982)——若在目标环境复现,降级为 unshare --net + 手动 ip link set lo up
  • 发行版级 iptables/nftables 也可行WSL2 是完整内核,实例内 root 有完整 netfilter 能力),但 netns 方案更彻底(连路由表都不存在,无法绕过),优先推荐。
  • localhostForwarding 的影响:默认 localhostForwarding=trueMicrosoft LearnWindows 侧可通过 localhost 访问 WSL 内监听的端口。对隔离的含义:① 沙箱内进程监听端口可能被 Windows 宿主触达(本地跨进程攻击面);② NAT 模式下 WSL 内可经宿主网关 IP 访问 Windows 上的 localhost 服务(如代理 7890 端口)。unshare --net 把这两条路一起断掉(新 ns 里根本没有到宿主的链路),所以 netns 方案天然覆盖该问题;若走 iptables 方案则必须额外阻断到宿主网关 IP 的流量。
  • 另可在 .wslconfignetworkingMode=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 程序,单次约 500800msSuper User #1663063)。
  • 极端病态案例wsl.exe 会话内命令 >5s 而 SSH 进同一 WSL <0.1sWSL issue #4712),说明 wsl.exe 通道本身可能是瓶颈。
  • 第三方 cold boot 基准:基于 WSL2 的 agent 运行时冷启动约 3500msZeroClaw 对比基准,社区数据,仅供参考量级)。
  • 对照Linux 原生 bwrap/容器级沙箱开销约 300ms 以内(Julia Evans 的 benchmark),即大部分开销在 wsl.exe 桥接层而非沙箱本身。

优化手段(都已验证可行)

  1. 防 VM 休眠.wslconfigvmIdleTimeout=-1(默认 60000ms 空闲即关 VMMicrosoft Learn);新版 WSL 还有 [general] instanceIdleTimeout(默认 15000msGreenGorych .wslconfig 参考)。
  2. 保活:后台跑一个无害长驻命令,如 wsl --exec dbus-launch true知乎实践)。
  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)。
  • 解法:设置环境变量 WSL_UTF8=1wsl.exe 即改用 UTF-8 输出微软官方支持的开关VS Code 曾因此产生解析 bug反向证实了该行为vscode issue #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 #13456MathWorks 论坛kevinskii.dev)。触发条件是 autoProxy=true(默认)+ Windows 配了监听 127.0.0.1 的代理 + NAT 模式。
  • 抑制手段:
    1. .wslconfig[wsl2] autoProxy=false官方文档确认该键控制「Enforces WSL to use Windows' HTTP proxy information」Microsoft Learn;社区模板即推荐关代理时设 falsefeamcor gist)。逻辑上检测关闭则警告不再产生——建议落地前实测一轮,因为微软没有单独文档承诺「该警告由 autoProxy 开关控制」。
    2. 或切 networkingMode=mirrored(镜像模式下 localhost 代理天然可用,警告消失;但会改变整体网络行为,并存在与代理工具冲突后回退 NAT 的已知问题,issue #13456issue #10794)。
    3. 兜底:该警告走 stderr可在 host 侧对 wsl.exe 的 stderr 做已知模式过滤(不影响子命令的退出码和 stdout
  • .wslconfig 修改后需 wsl --shutdown 重启 VM 生效8 秒规则,Microsoft Learn。WSLENV 是环境变量透传机制(仅传白名单变量,Zenn 实践),与该警告无关。

总体评估矩阵

能力 结论 关键手段
专用沙箱发行版供给 可行 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 uplocalhostForwarding 风险被 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) 可共用几乎同一份沙箱策略代码。