# dsh-wsl-gpufix [![test](https://github.com/Jumqyc/dsh-wsl-gpufix/actions/workflows/test.yml/badge.svg)](https://github.com/Jumqyc/dsh-wsl-gpufix/actions/workflows/test.yml) **Unofficial community plugin — not affiliated with or endorsed by DeepSeek.** [English](README.md) | [简体中文](README.zh.md) A Cordis host-composition plugin for [DSH](https://github.com/deepseek-ai) that grants the WSL2 GPU device to the DSH Landlock sandbox, so `torch.cuda.is_available()` is `True` inside a normal (confined) agent `bash` session. ```console $ 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 ``` Install in one command: ```sh dsh plugin --profile add github:Jumqyc/dsh-wsl-gpufix ``` --- ## The problem `@deepseek-ai/dsh-sandbox-local` builds its Landlock profile from a hard-coded list — `/dev/null` always, plus `/tmp` and the workspace root under `workspace-write`. Nothing else is writable, and there is no config field for extra paths. On WSL2 that blocks the GPU in two ways: | Symptom | Cause | |---|---| | `nvidia-smi` → "GPU access blocked by the operating system"; `cuInit(0)` → `100` (`CUDA_ERROR_NO_DEVICE`) | `/dev/dxg` cannot be opened `O_RDWR` | | `cuInit(0)` → `304` (`CUDA_ERROR_OPERATING_SYSTEM`) even though `nvidia-smi` works | libcuda writes its thread name to `/proc/self/task//comm` during `cuInit` and treats the denied write as fatal | The fix is two extra read-write grants: `/dev/dxg` and `/proc`. The background and the bisection that found them are in [`docs/why.md`](docs/why.md). ## What it does While the plugin row is mounted it wraps the public `ctx.sandbox.confine()` seam and inserts `--rw ` grants before the `--` argv separator, only for the configured sandbox modes and only when the selected runner is the Landlock launcher. It does **not** patch any vendor file, disable Landlock, or touch the approval policy, and it restores the original method when the row unloads. ## Requirements - WSL2 with working GPU passthrough — `nvidia-smi` must already work in a normal (unsandboxed) WSL terminal. - DSH with `dsh-sandbox-local` selecting its **Landlock** runner (bubblewrap is not installed). Tested with `dsh 0.1.2-rc.1` on kernel 6.18 / WSL 2.7.13. - A kernel with Landlock (`landlock-run --probe` prints `landlock: fully enforced`). ## Layout ```text dsh-wsl-gpufix/ ├── package.json # DSH bundle metadata (dsh.bundle.patch) ├── cordis.patch.yml # bundle patch: inserts the gpu-device-grant row ├── gpu-device-grant.mjs # the plugin (package entry point) ├── gpu-device-grant.test.mjs ├── home/cordis.patch.yml # template for the home-layer install (method C) ├── install.sh # method C installer ├── docs/why.md # root cause + the bisection that found it ├── SECURITY.md ├── README.md # this file └── README.zh.md # 简体中文 ``` ## Install This is a standard DSH bundle plugin (`dsh.bundle.patch` in `package.json`). Pick **one** install method. ### A. From GitHub (recommended, no clone) ```sh dsh plugin --profile add github:Jumqyc/dsh-wsl-gpufix ``` `dsh plugin` forwards to pnpm and then registers the package in `dsh.profile.bundles` because it declares a bundle patch. Restart the profile, then [verify](#verify). ### B. From a local clone ```sh git clone https://github.com/Jumqyc/dsh-wsl-gpufix.git dsh plugin --profile add link:/absolute/path/to/dsh-wsl-gpufix ``` Use an absolute path, or `link:.` from inside the clone: relative specs are anchored to your invoking directory, not to the profile. ### C. Home patch layer — every profile, no profile changes ```sh bash install.sh ``` Writes `$DSH_HOME/cordis.patch.yml` with a row whose `name` is this clone's absolute `file://` URL, so the grant applies to **every** profile without installing a package into any of them. Use this instead of A/B. ### Uninstall ```sh dsh plugin --profile remove dsh-wsl-gpufix # methods A–B # method C: delete the gpu-device-grant entry from $DSH_HOME/cordis.patch.yml ``` ### Install with an AI assistant Paste this to any DSH agent: ```text Install the DSH plugin dsh-wsl-gpufix from github:Jumqyc/dsh-wsl-gpufix into profile : 1. Run: dsh plugin --profile add github:Jumqyc/dsh-wsl-gpufix 2. Restart the profile (or let patchReload: live reload the patch layer). 3. Check it composed: dsh --profile --dump-config | grep gpu-device-grant 4. Verify the GPU from a normal workspace-write bash session: 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))" Expected: True . 5. If CUDA still fails: cuInit error 100 means /dev/dxg is not granted; error 304 with working nvidia-smi means /proc is not granted. Both belong in the row's readWrite list. ``` ## Verify From a normal agent `bash` session (default `workspace-write` mode): ```sh 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))" ``` Expected: `True `. Use `os.open(..., os.O_RDWR)`, not `open(path, 'r+b')`: the buffered `open` seeks, and a character device is not seekable, so it raises `UnsupportedOperation: File or stream is not seekable` *after* the permission check succeeded — that is not a denial. ## Config | Key | Default | Meaning | |---|---|---| | `readWrite` | `['/dev/dxg', '/proc']` | Absolute paths granted read+write inside the sandbox. Paths that do not exist at call time are skipped (the launcher fails closed on an unopenable grant root, so a missing device must never be granted). | | `modes` | `['workspace-write']` | Sandbox modes the grant applies to. Add `read-only` to allow GPU use there; `danger-full-access` never confines and needs nothing. | Minimal config for users who only need NVML (`nvidia-smi`) — note CUDA will still fail with error 304: ```yaml config: readWrite: ['/dev/dxg'] ``` ## Security This plugin deliberately **widens** the sandbox by the paths you configure. Read [`SECURITY.md`](SECURITY.md) before deploying it anywhere shared. In short: - It can only append `--rw` flags to the Landlock argv; it cannot disable the sandbox, alter approval policy, or run code from config. - `/proc` is broader than a single file. It is required for CUDA initialization; DAC still limits writes under `/proc` to the process's own entries and root-owned sysctls, and the launcher keeps `no_new_privs` set. - The grant applies only to the modes you list (default `workspace-write`). ## Troubleshooting | Symptom | Check | |---|---| | GPU still blocked | Is the row composed? `dsh --profile --dump-config \| grep gpu-device-grant` (bundle methods A–B) or `$DSH_HOME/cordis.patch.yml` (method C) | | `cuInit` = 304, `nvidia-smi` works | `/proc` is not granted (or was filtered out) | | Row logs "not the Landlock launcher" | bubblewrap is installed and won the runner chain; bwrap needs `--dev-bind /dev/dxg /dev/dxg`, a different profile change | | `dxgkio_query_adapter_info: Ioctl failed: -22` in `dmesg` | pre-existing WSL noise; also appears in a working unsandboxed terminal | | GPU blocked in `read-only` mode | add `read-only` to `modes` | ## Compatibility The plugin depends on `LocalSandboxProvider.prototype.confine` existing and on the Landlock launcher's `--rw` flag contract. If a future DSH release changes either, the row logs a warning and the sandbox keeps working without the GPU grant — it fails soft, never open. Re-run the verification command after upgrading DSH. ## Tests ```sh node --test ``` Hermetic: no GPU, no `/dev/dxg`, no sandbox required. ## License [MIT](LICENSE)