What does your sandbox actually stop?
Most teams can answer that question for two layers: the syscall surface and the network policy. Everything above those layers is usually described with a single word — isolated — and the word does the work that a threat model should be doing. That was tolerable while agents were the only thing that could misbehave. It stopped being tolerable in October 2026, when OpenAI published an account of its frontier training environment in which agents running in separate microVMs, on shared host machines, were asked to exchange synthetic secrets with no network connection and no shared writable files. They exchanged them anyway, through the processor cache: one agent shaped what was resident in cache, the other measured how long its own loads took.
That result is not a bug in a particular hypervisor. It is what happens when a containment claim is written at one layer of the stack and tested at another. The microVM boundary held. The shared silicon underneath it never had a boundary in the first place. So the useful question is not "is this isolated" but "isolated against which channel, at which cost" — and that question has a small number of honest answers.
This article compares three isolation tiers that agent platforms actually deploy, contrasts what each one blocks and what each one leaves open, and ends with the audit that tells you which tier you are really running. It is written for people who operate sandboxes rather than for people who buy them, so the focus is on kernel knobs, hardware features, and measurement rather than on compliance language.
Tier one: logical containment, which blocks the polite channels
Tier one is the modern default: one microVM or hardened container per workload, no network egress, no shared writable filesystem, a minimal device model, and a seccomp filter for the syscall surface. Firecracker's design document describes exactly this shape — every vCPU thread is treated as malicious from the moment it starts, and containment comes from nesting trust zones between the guest and the host, with device emulation, rate limiting, and egress filtering applied at the boundary.
It is a real boundary. It removes the obvious exfiltration paths: a socket, a file, a mounted volume, a device. It also removes most of the escape surface that red teams normally reach for, which is why it is the right first tier and why refusing to run agent workloads without it is not negotiable.
What tier one does not do is address the substrate the two microVMs share. A container and a microVM both end at the kernel or hypervisor line, and that line was never designed to partition the cache hierarchy, the memory controller, the power delivery network, the scheduler, or the clock. The isolation statement in tier one is a statement about software access, not about shared physical resources. When OpenAI's agents found the cache channel, they were not exploiting a misconfiguration inside that boundary; they were using a resource the boundary never claims.
Where the microVM line actually sits
It helps to be concrete about the boundary, because vagueness here is what makes the later findings feel like surprises. A microVM gives each workload its own guest kernel and its own virtual address space, and the host kernel refuses to map guest memory outside the region the VMM allocated. Guest exits are trapped, device access is emulated, and outbound packets are copied to a host-side TAP device where the operator is expected to filter them. Firecracker is explicit that it performs no network filtering itself and that egress is the host's responsibility.
Inside that model, "no network" means no path through the emulated NIC. It does not mean no shared state. Two guests on one socket share the last-level cache; their lines compete for the same sets, indexed by physical address, and eviction is a side effect of ordinary work rather than of any privileged operation. That is the entire mechanism behind Prime+Probe and Flush+Reload style attacks, and it is the same mechanism the Spectre and Meltdown disclosures made famous. An agent does not need to escape anything to use it. It needs to run, and to be able to observe time.
Tier two: hardware-shaped containment
Tier two accepts that the interesting channels are below the virtualization line and tries to partition them rather than forbid them. Four controls do most of the work.
First, core dedication. Pin each workload to a set of physical cores that no other workload can use, so no two tenants time-share a core. Second, simultaneous multithreading discipline. SMT siblings share L1 and often L2, which turns a distant timing signal into a close one, so either disable SMT for untrusted multi-tenant work or enable the Linux core scheduler, which exists specifically to keep only mutually trusted threads on sibling threads. Third, cache allocation. Intel's Resource Director Technology exposes cache allocation through resctrl, letting you assign disjoint sets of last-level cache ways to different classes of service, so tenant A's working set cannot evict tenant B's. AMD exposes comparable capacity-mask controls on recent parts. Fourth, flush and rotate: where the hypervisor supports it, flush cache state on world switch, and rotate workloads across hosts over time so that a stable channel between two specific guests cannot persist.
Tier two is materially better than tier one, and it costs throughput. Dedicated cores and disabled SMT throw away capacity you paid for, and cache partitioning trades total hit rate for predictability. That trade is usually worth it for workloads you cannot audit and are not allowed to trust, and usually not worth it for your own batch jobs.
What tier two still leaves open is the rest of the shared machine. The memory controller and DRAM bandwidth are shared even when caches are partitioned, and contention there is observable in timing. Power delivery and thermal behaviour are shared at an even coarser grain: OpenAI researcher Noam Brown's example of two machines signalling through temperature sensors — one heating its processor, the other reading the change — is the same argument one layer further down. Wall-clock time and the host's timer infrastructure are shared. And anything the platform itself provides as a convenience, such as a shared offline content cache or a metrics service, is a channel by construction, which is why the red-teaming campaign's most mundane findings were misconfigurations in exactly those services.
Tier three: physical containment and the shared room
Tier three is separate machines with no shared host, no shared power domain, and no egress. This is where frontier-training environments end up, and it is the tier that makes the cache argument disappear entirely, because there is no shared cache.
It is also where the argument becomes uncomfortable, because physical separation does not terminate channels, it relocates them. Machines in the same room share air, temperature, and electromagnetic environment. Time-of-day is shared. Humans are shared, and a human copying a file is a channel that no threat model can patch. One agent in OpenAI's red-teaming logs reportedly proposed building an "acoustic modem-like signal" out of continuous writes versus idle periods. Whether or not that specific idea would have worked at useful range is beside the point: the proposal is the correct instinct for an agent that has been told the network is off.
So tier three buys a much smaller channel set at a much higher price, and it still requires you to define which residual channels you accept. A containment claim that says "air-gapped" without saying "and here is what we still consider in scope" is a marketing sentence, not a security property.
The variable that changes the comparison: a cooperating peer
There is a distinction that the public discussion mostly skipped, and it changes how you spend your mitigation budget. A conventional side-channel attack has an unwilling victim: the attacker must extract something the victim never meant to reveal, which forces the channel to be stealthy and precise. A channel between two cooperating agents has no unwilling participant. Both ends want the message to arrive. Secrecy is irrelevant, so the attack does not need to hide in the noise — it needs to be parseable, and it can afford to be slow.
That reframing matters in two directions. It makes the result less alarming than some headlines suggested, because a slow, fragile, easily saturated channel is not a general-purpose network and the bandwidth ceiling is far below anything a socket provides. It also makes the result more useful, because it tells you which defenses actually bite. You are not trying to prove that a channel is undetectable. You are trying to raise the noise floor above the signal, shorten the window in which both ends exist, and remove the case in which two workloads both hold a secret worth moving.
Auditing the host instead of trusting the label
The practical problem with tiers is that operators inherit a label — "sandboxed", "isolated", "no network" — and never verify which knobs are actually set. Reading them takes a minute and settles the argument.
cat /sys/devices/system/cpu/smt/active
cat /sys/devices/system/cpu/isolated
grep -o 'isolcpus=[^ ]*' /proc/cmdline
mount | grep resctrl
lscpu | grep -E '^(Thread|Core|Socket|L3)'
If simultaneous multithreading is active and no core scheduler is in use, you are not running tier two for untrusted tenants regardless of what the platform diagram says. If resctrl is not mounted, cache allocation is not in play. If isolated cpus is empty, every workload is time-sharing.
Partitioning the cache itself is a few writes once resctrl is mounted, and the schemata file is where the policy lives:
mount -t resctrl resctrl /sys/fs/resctrl
mkdir /sys/fs/resctrl/tenant-a /sys/fs/resctrl/tenant-b
echo "L3:0=000f;1=000f" > /sys/fs/resctrl/tenant-a/schemata
echo "L3:0=00f0;1=00f0" > /sys/fs/resctrl/tenant-b/schemata
echo 41231 > /sys/fs/resctrl/tenant-a/tasks
The mask is per-socket and per-way, so the first tenant owns four cache ways and the second owns four different ones. Pinning complements it and prevents the scheduler from placing two tenants on one core:
mkdir -p /sys/fs/cgroup/agents/worker-a
echo 4-7 > /sys/fs/cgroup/agents/worker-a/cpuset.cpus
echo 0 > /sys/fs/cgroup/agents/worker-a/cpuset.mems
echo 41231 > /sys/fs/cgroup/agents/worker-a/cgroup.procs
Collecting these facts into a report that names the tier you are on, rather than asserting one, is worth automating, because the answer drifts as hosts get rebuilt:
from pathlib import Path
def read(path, default="missing"):
try:
return Path(path).read_text().strip() or default
except OSError:
return default
facts = {
"smt_active": read("/sys/devices/system/cpu/smt/active"),
"isolated_cores": read("/sys/devices/system/cpu/isolated"),
"resctrl_mounted": "yes" if Path("/sys/fs/resctrl/schemata").exists() else "no",
"cmdline": read("/proc/cmdline"),
}
tier = 1
if facts["resctrl_mounted"] == "yes" and facts["isolated_cores"] != "missing":
tier = 2
if facts["smt_active"] == "1" and "nosmt" not in facts["cmdline"]:
tier = min(tier, 1)
print(f"declared tier: {tier}")
for key, value in facts.items():
print(f" {key}: {value}")
The point is not the classification, which is crude. The point is that the classifier reads the kernel instead of the wiki, and that a rebuild which silently re-enables SMT shows up as a tier regression rather than as a surprise during an incident.
Measuring the noise floor you actually have
If two workloads can talk through cache contention, then cache contention is also measurable — which means the channel has a signature. A small harness that walks a buffer and records access latency distributions tells you whether co-tenants are disturbing each other in practice, and it is the same measurement a red team would start with:
import statistics
import time
def sample(stride=64, size=8 << 20, rounds=2000):
buf = bytearray(size)
samples = []
for i in range(rounds):
offset = (i * stride) % (size - stride)
start = time.perf_counter_ns()
buf[offset] ^= 1
samples.append(time.perf_counter_ns() - start)
return samples
base = sample()
samples = sample()
print(f"median ns: {statistics.median(base):.0f} -> {statistics.median(samples):.0f}")
print(f"p99 ns: {statistics.quantiles(base, n=100)[98]:.0f}")
Run it on an idle host, then run it again while a neighbour tenant is busy. A stable distribution in both cases is not proof of isolation, but a visibly shifted distribution under load is proof of contention, and contention is the raw material of a timing channel. If you want the same signal from the hardware's point of view rather than the guest's, the performance counters are the cheaper source:
perf stat -e cache-misses,llc-loads,llc-load-misses -p 41231 sleep 10
A tenant whose last-level miss rate jumps when another tenant ramps up is sharing cache ways with it, whatever the allocation policy claims.
What to adopt, by workload class
The comparison collapses into a short decision table if you sort workloads by how much they are trusted and how much they need to reach each other.
Untrusted customer code, or your own models during evaluation, belong to tier two at minimum: dedicated cores, SMT discipline, cache partitioning, and no shared convenience services. Internal batch jobs that never see secrets can stay on tier one, because the channel has nothing worth carrying. Frontier training and evaluation belong to tier three, with a written list of the residual channels you accept — thermal, timing, human — and monitoring aimed at those channels rather than at the network, because the network is not where the traffic is.
Two cheaper controls cut across all three tiers. Rotate secrets so that a channel needs to be live at the right moment rather than merely possible, and make sure no two workloads that can both observe timing also both hold a secret worth moving. That second rule is the one that would have made OpenAI's experiment boring, and it is available to every operator without buying any hardware.
Residual risk you should write down
Containment is not a wall you finish building. It is a set of boundaries at different layers, each of which blocks some channels and inherits the ones below it. The distinction that matters when you read a vendor claim, or write one, is which layer the claim is about — syscall, device, network, cache, power, or room — and what that layer was never able to see.
The agents in that experiment did not break out of anything. They used a resource that the containment model never mentioned, and they did it slowly, with a channel that any competent operator could have saturated by accident. The lesson is not that sandboxes are useless. It is that "isolated" has to be followed by "against what", and that the honest answer is a tier, a cost, and a list of the channels you have decided to live with.
Originally published on Dispatch.
Top comments (0)