fix(sandbox): add WSL2 GPU device and library paths to Landlock baseline

On WSL2, NVIDIA GPUs are exposed through the DXG kernel driver
(/dev/dxg) rather than the native nvidia* devices. CDI injects /dev/dxg
as the sole GPU device node, plus GPU libraries under /usr/lib/wsl/.

has_gpu_devices() previously only checked for /dev/nvidiactl, which
does not exist on WSL2, so GPU enrichment never ran. This meant /dev/dxg
was never permitted by Landlock and /proc write access (required by CUDA
for thread naming) was never granted.

Fix by:
- Extending has_gpu_devices() to also detect /dev/dxg
- Adding /dev/dxg to GPU_BASELINE_READ_WRITE (device nodes need O_RDWR)
- Adding /usr/lib/wsl to GPU_BASELINE_READ_ONLY for CDI-injected GPU
  library bind-mounts that may not be covered by the /usr parent rule
  across filesystem boundaries

The existing path existence check in enrich_proto_baseline_paths()
ensures all new entries are silently skipped on native Linux where
these paths do not exist.

Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
Evan Lezar
2026-04-29 07:21:21 -07:00
parent 93719da236
commit 2254c4d4fd
+61 -5
View File
@@ -1234,14 +1234,31 @@ const PROXY_BASELINE_READ_WRITE: &[&str] = &["/sandbox", "/tmp"];
/// socket at init time. If the directory exists but Landlock denies traversal
/// (EACCES vs ECONNREFUSED), NVML returns `NVML_ERROR_INSUFFICIENT_PERMISSIONS`
/// even though the daemon is optional. Only read/traversal access is needed.
const GPU_BASELINE_READ_ONLY: &[&str] = &["/run/nvidia-persistenced"];
///
/// `/usr/lib/wsl`: On WSL2, CDI bind-mounts GPU libraries (libdxcore.so,
/// libcuda.so.1.1, etc.) into paths under `/usr/lib/wsl/`. Although `/usr`
/// is already in `PROXY_BASELINE_READ_ONLY`, individual file bind-mounts may
/// not be covered by the parent-directory Landlock rule when the mount crosses
/// a filesystem boundary. Listing `/usr/lib/wsl` explicitly ensures traversal
/// is permitted regardless of Landlock's cross-mount behaviour.
const GPU_BASELINE_READ_ONLY: &[&str] = &[
"/run/nvidia-persistenced",
"/usr/lib/wsl", // WSL2: CDI-injected GPU library directory
];
/// GPU read-write paths (static).
///
/// `/dev/nvidiactl`, `/dev/nvidia-uvm`, `/dev/nvidia-uvm-tools`,
/// `/dev/nvidia-modeset`: control and UVM devices injected by CDI.
/// Landlock restricts `open(2)` on device files even when DAC allows it;
/// these need read-write because NVML/CUDA opens them with `O_RDWR`.
/// `/dev/nvidia-modeset`: control and UVM devices injected by CDI on native
/// Linux. Landlock restricts `open(2)` on device files even when DAC allows
/// it; these need read-write because NVML/CUDA opens them with `O_RDWR`.
/// These devices do not exist on WSL2 and will be skipped by the existence
/// check in `enrich_proto_baseline_paths()`.
///
/// `/dev/dxg`: On WSL2, NVIDIA GPUs are exposed through the DXG kernel driver
/// (DirectX Graphics) rather than the native nvidia* devices. CDI injects
/// `/dev/dxg` as the sole GPU device node; it does not exist on native Linux
/// and will be skipped there by the existence check.
///
/// `/proc`: CUDA writes to `/proc/<pid>/task/<tid>/comm` during `cuInit()`
/// to set thread names. Without write access, `cuInit()` returns error 304.
@@ -1255,12 +1272,17 @@ const GPU_BASELINE_READ_WRITE: &[&str] = &[
"/dev/nvidia-uvm",
"/dev/nvidia-uvm-tools",
"/dev/nvidia-modeset",
"/dev/dxg", // WSL2: DXG device (GPU via DirectX kernel driver, injected by CDI)
"/proc",
];
/// Returns true if GPU devices are present in the container.
///
/// Checks both the native Linux NVIDIA control device (`/dev/nvidiactl`) and
/// the WSL2 DXG device (`/dev/dxg`). CDI injects exactly one of these
/// depending on the host kernel; the other will not exist.
fn has_gpu_devices() -> bool {
std::path::Path::new("/dev/nvidiactl").exists()
std::path::Path::new("/dev/nvidiactl").exists() || std::path::Path::new("/dev/dxg").exists()
}
/// Enumerate per-GPU device nodes (`/dev/nvidia0`, `/dev/nvidia1`, …).
@@ -1531,6 +1553,17 @@ mod baseline_tests {
);
}
#[test]
fn gpu_baseline_read_write_contains_dxg() {
// /dev/dxg must be present so WSL2 sandboxes get the Landlock
// read-write rule for the CDI-injected DXG device. The existence
// check in enrich_proto_baseline_paths() skips it on native Linux.
assert!(
GPU_BASELINE_READ_WRITE.contains(&"/dev/dxg"),
"/dev/dxg must be in GPU_BASELINE_READ_WRITE for WSL2 support"
);
}
#[test]
fn local_enrichment_preserves_explicit_read_only_for_baseline_read_write_paths() {
let mut policy = SandboxPolicy {
@@ -1565,6 +1598,29 @@ mod baseline_tests {
"baseline enrichment must not promote explicit read_only /tmp to read_write"
);
}
#[test]
fn gpu_baseline_read_only_contains_usr_lib_wsl() {
// /usr/lib/wsl must be present so CDI-injected WSL2 GPU library
// bind-mounts are accessible under Landlock. Skipped on native Linux.
assert!(
GPU_BASELINE_READ_ONLY.contains(&"/usr/lib/wsl"),
"/usr/lib/wsl must be in GPU_BASELINE_READ_ONLY for WSL2 CDI library paths"
);
}
#[test]
fn has_gpu_devices_reflects_dxg_or_nvidiactl() {
// Verify the OR logic: result must match the manual disjunction of
// the two path checks. Passes in all environments.
let nvidiactl = std::path::Path::new("/dev/nvidiactl").exists();
let dxg = std::path::Path::new("/dev/dxg").exists();
assert_eq!(
has_gpu_devices(),
nvidiactl || dxg,
"has_gpu_devices() should be true iff /dev/nvidiactl or /dev/dxg exists"
);
}
}
/// Load sandbox policy from local files or gRPC.