🌏 中文版
The last post left one question open: containers, gVisor, and microVMs all separate kernels, yet sandboxes on one host still share CPU caches, storage, and CPU time. Can two programs with no connection and no shared writable file exchange data using only those shared resources?
They can, under many conditions. This post covers the mechanism, then uses a small experiment you can run on your own machine to measure how slow such a channel is and when it breaks. Afterwards you should be able to judge whether a shared resource can serve as a channel, and size the threat on three scales: slow, noisy, and needs an agreed protocol.
It connects best after Where an Agent's Code Runs and Containers, gVisor, and microVMs; everything here sits in L3, shared host and hardware.
Two terms: covert channel and side channel
- Side channel. One party observes the side effects of an unwitting target's execution, such as how long something takes. The Flush+Reload paper's phrasing is that side-channel attacks target information an implementation leaks through its interaction with its environment. The Meltdown and Spectre site says both attacks use side channels to get data, and that in the cloud it may be possible to steal data from other customers.
- Covert channel. Two parties collude and deliberately use a shared resource to pass messages. The 2009 EC2 paper describes it as two instances cooperating to send a message via a shared resource, and notes that covert channels provide evidence that exploitable side channels may exist.
The difference is whether one side is a passive victim. This series is about covert channels: both agents are insiders, and the goal is to get around the rule that they must not communicate. Technically, both use the same shared resources.
The mechanism: read the state of a shared resource through speed
A cache is a small bank of fast memory holding recently used data. Reading from cache is faster than reading from main memory, and the time difference is the signal. In their background section, Flush+Reload's authors, Yarom and Falkner, describe this class of cache side-channel attacks as: put the cache in a known state, let the other party act, then measure how long it takes to read the same data again.
To turn that into communication, both sides agree on a convention: to send 1, the sender evicts a piece of data from the cache; to send 0, it does nothing. The receiver re-reads that data in each slot and times it. Slow means evicted, read as 1; fast means read as 0.
- The sender wants to send 1 and evicts a piece of data from the shared cache
- The receiver reloads that data and times it
- The data has to come back from memory, so the read is slow; the receiver reads 1
- The sender wants to send 0 and does nothing
- The receiver reloads the same data and times it
- The data is still in cache, so the read is fast; the receiver reads 0
Illustration with the simplest one-bit mapping. OpenAI's write-up only says one side evicted data and the other timed the reload; it does not give the actual encoding.
This is only one way to use a shared cache. Flush+Reload needs memory pages that both sides share and evicts with the clflush instruction; the 2009 EC2 study used a variant of Prime+Probe (Prime+Trigger+Probe), which fills the cache with the party's own data and then times a re-read, without needing shared pages. OpenAI's write-up does not say which one its agents used; the next post returns to this point.
Beyond cache: storage and CPU time
The same logic applies to any shared resource that gets slower when more people use it. The 2009 paper built a hard-disk version: to send 1 the sender reads random locations on a shared disk volume, to send 0 it does nothing, and the receiver keeps timing a read of a fixed location, slow meaning 1. The authors also measured a memory-bus contention channel, again two instances cooperating through a shared resource. CPU time works the same way: when one side spins, the other side on the same CPU gets less time.
The July 2026 preprint Isolation Failure From Shared Storage is the page-cache version: the sender reads one of two file pages into the cache, the receiver times reads of both pages, and whichever is faster is the bit. It finds a signal on Docker, gVisor, Kata, and others, which is the earlier point that a sandbox can stay off the host and still share the page cache.
Three scales: slow, noisy, needs an agreed protocol
Slow. These channels have low bandwidth, and the range is wide:
| Source | Channel | Measured bandwidth |
|---|---|---|
| 2009 EC2 paper (unoptimized) | Memory bus | 0.006 bps |
| Same | Hard disk | 0.0005 bps |
| Same (refined) | Cache | about 0.2 bps |
| 2026 preprint (Core i7-7700 testbed, lab) | Page cache on Docker | about 478 bps, error rate 0% |
| Same | Page cache on gVisor | about 290 to 357 bps, error rate up to 1.96% |
The hardware, virtualization, and measurement conditions differ, so the numbers cannot be compared directly. What they show is that people have measured everything from under one bit per second to hundreds, many orders of magnitude below a network connection.
Noisy. A real host has other people's work running. The Flush+Reload paper states that its cross-VM experiments were run in a lab on otherwise idle machines. The 2026 preprint found that background file-cache pressure did not drown the signal; it made the "not recently primed" side slower and, in some settings, widened the gap, while the same study shows that a different storage path can make the signal vanish completely. Noise sometimes hides a channel and sometimes does not, depending on the resource.
Needs an agreed protocol. Sender and receiver must know when to start, how long each bit is, and whether slow means 0 or 1. In the 2009 cache channel, the authors describe returning to the prime stage whenever the VM was moved to a different core. Those agreements must themselves be passed over somehow, or hard-coded in advance.
Experiment: two processes fighting over one CPU
Download this Python program; it uses only the standard library and needs Linux. Both processes are pinned to the same CPU: to send 1 the sender spins at full speed for the whole slot, to send 0 it sleeps, and the receiver counts how many small chunks of work it completes in the slot. When the sender spins, the receiver gets less time and a lower count. The two sides agree in advance on the start time, the bit slot, and a 10101010 preamble the receiver uses to compute its decision threshold.
python3 cpu-contention-channel.py
On a 4-core Linux virtual machine, I sent 64 bits with each of six different random messages (the program's --seed 1 to 6); the range of wrong bits at each speed:
| Bit slot | Speed | Wrong bits across six runs (of 64 each) |
|---|---|---|
| 200 ms | 5 bps | 0 |
| 100 ms | 10 bps | 0 |
| 50 ms | 20 bps | 0 to 2 |
| 20 ms | 50 bps | 0 to 4 |
| 10 ms | 100 bps | 7 to 34 |
| 5 ms | 200 bps | 21 to 38 |
Up to 10 bps all six runs were exactly right; at 20 and 50 bps a few bits were occasionally wrong; from 100 bps errors rose clearly, and at 200 bps the worst run had more than half the bits wrong (guessing 64 bits gets about 32 wrong). The threshold moves with the machine and its current load: in another independent rerun, errors at 50 bps were higher. My guess is that it relates to the interval between scheduler switches: once the bit slot gets close to that interval, the receiver's counts can no longer tell 1 from 0. That is "slow" and "noisy" in practice: go faster and it breaks.
A few things to be clear about:
- These are two processes inside one operating system. They do not cross a container or VM boundary, and this is not a cache attack. It only shows that the speed of a shared resource can serve as a signal.
- The preamble and the start time are a protocol I wrote into the program. Without them the receiver would not know where to start or where the threshold is.
- Your numbers will differ. Something you can do tonight: edit the list of bit slots in
main()to values you want to try and find the speed at which your machine starts to fail; then pin the two processes to different CPUs (inrun(), pass different CPU numbers tosenderandreceiver) and see whether the channel disappears.
Limits
- The experiment and the numbers cited all come from controlled settings: one local VM, EC2 in 2009, and the 2026 preprint's lab testbed. They show the mechanism and the order of magnitude, not the real risk on any particular cloud sandbox platform.
- This post does not discuss defenses: what disabling hyper-threading, direct I/O, and rate limiting each block is the subject of post 5.
References
- Hey, You, Get Off of My Cloud: Exploring Information Leakage in Third-Party Compute Clouds — Ristenpart et al., CCS 2009
- FLUSH+RELOAD: a High Resolution, Low Noise, L3 Cache Side-Channel Attack — Yarom and Falkner, USENIX Security 2014
- Meltdown and Spectre
- Isolation Failure From Shared Storage (arXiv:2607.17518)
- Securing our frontier model training environment — OpenAI
- Where an Agent's Code Runs — this site
- Containers, gVisor, and microVMs — this site