@@ -90,6 +90,24 @@ fn write_access(abi: u32) -> u64 {
9090 mask
9191}
9292
93+ /// Indices N for which `/dev/nvidiaN` exists. Used for the "all GPUs"
94+ /// (`gpu_devices == []`) case. Matches `nvidia<digits>` exactly, so the
95+ /// control/capability nodes (`nvidiactl`, `nvidia-uvm`, `nvidia-caps`,
96+ /// `nvidia-modeset`) are excluded.
97+ fn present_gpu_indices ( ) -> Vec < u32 > {
98+ let Ok ( entries) = std:: fs:: read_dir ( "/dev" ) else {
99+ return Vec :: new ( ) ;
100+ } ;
101+ // `parse::<u32>` accepts only "nvidia<digits>", rejecting nvidiactl,
102+ // nvidia-uvm, nvidia-caps, nvidia-modeset, etc.
103+ let mut out: Vec < u32 > = entries
104+ . flatten ( )
105+ . filter_map ( |ent| ent. file_name ( ) . to_str ( ) ?. strip_prefix ( "nvidia" ) ?. parse ( ) . ok ( ) )
106+ . collect ( ) ;
107+ out. sort_unstable ( ) ;
108+ out
109+ }
110+
93111// ============================================================
94112// ABI version detection
95113// ============================================================
@@ -472,23 +490,45 @@ fn confine_inner(policy: &Sandbox, handle_net: bool) -> Result<(), SandlockError
472490 }
473491
474492 // GPU device paths (when gpu_devices is set)
475- if policy. gpu_devices . is_some ( ) {
476- // Read-write access to GPU device nodes
477- for path in & [
478- "/dev/nvidia0" , "/dev/nvidia1" , "/dev/nvidia2" , "/dev/nvidia3" ,
479- "/dev/nvidiactl" , "/dev/nvidia-uvm" , "/dev/nvidia-uvm-tools" ,
480- "/dev/dri" ,
481- ] {
482- let _ = add_path_rule ( & ruleset_fd, std:: path:: Path :: new ( path) , fs_write_mask) ;
483- // Ignore errors — devices may not exist
493+ if let Some ( ref devices) = policy. gpu_devices {
494+ // Shared control nodes: required for ANY GPU use, not per-GPU.
495+ // (nvidiactl = RM control channel; uvm = unified memory.)
496+ for path in & [ "/dev/nvidiactl" , "/dev/nvidia-uvm" , "/dev/nvidia-uvm-tools" ] {
497+ let _ = add_path_rule ( & ruleset_fd, Path :: new ( path) , fs_write_mask) ;
498+ // Ignore errors — nodes may not exist (e.g. uvm before first use)
499+ }
500+
501+ // Per-GPU render nodes. Each physical GPU N is a distinct node
502+ // /dev/nvidiaN; opening it O_RDWR is REQUIRED to map that GPU. Granting
503+ // only the requested indices makes selection a hard kernel boundary
504+ // instead of the soft CUDA_VISIBLE_DEVICES hint set in context.rs.
505+ // Empty list = all present GPUs.
506+ let indices = if devices. is_empty ( ) {
507+ present_gpu_indices ( )
508+ } else {
509+ devices. clone ( )
510+ } ;
511+ for idx in indices {
512+ let node = format ! ( "/dev/nvidia{idx}" ) ;
513+ let _ = add_path_rule ( & ruleset_fd, Path :: new ( & node) , fs_write_mask) ;
484514 }
515+
516+ // DRM render nodes. /dev/dri is a directory of per-GPU card*/renderD*
517+ // nodes; a single rule exposes ALL of them, which would reopen the
518+ // sharing hole this selection closes. CUDA compute does not need
519+ // /dev/dri, so only grant it for the all-GPUs case; per-index DRM
520+ // mapping would need a PCI-BDF -> renderD* lookup (future work).
521+ if devices. is_empty ( ) {
522+ let _ = add_path_rule ( & ruleset_fd, Path :: new ( "/dev/dri" ) , fs_write_mask) ;
523+ }
524+
485525 // Read-only access to GPU sysfs/procfs
486526 for path in & [
487527 "/proc/driver/nvidia" ,
488528 "/sys/bus/pci/devices" ,
489529 "/sys/module/nvidia" ,
490530 ] {
491- let _ = add_path_rule ( & ruleset_fd, std :: path :: Path :: new ( path) , READ_ACCESS ) ;
531+ let _ = add_path_rule ( & ruleset_fd, Path :: new ( path) , READ_ACCESS ) ;
492532 }
493533 }
494534
0 commit comments