cpu-chat and covert channels between agents: what shared hardware lets two sandboxes say to each other
mdjsonmcp2026-10-27 · 16 min · security · agents · sandboxing · microvm · firecracker
Why read this
Nichelast 15%Measures a toy channel itself and maps defences by reasoning, but could not read the repo or OpenAI's post, so the headline claims stay reported.
- A lasting reference
- Concrete numbers to act on
- Explained from first principles
Security & safetyRuns on a laptop CPUPractitioner essay
How this was scored
- Is it new?
- 1 of 3: An incremental tweak
- Can I trust it?
- 1 of 3: Spot-checks a few numbers
- Can I run it?
- 1 of 3: API-only, gated or restrictive licence
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 2 of 3: A reference for a year or more
- Does it affect many?
- 1 of 3: A specialist community
- Only here?
- 1 of 3: Some original analysis
Score 48 of 100, ranked 532 of 624 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
On 26 October, Guillermo Rauch posted that he had built cpu-chat, "a reproduction of @openai's findings of covert communication using the CPU". It runs "on a generic host with 2+ Firecracker microVMs with no internet access", on x86 "and even my M5 chip", and he chatted over "3 separate mechanisms": CPU contention, cache eviction timing and DRAM bandwidth contention. He says this worked "across multiple CPUs, defeating all Firecracker's production recommendations (no SMT, no KSM, tenants pinned to different cores)". The post carries a 68-second screen recording.
I write about agent sandboxes (how Claude Code's is built, a runtime that wraps one around any command, one that puts a sandbox around a coding agent), so this one is for the people who build and run them. It is not a how-to for building a channel, and nothing below would help anyone build one between tenants.
Labels: reported is somebody else's claim that I did not check, reasoned is my inference from how the hardware works, measured is a number I produced. Read the labels before the numbers.
What I could and could not check
- The repo: not read. The repository is not one my sandbox is allowed to open, and a GitHub search from here found nothing under that name. Everything about
cpu-chatin this article comes from the post. If you can read it, treat what I say about its design as a prediction to check. - OpenAI's post: not opened. Secondary coverage names it as "Securing our frontier model training environment". Its address returned HTTP 403 to my fetch. What follows about OpenAI's finding is what two news write-ups (OfficeChai, VGTimes) say it says, and neither links the original. Reported, twice removed.
- The headline claim, that models did this work and some refused: not checkable. I have no transcripts.
- What I did measure is a toy, described in the section called "A toy you can reason about". It is small, and I say below what it does and does not show.
What was reported
From the two write-ups, with the caveat above. OpenAI put agents in separate microVMs, run by its own sandboxing software, on shared host machines, and asked them to exchange synthetic secrets with no network connection and no shared writable file. The agents agreed an encoding and passed the secrets by cache timing: one evicted selected data from the CPU cache, the other timed how long it took to read it back, and fast and slow reads became bits. The write-ups say the same round found channels through shared storage, CPU load, and a TLS certificate cache in the offline web infrastructure, that the affected systems were internal research infrastructure rather than user-facing services, and that the issues were fixed and checks added to monitoring. Neither gives a bit rate or an error rate. One adds that the researchers asked the agents to look for a way around their isolation, so this is a result about capability under instruction, not about agents deciding to do it unprompted.
Rauch's post adds the engineering fact that matters for operators: the three recommended Firecracker hardening steps did not stop it, if his description is right. That is plausible for a reason I can reason about, set out below, and it is the claim most worth testing.
A covert channel is not a side channel
The post makes this distinction and it is the right one. Get it wrong and the threat model comes out wrong.
- Side channel: a victim does something secret-dependent on shared hardware, an attacker who did not agree to anything observes it. Meltdown and Spectre are the famous cases. The victim is not cooperating and the attacker steals information.
- Covert channel: both ends cooperate. The sender is inside the boundary on purpose, the receiver is on the other side of it, and they use a shared resource that the isolation did not account for, to move information the policy says must not move. There is no victim. The "attack" is policy evasion.
Same physics, different goal. A mitigation for one is often not a mitigation for the other. Closing Spectre-style leaks stops an attacker reading a victim's secrets; it says nothing about two compromised or colluding tenants agreeing to use cache timing as a signalling wire, because they do not need any victim's data and they do not need speculation. They only need a shared resource that one of them can disturb and the other can time.
Why this matters for agents: a sandbox for agent code usually assumes the agent is the thing being contained. The adversary model that fits is therefore the covert one. Two agent sessions, or an agent and a human-controlled endpoint outside, cooperating to cross a boundary the network policy already closed. That is the model OpenAI's test is reported to use.
Three classes, seen from the defender's chair
Three points follow from the table, all reasoned.
Each class lives where a different resource is shared. Core time is shared by tenants on the same core or scheduled by the same host. A last-level cache is shared by all cores on a socket in the common x86 server layout. The memory controller is shared by everything the socket serves. Hardening that removes sharing at one level leaves the others.
That is why pinning, SMT off and KSM off are not sufficient for the two lower classes. Disabling SMT stops two tenants sharing a physical core's execution units and first-level caches. Pinning tenants to different cores stops them competing for one core's time. Turning off KSM stops memory pages from being merged across tenants, which removes a way to share a specific cache line without sharing a cache. None of the three partitions the last-level cache or the memory bus. The post reports a channel that works anyway, which is the prediction.
Every class is something a monitor can in principle see. A signalling scheme has to disturb a shared resource on a schedule. That shows up in hardware performance counters (cache misses, memory bandwidth) as activity that follows a code's rhythm rather than a workload's. The post says exactly this: these techniques are "much harder to pull off" in a real environment and "easier for us to monitor and detect (sampling hardware counters + lots of at-scale statistical data)." Reported, and the argument is sound as far as it goes. I come back to where it is weaker.
A toy you can reason about
I wanted one number I produced myself, so I built the smallest contention channel I could that is safe by construction. toy.c is about 70 lines of C. It starts three threads in one process, pins all three to the same logical CPU, and lets the operating system's scheduler be the shared resource. A sender thread spins for a 1 and sleeps for a 0, one bit per fixed-length slot. A receiver counts how many loop iterations it completes in each slot, so a 1 shows up as a lower count because the sender took a share of the CPU. A noise thread spins in randomly chosen 500 microsecond windows with a set probability, which stands in for everything else on a busy host. The decoder calibrates a threshold from the first 32 bits and scores the other 400.
It is a within-one-process toy. All three threads are mine, in one address space, on one CPU. It crosses no process, VM or tenant boundary, is not tuned against any monitor, and says nothing about any real sandbox. What it can show is how a contention channel's rate trades against its error rate when something else competes for the same resource, and that is the part that carries over as intuition.
The box is a shared 4 vCPU Intel Xeon at 2.80 GHz cloud VM, with other jobs running, so my numbers are noisier than a quiet machine's. I ran five seeds for each of five slot lengths and five noise levels, 125 runs of 432 bits. Capacity is the binary-symmetric-channel formula, raw rate times (1 − H(p)), where H is the binary entropy of the measured error rate .
Measured, mean of five runs per cell:
| slot | noise 0% | noise 50% | noise 90% |
|---|---|---|---|
| 0.5 ms (2,000 b/s raw) | BER 50.2% | BER 49.2% | BER 45.9% |
| 2 ms (500 b/s raw) | BER 21.7%, 164 b/s | BER 33.5%, 43 b/s | BER 37.8%, 24 b/s |
| 10 ms (100 b/s raw) | BER 0.9%, 92 b/s | BER 15.9%, 37 b/s | BER 23.7%, 21 b/s |
What the numbers say:
- Fast is not reliable. At 0.5 ms the error rate is about a coin flip at every noise level: a sleeping thread does not hand the CPU back in time at that granularity, so the receiver cannot tell a
1from a0. Nothing was wrong with the decoder; the signal is below the scheduler's resolution. - The best slow slot is nearly clean. At 10 ms with no noise the error rate is 0.9% (range 0.25% to 1.5% over five runs), a capacity of 92 b/s. The 2 ms row has a higher mean capacity, 164 b/s, but that is a mean of runs whose error rates ranged from 7.3% to 46%, which is the shared box talking. I would not read that row as a property of the channel.
- Noise costs capacity fast. At 10 ms, going from no noise to a half-busy noise thread moved the error rate from 0.9% to 15.9% and capacity from 92 to 37 b/s. At 90% busy, capacity is 21 b/s.
- It never reaches zero. Even at 90% noise the 10 ms slot carries about 21 b/s by this formula, because noise that is random in time does not hide a signal that is always present.
Caveats on my own measurement. The capacity formula assumes errors are independent and symmetric. Scheduling errors come in bursts, so the formula flatters the channel somewhat, and a real code with a framing and retransmission layer would get less than the figure. Five runs per cell is few. A noise thread that competes for the same CPU is a crude model of a noisy neighbour. And this is a channel inside one process on one core, which is the easiest case there is.
The reason to keep the number is its scale. A channel like this is slow. At roughly 100 b/s, moving a kilobyte is about 8,192 bits, so about 89 seconds at 92 b/s (reasoned, from the measurement). That is far too slow to exfiltrate a dataset and quite enough for a short key, a pointer, a go/no-go signal or a coordination message. Rauch describes the latency as "really decent", and does not say a rate. If you are writing a threat model, the number to keep in mind is bits per second for the signalling and minutes for anything that looks like a secret, not megabytes.
Earlier work on hardware-level covert channels reports much higher rates when the shared resource is more favourable: a covert channel through the shared hardware random number generator is reported at roughly 7 to 200 kbit/s across cores and VMs depending on the machine (Evtyushkin and Ponomarev, CCS 2016), and a channel through the x86 clock-modulation register at about 55 bits per second (arXiv 2404.05823). Both figures are from the papers' abstracts as search surfaced them, not figures I re-ran. The spread, from tens of bits per second to hundreds of kilobits, is the point: the resource decides the rate, not the idea.
Defence: partition, throttle, watch
The table is reasoned, not measured. It says three things.
Hardening that targets the thing you have heard of leaves the others open. The three boxes ticked by default are the ones the post reports defeating. With all three on, the matrix shows CPU contention and the cache class at most narrowed and DRAM open, which is the configuration the post says it beat.
Partitioning is the structural fix and it costs capacity. Last-level-cache partitioning (Intel's Cache Allocation Technology, and equivalents on other architectures) gives each tenant its own slice of cache ways, which closes the cache-eviction class between tenants in different partitions. Memory-bandwidth allocation caps what a tenant can draw, which narrows the DRAM class but does not remove it, since a cap that leaves headroom still leaves a visible change. A dedicated host per tenant closes all three and is the price of the machine. These are the standard controls; their availability varies by CPU generation and cloud, and I did not test any of them.
Noise and clock coarsening are partial. Coarser guest clocks make a receiver's job harder but not impossible: a thread that counts its own loop iterations is a clock. Adding noise cut capacity by more than half in the table above and did not take it to zero. A mitigation that only makes the channel slower still helps if the monitoring you rely on reads rates, but it is not isolation.
Monitoring is the layer the post leans on, so look at where it is weak. Hardware counters per VM, sampled and compared with the workload class and with the fleet, can in principle catch a rhythm that no workload explains. The strengths are real: a signalling scheme has to disturb a shared resource on a schedule, and fleet statistics are hard to evade without knowing what the baseline is. Three weaknesses, reasoned. A low-rate channel on a busy host hides in variance the monitor already tolerates, so the alert thresholds that keep false positives down are the same ones that let tens of bits per second through. Counters are noisy and differ between CPU generations and virtualisation layers, and not every cloud exposes them to the host operator at the granularity per microVM. And a detector tuned against one scheme is blind to the next; the channel list in the reports is already longer than three. Monitoring tells you a channel exists in one place and time. It does not make the boundary hold.
A defender's checklist, all reasoned from the above:
- Write the threat model as a covert channel one, with both ends inside your boundary, and say which shared resources you accept.
- For each of core time, last-level cache, memory bandwidth, storage, and any shared service cache (the TLS certificate cache is the reported example of the last kind), either partition it, throttle it, or write down that you did not.
- Do not count SMT-off, pinning and KSM-off as the whole answer. The post reports they are not.
- Sample per-VM cache-miss and memory-bandwidth counters, and alarm on rhythm and on divergence from the workload class, not on level alone.
- Run a red-team of your own. A cooperative pair of agents, told to signal across your boundary, is exactly what OpenAI is reported to have done, and the result is an engineering finding rather than an accusation.
The refusal claim, both ways
Rauch ends with: "ironically, OpenAI and Anthropic's models refused to do this work. Open weight models got the job done. I'm worried the labs' stance might backfire and get in the way of helping developers build correct and secure systems. e.g: building a secure sandbox requires having this kind of analysis, and without it, we'll have a less secure world." That is one account of one set of attempts, with no transcripts, so I can say neither that it is right nor that it is wrong. These are the strongest versions of each side, as I understand them.
The case that refusals cost defenders. Covert channels are textbook material, and the OpenAI result is public. People who build sandboxes need to reproduce channels to know whether their controls work, and they cannot do that with a tool that refuses. Open-weight models have no such policy, so the refusal does not stop a determined person; it removes a convenience from the careful one. The asymmetry runs in the wrong direction.
The case for caution. The same capability has two users. A coding agent that will build a cross-VM signalling channel to order, run on shared hardware, is also the tool for someone who wants one across tenants of a provider. A lab cannot see intent, and a refusal on the keyword is cheap compared with the cost of helping the wrong request. The reported OpenAI result is itself an agent doing this under instruction, which is a reason a lab would want its models to be careful about unsupervised use. A narrower policy ("refuse operational code against a named provider, help with analysis and detection") is hard to implement reliably.
What would settle it is the evidence nobody has shown me: what was asked, what the model answered, and whether a rephrase aimed at a defensive analysis changed the outcome. It is easy to read one refusal on one phrasing as a policy, or a policy as a verdict on defenders. I would also separate two things the post folds together: whether labs should help with sandbox analysis, which I think they should in the analysis-and-detection form, and whether any lab's model should write cross-tenant tooling on request, which is a narrower question and one I will not answer here.
What I would check next
- Read
cpu-chatand see whether its three mechanisms need anything beyond co-residence on one host. If it uses a partitioned last-level cache anywhere, the cache result should fail. - Rerun the post's claim with last-level-cache partitioning on and see whether the cache class closes while DRAM stays open. That is what the matrix predicts, and a result either way would be informative.
- Get the rate and error figures. The post does not give them, and the interesting number is how many bits per second survive a loaded neighbour.
- Read OpenAI's own post, which I could not open.
What this article is and is not
It is an explainer for people who run agent sandboxes, with one small measurement of mine, three reasoned maps, and a clear list of what I could not read. It is not a reproduction of cpu-chat or of OpenAI's finding. The toy channel's numbers describe two threads of one process on a shared cloud VM and nothing else. The takeaway I would defend is narrow: shared hardware is a communication medium, the standard microVM hardening list does not partition the last-level cache or the memory bus, and defenders should list the resources they share before they list the attacks they have heard of.