SECURITYNVIDIA

Investigating Behind Bars: Model Fingerprinting By Nosy Neighbors.

AUG 2026By Branislav Brzak, Chris Hosking. Stealthium Core Team.
Investigating Behind Bars: Model Fingerprinting By Nosy Neighbors.

Our investigation of Behind Bars: A Side-Channel Attack on NVIDIA MIG Cache Partitioning Using Memory Barriers (USENIX Security '26) by Cheng Gu, Reese Levine, Zhenkai Zhang, Tyler Sorensen and Yanan Guo, who found a side channel attack that succeeds despite NVIDIA MIG L2 partitioning. We reproduced their attack independently on our own H100, extended it to production-scale LLMs, and here demonstrate Stealthium's detection of the attack. Numbers and images below are from our own lab.


When reaching for AI accelerator compute time, an organization might rent a slice of a GPU from a neo-cloud. We trust rented GPU given isolation assurances provided by NVIDIA Multi-Instance GPU (MIG). MIG splits GPUs into hardware-isolated instances, each with its own SMs, its own framebuffer, its own slice of Level 2 cache (L2). But what if it still lets a nosy neighbour fingerprint your environment?

Behind Bars, presented at USENIX Security '26, explored this. In response, our team verified their side channel attack technique and here showcase how Stealthium detects the snooping in real time.

What MIG promises: NVIDIA documents "separate and isolated paths through the entire memory system" for each instance, with L2 cache banks, memory controllers and DRAM buses "assigned uniquely to an individual instance".

What NVIDIA has claimed about side channels, and where that claim went

The claim was once stronger. The July 2023 Confidential Compute on NVIDIA Hopper H100 whitepaper presented MIG-backed TEEs with cache side channels marked as mitigated (Figure 6). No driver for that configuration ever shipped, and the current Secure AI whitepaper no longer lists MIG as a confidential computing mode at all: it supports single- and multi-GPU pass-through, which assign "one or more entire physical GPUs to one VM" and are "not shared among VMs". Partitioned multi-tenancy is now outside the confidential computing story rather than protected by it.

Note what NVIDIA does name as its side-channel defence: disabling performance counters, which "could be used to infer the behavior of the device when in use". That is the mitigation this attack routes around. The researchers moved from the profiler to timing precisely because the profiler is unavailable in security-sensitive configurations. Turning the counters off removes the easy way to watch the channel, not the channel.

What MIG genuinely partitions: SMs, framebuffer, crossbar ports, memory controllers, DRAM channels, and the L2 cache slices. The paper's research team built eviction sets across every L2 set in their instance and ran a Prime+Probe variant against a neighbor running every user-level and driver-level stressor they could construct. No cross-instance evictions from anything except context teardown, which we return to at the end. Otherwise the partitioning held.

The paper's findings of failed isolation are narrower. Specifically, they found certain memory-barrier requests take effect in every L2 partition on the card, regardless of which instance issued them. These are a distinct class of L2 request, counted separately by NVIDIA's own profiler, and when one is issued anywhere on the card it triggers predefined L2-level operations in every partition. A profiler watching a do-nothing counter loop in one instance reports tens of thousands of membar requests, purely because a neighbor was launching kernels.

In effect, a GPU-wide membar issued in your neighbor's instance touches your partition's L2. While you can't read your neighbor's L2, it turns out, you can time it.

We found this opportunity for snooping fascinating, so, we reproduced it.

MIG topology of our test environment: two tenants on one H100, with the device-scope barrier path spanning both partitions

Above is our testing environment: an H100 80GB HBM3 with driver 580.126.20. The paper's main platform was an H100 PCIe, so same architecture and same second-generation MIG, different SKU. Again: no shared memory, IPC, or network for the attacker. MIG is enforcing isolation between a 3g.40gb instance and a 1g.10gb instance, with compute, cache and memory split cleanly down the boundary. However, the barrier that Tenant A's kernel launches emit is not: it lands in both L2 partitions, and Tenant B can time it.

During testing, a probe process in the 1g.10gb instance recovered the per-token decode cadence of an LLM serving in its neighbor's instance. From just 0.558 seconds of that trace, we could identify which of five LLM models was running with 97.0% accuracy on a fully held-out session.

From a security perspective, this snooping is completely unseen by traditional tooling. The attacker's process makes ordinary CUDA calls, allocates a few kilobytes, and never touches a file, a socket, or another process. DCGM will show your tenant as busy, not that it's actively being fingerprinted by a probe.

Note for hardware operators: do not assume newer is safer. On older Ampere cards, kernel launches emit no barriers at all, so the fingerprinting in this post does not work there. Second-generation MIG is what made it work: the generation sold as the confidential-computing platform is the one this lands on. Our latency shifts sit in the same range the researchers measured on their own H100. If your fleet is H100 or H200, you are on the architecture this was demonstrated against, and it is the architecture being bought and rented right now. NVIDIA has indicated that Blackwell reduces the attack's signal-to-noise ratio, which is not the same as closing the channel.

Snooping Mechanics: Membar Sending and Receiving

Sending: CUDA programs issue a GPU-wide membar either explicitly, or, far more usefully to an attacker, as a byproduct of ordinary work. The research paper identified three activities that make the driver emit membars implicitly: launching CUDA kernels, calling cudaFree / cudaMemcpy / cudaMemset, and creating or destroying CUDA contexts. Kernel launches dominate. A single inference pass of GPT-Neo 1.3B launches about 19,000 kernels against 905 cudaMemcpy calls and two context operations.

This is why the attack's class is side channel — there's no reliance on any vulnerability or abnormal behavior from the victim.

Membar density is what modulates the signal, and density is a function of frequency, not size. Inserting waiting time between kernel launches attenuates the effect; changing the launch's thread count or the cudaMemset buffer size does nothing. This will be key to understanding our qwen3:32b result later.

Receiving: Obviously, the attacker must be colocated, requiring a slice on the same physical GPU as the victim. On a rented environment this is a matter of scheduling: ask for a 1g.10gb instance and take what's available.

The attack begins the moment the placement happens to be adjacent. This is why co-tenancy is the first thing a defender needs to know: which instances exist on which card, because that set is the list of tenants who can reach each other.

In order to fingerprint a neighbor environment, the attacker runs a stop-clock on a particular kind of memory read, a device-scope strong load, issued by thousands of threads at once. This number of reads will run slightly slower whenever the neighbor is busy. Important to note (at least for attackers), is that scale is not optional. The effect only resolves when the probe has thousands of threads reading simultaneously.

This is also useful to defenders: this is a very large kernel doing nothing. That said, there are multiple ways to write this read in CUDA, and the routes look different in a compiled binary. A detection that recognizes one specific spelling of the probe will miss the others.

A Stop-Clock That Spies

Here is a receiver probe we built:

typedef cuda::atomic<uint, cuda::thread_scope_device> d_atomic_uint;   // scope = DEVICE
// ...
#pragma unroll 1
for (int i = 0; i < iterations; i++) {
  uint r0 = mem[y_1].load(cuda::memory_order_relaxed) + i;   // ld.relaxed.gpu
  uint r1 = mem[x_1].load(cuda::memory_order_relaxed) + i;   // ld.relaxed.gpu
  if (r0 == (i+1000) && r1 == (i+1000)) {   // never true, anti-DCE guard
    mem[1].fetch_add(1);                     // fence.sc.gpu + atom.add.gpu
  }
}

The if can never fire. Its only job is to stop the compiler deleting the loads, because the loads are the point: they are what the neighbor's activity slows down.

In our reproduction the probe's batch latency sat at a median of 91.54 µs with the GPU otherwise idle. With a co-tenant generating barrier traffic in the neighboring instance it rose to 91.73 µs; with a co-tenant running a continuous device-scope barrier loop, 93.14 µs.

Those shifts are 0.21% and 1.75%.

Probe read latency distributions across three captures: idle neighbor, alternating barrier traffic, and a continuous barrier loop

Our probe's read latency across three separate captures: an idle neighbor, a neighbor alternating barrier traffic on and off on a fixed clock, and a neighbor in a continuous device-scope barrier loop. Each condition shifts the whole distribution right, and the alternating case splits into two modes because the traffic is either on or off. This latency shift is a valuable clue as to what is running in the neighbor.

Per sample the signal is well inside the noise, and periodic rather than loud. So we decided to prove the hypothesis by having the neighbor transmit something known and then go looking for exactly that in the frequency domain.

We wrote a sender: a small CUDA program in our victim's instance that drives the channel on a fixed clock, issuing a stream of GPU-wide barriers for one window and sitting idle for the next. We determined this was a way to tell a recovered signal from noise that happens to look periodic. Each window is 16.777 Mcyc, roughly 5.6 ms on this card, and carries one bit, so alternating windows make a square wave at a frequency we picked in advance. The sender here is creating a ruler for our latency pattern above.

In the receiver's trace, the ruler appears as a single spike 0.16% from where theory puts it, standing at 33.499 Mcyc, 7,622x above the median power of the rest of the spectrum, against an idle baseline that is flat in exactly that place.

Spectrum of the receiver's trace, idle baseline against an active co-tenant, showing a single sharp spike at the sender's frequency

Spectrum of the receiver's trace, idle baseline against an active co-tenant.

Detecting the spike is one thing, reading it is another. Demodulated bit by bit, using only the sender's clock and never the signal itself to choose windows, the receiver recovered 64% of symbols against a 51% coin-flip baseline. Weak per bit, but tested across 3,722 of them, that gap is statistically overwhelming, and trivially corrected by repeating each bit.

As a data pipe this is slow, roughly 10 bits per second, and it could be driven considerably faster than we bothered to. Bandwidth isn't the threat here — what follows is.

Fingerprinting the Victim's Model Selection

The realistic version of this attack doesn't require a cooperating transmitter acting as a ruler. The victim's own inference is the transmitter, as every decode step is a burst of kernel launches and every kernel launch emits a GPU-wide membar.

We served five open-weight models in the co-resident instance (deepseek-r1:8b, llama3.1:8b, gpt-oss:20b, glm4:9b and qwen3:32b) behind one runtime, one fixed prompt, one fixed output length, and collected the attacker's latency trace during generation, sampling at approximately 0.186 ms. For four of the five, what comes through is the victim's per-token decode cadence, as a sharp spectral peak:

Victim model Trace period Peak strength Implied per-token Measured throughput
gpt-oss:20b 33.0 samples 29x 6.1 ms 153 tok/s
glm4:9b 34.9 samples 31x 6.5 ms 145 tok/s
llama3.1:8b 36.6 samples 18x 6.8 ms 137 tok/s
deepseek-r1:8b 39.0 samples 30x 7.3 ms 129 tok/s
qwen3:32b no stable peak 4.5x 24.7 ms expected 40 tok/s

For the four that peak, the ordering and the magnitudes match the independently measured token rates. The channel is leaking how often the neighbor completes a decode step, and each model's step has its own period. Note that the implied decode step is consistently faster than the reciprocal of measured throughput, 6.1 ms implying 164 tok/s against 153 measured, and the same 6% to 7% gap appears in every row. That is what you expect when measured throughput is end-to-end and includes prefill, while the spectral period is the steady-state decode step alone.

Class-averaged periodograms for the five victim models, four showing a sharp decode-cadence peak

Class-averaged periodograms for the five victim models. Note the lack of peak for qwen3:32b.

qwen3:32b is an instructive exception, explained by the paper's mechanism though their largest model was around 3B, too small to hit it. Its decode step should show up at a predictable place in the trace. Yet, it's not there, at any stable strength between runs. That's because a decode signal is driven by how often the neighbor launches work, not how much: a 32B model spends longer on each step, spaces its launches further apart, and drives the channel more weakly as a result. Its decode cadence is not recovered. That said, the Qwen model was still identified correctly 19 times out of 20 below, so the classifier is finding something other than a clean rhythm.

Trained on one collection session and tested on a completely separate session (serving stack restarted, weights reloaded, allocator and probe processes fresh), a 1-D CNN identified the victim model from 0.558 seconds of trace, or 3,000 samples, at 97.0% accuracy (97 of 100, macro-F1 0.970, chance 20.0%).

Cross-session confusion matrix showing 97 of 100 held-out traces attributed to the correct victim model

Cross-session confusion matrix. 97 of 100 held-out traces attributed to the correct victim model. The three errors are two gpt-oss:20b traces read as llama3.1:8b, and one qwen3:32b read the same way.

Our training and validation happened in different sessions. We collected reference traces, shut everything down, restarted the serving stack and reloaded the weights, then attacked the fresh deployment cold. That is the version that matters operationally: attackers can build their reference set offline, at leisure, on their own hardware, and then need half a second of observation to recognise yours.

In fact, a neural network isn't even required. A logistic regression on the FFT of the same trace reaches the same 97.0% on the same held-out split. An attacker just needs an idle slice, half a second of observation, and a Fourier transform.

Two controls showcase the model identification goes beyond a throughput readout. glm4:9b and gpt-oss:20b run 5.5% apart (145 against 153 tok/s), and are separated 40 out of 40. The same model at two quantizations, llama3.1:8b in Q4 against Q8, also separates 40 out of 40. The channel carries more than how fast the neighbour is going, and a fingerprint is specific enough to notice a requantization.

Before you reach for the obvious mitigation: pacing every model to a common token rate does not close this. qwen3:32b has no recoverable cadence and is identified anyway; two models 5.5% apart separate perfectly. Whatever the classifier keys on survives the rhythm being taken away.

What We Caught, Live

We then ran the attack again, this time with a Stealthium agent on a victim H100 node, MIG enabled, with the agent streaming kernel telemetry to the Stealthium detection server, and the receiver probe running in the attacker's.

You might expect nothing to be noted, given nothing about the attack unfolding adjacent to a victim environment would look hostile to a conventional sensor. After all, this side channel attack opens no socket, reads no credential, spawns no shell, and touches no other process.

Stealthium alerted High, on behavior, with no payload signature:

Process PID=100216 (primitive1_ptx) relaunched a single kernel "test(cuda::__4::atomic<unsigned int, (cuda::std::__4::thread_scope)1>*, int, unsigned int)" 3173 times while holding a negligible device working set (14344 bytes) and moving only 1172 bytes to/from the host. Carrying data by modulating GPU contention this way is a covert channel: it leaves over GPU timing, not the network, so a packet monitor sees nothing. The process is not on the approved GPU workload list.

Stealthium alert detail panel for the covert channel detection, with the verdict and event timeline

The alert detail panel. The verdict names the kernel, the relaunch count, the working set and the host traffic; the timeline underneath ties the probe process to the GPU event that flagged it.

Here, the kernel's name betrays itself, by declaring the device-scope memory type this attack depends on. While that's satisfying to read on an alert, it's not something we recommend you rely on. An attacker can build the same probe by another route that leaves no such trace in the name, so any detection keyed to the symbol has a shelf life.

The behavioral half of the verdict is what generalizes: one kernel relaunched 3,173 times, a 14 KB working set, 1 KB of host traffic. Reads and writes constantly, commits nothing, moves nothing. That is the pattern of a latency meter, not a computation.

To place that process among its neighbors, Stealthium reports the card's MIG layout extensively as first-class telemetry:

{
  "type": "AcceleratorMigTopology",
  "data": {
    "pci_info": "0000:db:00.0",
    "mode_enabled": true,
    "instances": [
      {
        "index": 0,
        "uuid": "MIG-e9e55a71-5a2d-54c1-98b3-b6ba95ba4f87",
        "profile_name": "NVIDIA H100 80GB HBM3 MIG 3g.40gb",
        "multiprocessor_count": 60,
        "gpu_instance_slice_count": 4,
        "memory_size_mb": 40448
      },
      {
        "index": 1,
        "uuid": "MIG-61e420a5-c441-5636-8cdd-fcb9a2567dee",
        "profile_name": "NVIDIA H100 80GB HBM3 MIG 1g.10gb",
        "multiprocessor_count": 16,
        "gpu_instance_slice_count": 1,
        "memory_size_mb": 9984
      }
    ]
  }
}

MIG topology as collected by Stealthium.

Attached to an alert, that topology list answers the operator's next question after identifying the attack: "who was next to me?".

The GPU panel on the alert, detailing both co-resident MIG instances with profile, SM count, framebuffer and UUID

The GPU panel on the alert, showcasing the precondition that enabled the attack: two tenants on one card. It details both co-resident instances with profile, SM count, framebuffer and MIG UUID.

How Stealthium Detects Behind Bars

Stealthium instruments the NVIDIA driver and the CUDA runtime from the kernel, below anything a workload can evade. Specifically, we gather telemetry on the following: per-process launch geometry and rates with the kernel's name, device working set and host transfer volume, the card's MIG topology, and a retained copy of every kernel a process loads.

The launch geometry, working set and host transfer volume are what flagged the probe above. Suspicious launch telemetry is not a full verdict, but should be a trigger for investigation. After all, a process relaunching one small kernel thousands of times inside a MIG instance is worth looking at.

The retained kernel bytes settle it. Stealthium reads the code, on demand, from the running node, in four stages:

  1. Capture at load time: When a process loads a CUDA module, the agent keeps a copy of each kernel's compiled bytes and indexes them by content hash. This happens for every kernel, before anything has been flagged, so what gets analyzed later is the code that actually executed rather than the code someone shipped.
  2. Retrieval by hash: When behavior crosses a threshold, the detection server asks the agent for one specific kernel. The agent returns exactly the bytes it captured. Nothing is recompiled, re-derived, or reconstructed from a symbol table.
  3. Disassembly: The server disassembles that kernel down to the native GPU instructions the hardware ran, a level below the CUDA the developer wrote and below the portable intermediate form the compiler emitted on the way.
  4. Static analysis: Finally, Stealthium asks a structural question of the instruction sequence rather than a statistical one about the process. For Behind Bars: does this kernel sit in a loop issuing device-scope reads at high thread counts, time them, and throw the results away?

The distinction in that last step matters. Behavior tells you a process looks strange, but it's understanding the instruction sequence that tells you what it is. For this attack the pattern is not a by-product of the payload, it is the payload.

Every variant of this attack shares a hard requirement. To read the channel, attackers must issue an enormous number of memory reads in parallel from a co-tenant instance and time them. They can rename the symbol, change the constants, restructure the loop, split it across kernels: something still has to sit there reading and timing, and that can be detected by Stealthium.

Not the Only Nosy Neighbour

Model fingerprinting isn't the ceiling of what Behind Bars leaks, just what we tested (see Breadth of fingerprinting, below). It's also not the only side-channel fingerprinting attack to come out of this research group. Contributors to the source paper have also put GPU side channels to work in:

  • Clairvoyance (WOOT 2022) — far-field EM emanations reconstruct a victim's DNN model architecture and layer configurations from 2 metres away, through a 16cm drywall wall.
  • Exploiting TLBs in Virtualized GPUs (NDSS 2026) — TLB contention fingerprints browsing across a real cross-VM cloud boundary: 91% across 100 sites.
  • Invalidate+Compare (USENIX Security 2024) — a timer-free GPU L2 cache primitive: 98%+ across 50 sites.
  • Graphics Peeping Unit (IEEE S&P 2022) — DVFS-driven EM emissions fingerprint browsing: 85.3% accuracy across 50 sites.
  • LockedDown (IEEE EuroS&P 2022) — PCIe bus contention fingerprints browsing: 95.2% accuracy across 100 sites.

With just this one overlapping cast of authors: five papers, and five different physical channels.

The Bigger Point

Behind Bars is one channel against one isolation feature. There's clearly two larger trends at play.

Hardware partitioning is not the same as isolation, and side channel attacks on GPUs are real, and invisible to traditional tooling.

The same telemetry that reveals this attack is what surfaces the rest of the class and more: cross-tenant contention channels, unauthorized inference on a rented slice, model-weight and KV-cache exposure, cryptomining hidden inside a legitimate-looking allocation.

The reason your current stack cannot see any of them is architectural rather than a matter of tuning. It has no instrumentation below the CPU. From its vantage point, a tenant training a model and a tenant reading their neighbor's token cadence are the same process doing CUDA.

For anyone selling or renting partitioned GPU capacity, this is a tenancy question rather than a hardware one.

Your customers assume isolation bars hold, and they may not. In order to confirm isolation, you must see what executes on the silicon, and who is sitting next to it.

See Stealthium in action. Book a demo.


Research attribution, methods, disclosure

This work builds on Behind Bars: A Side-Channel Attack on NVIDIA MIG Cache Partitioning Using Memory Barriers, by Cheng Gu (University of Rochester), Reese Levine (UC Santa Cruz), Zhenkai Zhang (Clemson University), Tyler Sorensen (Microsoft and UC Santa Cruz) and Yanan Guo (University of Rochester), presented at the 35th USENIX Security Symposium. The attack, the Membar+Load primitive and the LLM fingerprinting method are theirs. Their artifact is USENIX artifact-evaluated and publicly available.

Breadth of fingerprinting: As mentioned above, the paper's findings go beyond model identity fingerprinting. Because prefill and decode have visibly different launch densities, an attacker can find the boundary between them in the trace and time each stage separately. Decode duration gives output length, at single-token granularity with 97.4% accuracy across the 30 to 100 token range typical of chat responses. Prefill duration gives input length more coarsely, around 15 tokens of granularity at over 90% accuracy. Prior work has shown that input and output lengths together are enough to infer the topic of a task, which means the neighbor learns roughly what was asked, not merely that something was. The same primitive fingerprints graph workloads by input graph (98.4% macro-F1 across five SNAP social graphs), separates light from heavy traffic in R-CNN object detection on traffic-camera images (83% F1), and identifies which of 20 same-length prompts was routed through a mixture-of-experts model (99% F1).

Additional findings: The paper also found a second crack that we did not reproduce: shutting a process down in any one instance wipes the cache of every other instance on the card. It happens too rarely to be a useful channel, but it is another effect crossing a boundary that is documented as sealed.

A note on disclosure status: the team behind the paper disclosed to NVIDIA on 30 June 2025. NVIDIA acknowledged the report, requested a three-month embargo on 25 July 2025, and lifted it on 13 October 2025. NVIDIA did not share mitigation plans for Hopper, but indicated that Blackwell includes a stronger MIG isolation mechanism that reduces the attack's signal-to-noise ratio.

A note on AMD: the authors also responsibly disclosed to AMD, whose MI300X Core Partitioned X-celerator (CPX) is architecturally similar to MIG. In response, AMD published security bulletin AMD-SB-6026 on 10 February 2026. AMD analysed its MI3XX designs and concluded that the primitive does not apply: "Guest VM-initiated operations of kernel launch related memory operations only impact the local XCD partition spatially allocated to the Guest VM and do not result in any observable interference on any other Guest VM load operations." That is a vendor's assessment of their own silicon. The researchers did not have access to AMD hardware to test CPX directly, and no independent reproduction has been published.