Below the Radar · 3 of 10L3offensive security
When Physics Leaks Secrets: Microarchitectural and Timing Side Channels
Your program keeps its secrets. The silicon underneath it does not — it leaves them in timing, caches, and the ghosts of instructions never meant to run.
Abstract
Software is written against a clean abstraction: a value is private unless you return it. This piece studies the assumption hiding inside that abstraction — that the hardware faithfully executing your code does not itself reveal what the code touched. It does. We trace the leak from Kocher's 1996 timing attacks, through cache side channels and the speculative-execution class named by Spectre and Meltdown, to the shared-hardware reality of multi-tenant model inference, where one user's prompt can leave a measurable shadow another user can read. The defense is not a patch but a discipline: make execution time and memory-access patterns independent of secrets, and isolate the shared state that turns a performance optimization into a covert readout.
A programmer reasons about secrets at the level of the language: this variable is private, that buffer is cleared, this branch is never returned. The machine underneath keeps a different set of books. Caches remember what you touched, the branch predictor remembers where you went, the pipeline speculatively runs instructions that were never supposed to execute — and all of it leaves traces in shared state that an unprivileged observer can measure through nothing more exotic than a clock. The secret never leaves through the front door your code controls. It seeps out through the physics of the hardware keeping the abstraction alive.
The assumption: the abstraction hides the implementation
Every layer of computing sells the same comforting promise: you can reason about the layer above without knowing how the layer below is built. A high-level language lets you ignore registers and caches; a virtual machine lets you ignore the physical host; a cloud tenancy lets you ignore your neighbors. The quiet security assumption riding along is that the implementation is not just invisible but inaudible — that nothing about how the lower layer does its work reveals what the upper layer was doing.
Paul Kocher put the first crack in that assumption in 1996, showing that the time a cryptographic operation takes can depend on the secret key it uses, so simply measuring durations recovers key bits. Nothing was returned, nothing was read; the secret leaked through the one channel no abstraction fully hides — how long the work took.
This class is relational in an unusual way: the leak lives in the relationship between a secret and a shared physical resource, not in any line of code. Read the program and it is correct — it never outputs the secret. Only when you observe the implementation's side effects does the secret appear, which is exactly why it is called a side channel and why source review alone never finds it.
- Name the trust boundary honestly: if two workloads share a cache, a core, or a clock, treat the implementation layer as an information channel, not an invisible substrate.
- Make secret-handling code constant-time by design: no branch, memory access, or loop count that depends on secret data.
- Where isolation matters more than throughput, stop sharing the physical resource rather than trying to obscure the leak.
How physics becomes a channel
A side channel needs two ingredients: a piece of shared microarchitectural state whose condition depends on what a secret-handling program did, and a way for an observer to sense that condition. The cache is the canonical example. When a program accesses memory, the data is pulled into a fast cache; a later access to the same location is faster. If which location a program touches depends on a secret, then the speed of subsequent accesses — measurable by anyone sharing that cache — depends on the secret too.
The observer does not need privilege or a bug. They run their own ordinary code alongside the victim, prime the shared state into a known condition, let the victim run, and then measure how the victim disturbed it. Timing is the readout, and a clock is all the equipment required. The branch predictor, the translation buffers, the execution ports, and the memory bus can all play the same role as the cache.
The width of the leak depends on how much the secret shapes the physical behavior and how cleanly the observer can measure it. A single conditional branch on a secret bit, repeated, is enough to reconstruct the bit over many samples.
- Remove secret-dependent memory-access patterns: use constant-time table lookups or bit-sliced implementations so touched addresses do not depend on the secret.
- Partition or flush shared state across trust boundaries (cache partitioning, cleared buffers on context switch) so the observer cannot read the victim's disturbance.
- Reduce measurement fidelity for untrusted code: coarsen or restrict high-resolution timers where feasible, as a defense-in-depth layer, never the sole control.
Cache timing: reading footprints, not data
The best-studied instances read the victim's memory footprint rather than its memory. In one family the observer fills the shared cache with its own data, lets the victim run, and then measures which of its own lines were evicted — the evictions map to the addresses the victim touched. In another, the observer ensures a shared line is absent, waits, and times how long it takes to access — fast means the victim pulled it in. Yarom and Falkner's work in 2014 showed how sharp this can be against real cryptographic libraries.
None of this defeats the cryptography; it defeats the implementation's habit of touching secret-chosen locations. A table indexed by key bits, a branch taken on a key bit, a multiply skipped for a zero bit — each turns a secret into a footprint, and the footprint into timing.
The countermeasure is to make the footprint constant. If the sequence of memory accesses and branches is identical regardless of the secret, there is nothing for the observer to distinguish, and the channel goes dark.
- Write cryptographic and secret-handling code to be constant-time and constant-footprint: fixed access patterns, no secret-dependent branches, verified with a constant-time checker.
- Prefer implementations and libraries hardened against cache attacks over rolling your own table-lookup code.
- Isolate secret operations onto dedicated cores or use cache-partitioning features so a co-resident observer shares no state with them.
Speculation: the ghosts of instructions never run
The deepest version of the leak comes from an optimization everyone relies on. To stay fast, a modern processor guesses which way a branch will go and speculatively executes ahead; if the guess was wrong, it discards the architectural results and pretends nothing happened. The problem, made famous by the Spectre and Meltdown work in 2018 and 2019, is that the discard is incomplete: the speculative instructions can still have touched the cache, and that microarchitectural trace is not rolled back.
So an attacker trains the predictor to speculate down a path that reads memory it should never reach — past a bounds check, or across a privilege boundary — and although the processor throws the value away, the value's influence on the cache remains. A cache-timing measurement afterward recovers what the doomed speculation saw. The architectural state stayed honest; the microarchitectural shadow told the truth.
Defenses here are layered and costly precisely because the behavior is a feature, not a flaw: serialize where speculation must not cross a boundary, keep secrets out of speculatively-reachable memory, and separate trust domains so a mistrained prediction cannot reach across them.
- Insert speculation barriers where a bounds or permission check must not be bypassed, and adopt the compiler and microcode mitigations for the relevant variants.
- Keep secrets out of memory reachable by mistrained speculation, and separate trust domains (process, core, or machine) so cross-domain speculation cannot occur.
- For high-assurance secret handling, disable simultaneous multithreading on the cores that touch secrets so a sibling thread cannot share the microarchitectural state.
The AI angle: shared silicon, shared secrets
Model inference has quietly recreated every precondition these attacks need. To be economical, providers pack many tenants onto shared accelerators and hosts, batch different users' requests together, and cache intermediate computation — the exact ingredients of a side channel, at a new layer. When two tenants share hardware, one can, in principle, sense the other's activity through timing, and the secrets in play are now prompts, retrieved documents, and model internals.
The channels are natural to the workload. How long a response takes, and how fast individual tokens stream, can depend on the content being processed; a shared key-value cache that speeds up repeated prefixes can reveal that someone else recently submitted a similar prompt; contention on a shared accelerator can betray a co-tenant's load. An attacker who cannot see another user's prompt may still be able to measure its shadow.
The defenses translate the classic ones into the agent era: do not share the caches and accelerators that carry sensitive content across trust boundaries, and make externally observable timing independent of the secret content wherever the leak would matter.
- Do not share KV-caches, prefix caches, or accelerators that carry sensitive content across tenants or trust boundaries; isolate per-tenant where confidentiality matters.
- Pad or normalize externally observable timing (total latency and token pacing) so it does not track the sensitive content being processed.
- Place high-sensitivity workloads on dedicated nodes rather than co-residing them with untrusted tenants, accepting the utilization cost.
Why the leak is structural, not a bug
It is tempting to treat each side channel as a defect awaiting a patch. The frame says otherwise. The leaks come from the very mechanisms that make computers fast and cheap to share: caches exist because reuse is common, speculation exists because branches are predictable, multi-tenancy exists because idle hardware is waste. Each optimization works by letting the recent past shape the present — which is exactly what an observer needs.
So there is a genuine, unavoidable tension: perfect performance sharing and perfect isolation cannot both hold. You can have a fast shared cache or a leak-free one, speculation or a fully-serialized boundary, dense multi-tenancy or strong physical separation. The honest engineering question is not how to remove the channel but where you are willing to pay — in speed, in utilization — to close it for the secrets that matter.
Framed that way, side channels become a placement decision: which secrets must never share a physical resource with an adversary, and which timing must be made independent of which data.
- Classify secrets by whether they may ever co-reside with an adversary, and give the highest tier dedicated, unshared hardware.
- Budget deliberately for isolation (constant-time code, partitioned or flushed caches, disabled sharing) on the paths that touch those secrets, accepting the performance cost.
- Re-evaluate the placement whenever the hardware, the tenancy model, or the optimization set changes, since new sharing reopens the channel.
Measuring the leak before an attacker does
Because a side channel is a statistical dependence between a secret and an observable, it can be tested for directly: vary the secret, hold everything else fixed, and measure whether the observable — execution time, an access pattern, a token-timing profile — changes with it. If the observable is independent of the secret across many trials, the channel is closed for that measurement; if it tracks the secret, you have found a leak without ever mounting an exploit.
For secret-handling code, constant-time verification tools check that no branch or memory access depends on secret inputs. For a service, a leakage test drives it with two classes of secret input and asks a simple question of the timing distributions: can you tell them apart? For an inference stack, the same test compares response and token timing across content that should be indistinguishable to an outsider.
The harness stays strictly defensive: it measures your own system's observables to reveal an unintended dependence; it never attacks a co-tenant, and it uses synthetic secrets rather than anyone's real data.
- Add a leakage test to CI that fails when an observable's distribution depends on secret inputs, for both secret-handling code and externally-timed services.
- Run constant-time verification on cryptographic and secret-branching code, treating any secret-dependent branch or access as a defect.
- Threats to validity: a null result only covers the observables and inputs you tried — pair it with isolation so untested channels cannot leak the crown jewels.
The discipline: make time indifferent to secrets
Every countermeasure here reduces to two moves: make the secret-handling code's observable behavior — its time and its footprint — independent of the secret, and stop sharing the physical state that would otherwise carry the leak across a trust boundary. Do the first and there is nothing to measure; do the second and there is no one positioned to measure it.
The reusable artifact is an assumption-ledger entry: the unstated assumption is that the implementation layer hides what the program did; the reason it fails is that shared microarchitectural state and secret-dependent timing make the implementation observable; the tell is any measurable dependence of an observable on a secret; and the assumption-free control is constant-time, constant-footprint code plus isolation of shared state. Carry that entry to any layer — a crypto routine or a shared inference cluster — and ask the one question this class demands.
Ask it wherever a secret meets shared hardware: can anyone downstream tell, from timing alone, what my secret was? Where the answer is 'maybe', the abstraction is leaking, and physics is doing the talking.
- Adopt one rule per secret path — constant-time behavior plus isolation of shared state — and record which the path relies on.
- Inventory every place a sensitive workload shares a cache, core, accelerator, or clock with untrusted code, and close or accept each consciously.
- Audit the inference stack for content-dependent timing and cross-tenant caches, the modern home of this classic leak.
Key takeaways
- A side channel leaks a secret through the implementation's physical side effects — timing, caches, speculation — not through any value the program returns, so code review misses it.
- The broken assumption is that the hardware faithfully hiding the abstraction does not reveal what the code touched; shared microarchitectural state and secret-dependent timing break it.
- Cache-timing attacks read a secret-shaped memory footprint; speculative-execution attacks (Spectre, Meltdown) read a trace that survives an architectural rollback.
- Model inference recreates every precondition: shared accelerators, batching, and prefix/KV caches let one tenant sense another's prompt or content through timing.
- The leak is structural — the optimizations that make systems fast and shareable are the ones that leak — so defense is a placement and constant-time decision, not a single patch.
- You can test for it directly: vary the secret, hold the rest fixed, and fail closed if any observable's distribution depends on it.
Practitioner Toolkit
Copy-paste, strictly defensive artifacts you can use today. Nothing here attacks a real system.
Run this wherever a secret meets shared hardware.
- List every secret and every physical resource (cache, core, accelerator, clock) it shares with other workloads.
- Confirm secret-handling code is constant-time and constant-footprint: no secret-dependent branch, access, or loop count.
- Separate trust domains so mistrained speculation cannot cross into secret memory; apply the relevant speculation mitigations.
- For inference, confirm KV/prefix caches and accelerators are not shared across tenants for sensitive content.
- Pad or normalize externally observable latency and token pacing so they do not track secret content.
- Add a leakage test that fails when an observable's distribution depends on a secret.
A decision rule for where a secret may run.
def place(workload):
if workload.secret_tier == "crown_jewel":
return dedicated_unshared_host() # no shared cache/core/accelerator
if workload.handles_secrets:
require(constant_time=True, # no secret-dependent branch/access
caches_partitioned_or_flushed=True,
smt_disabled_on_secret_cores=True)
return shared_pool()Reveals a secret-dependent observable in your own system.
def leakage_test(run, secret_A, secret_B, trials=10000):
t_A = [time(run(secret_A)) for _ in range(trials)]
t_B = [time(run(secret_B)) for _ in range(trials)]
if distributions_distinguishable(t_A, t_B): # e.g. a statistical test
fail("timing depends on the secret — side channel present")
# Synthetic secrets only; never measure a real co-tenant's data.The highest-leverage steps before deeper hardening.
- Make secret-handling code constant-time and constant-footprint; verify it.
- Give crown-jewel secrets dedicated, unshared hardware.
- Stop sharing KV/prefix caches and accelerators across tenants for sensitive content.
- Normalize externally observable latency and token pacing for sensitive responses.
Glossary
- Side channel
- An information leak carried by a system's physical or timing side effects rather than by its intended outputs.
- Microarchitectural state
- Hidden processor state (caches, branch predictors, buffers) that is shared and observable through timing but not part of the programming model.
- Cache-timing attack
- Inferring a victim's secret-dependent memory accesses by measuring how it changed the shared cache.
- Speculative execution
- A processor running instructions ahead of a branch decision and discarding them if it guessed wrong — while leaving microarchitectural traces.
- Constant-time code
- Code whose execution time and memory-access pattern do not depend on secret inputs, closing timing side channels.
- KV-cache
- Cached key/value tensors that speed up repeated or shared prompt prefixes in model inference, and can leak co-tenant activity through timing.
- Co-tenancy
- Multiple independent workloads sharing the same physical hardware, the precondition for cross-tenant side channels.
References
- Kocher, Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and Other Systems (CRYPTO 1996)
- Kocher et al., Spectre Attacks: Exploiting Speculative Execution (arXiv:1801.01203)
- Lipp et al., Meltdown: Reading Kernel Memory from User Space (arXiv:1801.01207)
- Yarom & Falkner, FLUSH+RELOAD: A High Resolution, Low Noise, L3 Cache Side-Channel Attack (USENIX Security 2014)
- MITRE ATLAS (Adversarial Threat Landscape for AI Systems)
- OWASP Top 10 for Large Language Model Applications
- NIST AI Risk Management Framework (AI RMF 1.0)