为 WSL2 上的 DSH Landlock 沙箱追加 /dev/dxg 与 /proc 的读写授权,使受限的 agent bash 会话中也能使用 CUDA。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:Jumqyc/dsh-wsl-gpufix
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
社区非官方插件,与 DeepSeek 官方无关,未获官方背书。
English | 简体中文
一个 DSH(DeepSeek Harness)宿主侧 Cordis 插件:让 WSL2 的 GPU 设备在 DSH 本地沙箱里
可用,使普通(受限)agent bash 会话里的 torch.cuda.is_available() 变为 True——
无论 dsh-sandbox-local 最终选中 Landlock runner 还是 bubblewrap。
$ python -c "import os, torch; os.close(os.open('/dev/dxg', os.O_RDWR)); print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
True NVIDIA GeForce RTX 4060 Laptop GPU
一条命令安装:
dsh plugin --profile <profile> add github:Jumqyc/dsh-wsl-gpufix
问题
@deepseek-ai/dsh-sandbox-local 把两个 runner 的 profile 都写死了,而两者都没能用上
/dev/dxg。
Landlock 这一档的授权列表是写死的(永远有 /dev/null,workspace-write 下再加
/tmp 和 workspace 根目录),没有配置项可以追加路径。在 WSL2 上这会以两种方式挡住 GPU:
| 现象 | 原因 |
|---|---|
nvidia-smi 报 "GPU access blocked by the operating system",cuInit(0) 返回 100(CUDA_ERROR_NO_DEVICE) |
/dev/dxg 无法以 O_RDWR 打开 |
nvidia-smi 正常但 cuInit(0) 返回 304(CUDA_ERROR_OPERATING_SYSTEM) |
libcuda 在 cuInit 时向 /proc/self/task/<tid>/comm 写线程名,写入被拒后直接判定为致命错误 |
所以这一档需要两条读写授权:/dev/dxg 和 /proc。完整排查过程见
docs/why.md。
bwrap 这一档加"可写路径授权"没有意义,因为它的 profile 会挂 --dev /dev:一个全新的
最小 devtmpfs,里面根本不存在 /dev/dxg。ls /dev/dxg 直接报 "No such file or
directory"——权限都还没轮到检查。bwrap 的平台链是 ["bwrap", "landlock"] 并按序探测,所以
装上 bubblewrap 就会把原本可用的 Landlock 配置静默切到这一档,同时只针对 Landlock 的
修复也一起失效。设备必须被挂进去:
--dev-bind /dev/dxg /dev/dxg
这一档不需要处理 /proc:profile 自带的 --proc /proc 挂的是新 procfs,受限进程本来就能
写自己的 task comm。
位置本身就是修复的一部分
--dev-bind 必须落在 --dev <dest> 之后,因为 --dev 会把新 devtmpfs 挂在 /dev
之上。放在它之前会被这次挂载盖掉,设备再次消失——而且 bwrap 不报错、退出码也是 0,也就是
把要修的故障原样静默恢复:
$ bwrap --ro-bind / / --dev-bind /dev/dxg /dev/dxg --dev /dev … -- ls /dev/dxg
ls: cannot access '/dev/dxg': No such file or directory
$ bwrap --ro-bind / / --dev /dev --dev-bind /dev/dxg /dev/dxg … -- ls /dev/dxg
crw-rw-rw- 1 nobody nobody 10, 258 /dev/dxg
它做什么
插件挂载期间包装公开的 ctx.sandbox.confine() 接口:先调用原方法,再从已解析的
ConfinedArgv 里读出实际选中的 runner,按其自身方言改写,且只对配置的 sandbox 模式生效:
| 选中的 runner | 改写方式 |
|---|---|
| Landlock launcher | 为每个 readWrite 路径在 -- 分隔符前插入 --rw <path> |
| bwrap | 为每个 deviceBinds 路径紧跟在 --dev <dest> 之后插入 --dev-bind <path> <path> |
其它 runner 一律不动,只打一次 warning。它不改任何 DSH 源码,不关闭沙箱,不影响审批策略, 行卸载后自动还原。
runner 之所以要从解析结果里读:confine(argv, policy) 收到的是调用方自己的命令
(['bash', '-c', …]),runner 是它内部选的,调用方 argv 里从来不含它。从那个 argv 反推
后端是不可能成立的。
环境要求
- WSL2 GPU 直通本身正常(普通未受限终端里
nvidia-smi能用)。 - DSH 使用
dsh-sandbox-local,两个 runner 任一皆可:- Landlock(未安装 bubblewrap):内核启用 Landlock
(
landlock-run --probe输出landlock: fully enforced)。 - bubblewrap(
PATH里有bwrap):只要它存在就会赢下 runner 链,此时readWrite对它无效,见deviceBinds。
- Landlock(未安装 bubblewrap):内核启用 Landlock
(
- 已在
dsh 0.1.2-rc.1(同步confine)与dsh 0.2.0-rc.2(async confine、Promise<ConfinedArgv>、第三个signal参数)上验证,内核 6.18 / WSL 2,bubblewrap 0.13.0。
安装
本插件是标准 DSH bundle 插件(package.json 里的 dsh.bundle.patch)。三选一。
方式 A:从 GitHub 安装(推荐,无需克隆)
dsh plugin --profile <profile> add github:Jumqyc/dsh-wsl-gpufix
dsh plugin 会转发给 pnpm,并在成功后自动把该包登记进 dsh.profile.bundles。装完重启
该 profile。
方式 B:从本地克隆安装
git clone https://github.com/Jumqyc/dsh-wsl-gpufix.git
dsh plugin --profile <profile> add link:/绝对路径/dsh-wsl-gpufix
请用绝对路径(或在仓库目录内用 link:.):相对路径会按你执行命令的目录解析,而不是
profile 目录。
方式 C:home patch 层(所有 profile 生效,不改任何 profile)
bash install.sh
会写入 $DSH_HOME/cordis.patch.yml,行里的 name 是本仓库 gpu-device-grant.mjs 的
绝对 file:// URL,因此对每个 profile 生效。请与 A/B 二选一。
卸载
dsh plugin --profile <profile> remove dsh-wsl-gpufix # 方式 A–B
# 方式 C:删除 $DSH_HOME/cordis.patch.yml 里的 gpu-device-grant 条目
让 AI 帮你装
把下面这段直接发给任意 DSH agent:
请帮我安装 DSH 插件 dsh-wsl-gpufix,来源 github:Jumqyc/dsh-wsl-gpufix,装到 <profile>:
1. 执行:dsh plugin --profile <profile> add github:Jumqyc/dsh-wsl-gpufix
2. 重启该 profile(或等待 patchReload: live 重载补丁层)。
3. 确认已生效:dsh --profile <profile> --dump-config | grep gpu-device-grant
4. 在普通 workspace-write bash 会话里验证 GPU:
python -c "import os, torch; os.close(os.open('/dev/dxg', os.O_RDWR)); print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
期望输出:True <GPU 名称>。
5. 若仍失败:cuInit 报 100 说明 /dev/dxg 不通;nvidia-smi 正常但报 304 说明 /proc 没
授权。Landlock 档这两条都写进该行的 readWrite;bwrap 档设备写进 deviceBinds,
/proc 由 profile 自带。
6. 若 `ls /dev/dxg` 报 "No such file or directory",说明 runner 是 bwrap 且 --dev-bind
没注入、或落在了 `--dev /dev` 之前。检查该行的 deviceBinds 与插件 warning 日志。
验证
在普通(默认 workspace-write)agent bash 会话里执行:
python -c "import os, torch; os.close(os.open('/dev/dxg', os.O_RDWR)); print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
期望 True <你的 GPU 名称>。
请用 os.open(..., os.O_RDWR),不要用 open(path, 'r+b'):带缓冲的 open 会 seek,
而字符设备不可 seek,于是会在权限检查通过之后抛
UnsupportedOperation: File or stream is not seekable——那不是被拒绝。
配置
| 键 | 默认值 | 含义 |
|---|---|---|
readWrite |
['/dev/dxg', '/proc'] |
授予沙箱读写的绝对路径——仅 Landlock 档。调用时不存在会被跳过(launcher 遇到打不开的授权路径会 fail closed,所以缺失的设备绝不能授权)。 |
deviceBinds |
['/dev/dxg'] |
用 --dev-bind <path> <path> 挂进沙箱的绝对设备节点——仅 bwrap 档。不存在的会被跳过:bwrap 遇到绑不上的源会让整条命令失败。 |
modes |
['workspace-write'] |
生效的 sandbox 模式。想让 read-only 也能用 GPU 就加进去;danger-full-access 本就不受限,无需配置。 |
两个路径列表刻意不通用:Landlock 表达不了设备节点挂载,bwrap 也表达不了 --rw /proc
(把宿主 procfs 绑进去会破坏 profile 自带的 --proc /proc)。每个 runner 只读自己能兑现
的那个列表,所以在 bwrap 档下 readWrite 完全不生效。
Landlock 档下只跑 nvidia-smi(NVML)而不跑 CUDA 的最小配置(注意此时 CUDA 会报 304):
config:
readWrite: ['/dev/dxg']
安全
本插件会按你配置的路径扩大沙箱边界,请先阅读 SECURITY.md。
要点:
- 它只是往
dsh-sandbox-local已经构造好的 argv 上追加 runner 参数:Landlock 档加--rw,bwrap 档加--dev-bind。它无法关闭沙箱、修改审批策略,也不会执行配置里的代码。 - Landlock 档下
/proc比单个文件宽(CUDA 初始化必需)。DAC 仍会把/proc的写入限制在 进程自身条目和 root 所有的 sysctl 上,且 launcher 保持no_new_privs。 - bwrap 档下只挂你点名的设备节点,一条
--dev-bind <path> <path>对应一个。它绝不使用--dev-bind /dev /dev——那会把宿主机所有块设备、输入设备、随机数设备都交给沙箱,正好 废掉--dev /dev建立最小/dev的目的。 - 改写只作用于你列出的模式(默认
workspace-write)。
故障排查
| 现象 | 检查 |
|---|---|
| GPU 仍被挡 | 行是否已生效:dsh --profile <profile> --dump-config | grep gpu-device-grant(方式 A–B)或 $DSH_HOME/cordis.patch.yml(方式 C) |
ls /dev/dxg 报 "No such file or directory" |
runner 是 bwrap:该行的 deviceBinds 必须含 /dev/dxg,且插件要新到会注入 --dev-bind。这是 bwrap 档特有的"节点不存在",不是权限被拒 |
cuInit = 304 且 nvidia-smi 正常 |
Landlock 档下 /proc 没授权(或不存在被跳过);bwrap 档不会出现,/proc 由 profile 自带 |
| 日志提示 "neither the Landlock launcher nor bwrap" | 选中的是别的后端(如 Seatbelt、Windows ACL runner,或自定义 runnerCommand),没有做任何改写 |
| 日志提示 "no --dev before the separator" | 该 bwrap profile 不挂 devtmpfs——bind 会被它盖掉,所以插件宁可不注入,也不重演原故障 |
每条受限命令都失败并报 TypeError: Cannot read properties of undefined (reading '0') |
包装层从 promise 上读了 .argv:confine 自 dsh 0.1.3 起是 async。升级插件——现在会同步/异步自适应并转发 signal |
每条受限命令都失败并报 bwrap: Unknown option --rw |
旧版插件把 Landlock 参数注入了 bwrap profile,升级即可 |
dmesg 里 dxgkio_query_adapter_info: Ioctl failed: -22 |
WSL 既有噪声,未受限终端同样出现 |
read-only 模式下 GPU 被挡 |
把 read-only 加入 modes |
兼容性
插件依赖 LocalSandboxProvider.prototype.confine 存在、Landlock launcher 的 --rw 参数
契约,以及 bwrap 的 --dev /dev … --dev-bind … profile 形状。若未来 DSH 改动其中任一,
该行会打 warning 并停止改写,但沙箱继续正常工作——失败方向是「更安全」,不会放开。升级
DSH 后重跑验证命令即可。
测试
node --test
测试不依赖 GPU、/dev/dxg 或真实沙箱。
许可证
链接
同类插件
6Mikao9/dsh-wsl-workspace★ 100
从 Web GUI 添加 WSL 工作区,无需在 WSL 之中再次安装 dsh 以及相关工具,bash 命令与文件读写运行在本机 WSL 发行版内,Windows 文件仍可访问。
XINY11451/dsh-wsl★ 7
将 DSH 的工作范围扩展到 WSL。插件支持了发行版中的文件直接被 DSH 读取、写入、编辑与搜索。面对不同任务需求,AI可根据任务需求自行选择执行一次性命令/自建终端执行多条命令以完成任务。支持在 WSL 终端中键入 dsh 命令调用 DSH(详情在 WSL 终端键入dsh help),深度融合了 WSL 与 DSH。用户新建的所有终端支持与AI可视化协作。
Edge-Echo/dsh-win-toolkit★ 4
面向 DSH agent 的 Windows 原生工具:剪贴板读写、系统通知、hosts 文件查看与网络诊断,均返回结构化数据(带 UI 卡片),底层为防注入的 PowerShell。
173787247/dsh-wsl-env★ 3
向 system prompt 注入 WSL 发行版、Linux 路径映射、/mnt/c 的 CRLF 与 git 注意点,以及 NODE_USE_ENV_PROXY。
173787247/dsh-wsl-open★ 3
把 DeepSeek Harness 聊天里的 WSL Linux 路径在 Windows 默认程序或资源管理器中打开。
Han-1413141/dsh-wsl-native★ 3
在同一个 Windows 窗口中使用 Windows 与原生 Linux DSH 会话,支持 WSL 环境切换和双向系统操作。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。