Home · ASPLOS 2024 · Security, Privacy & Trusted Execution

Avoiding Instruction-Centric Microarchitectural Timing Channels Via Binary-Code Transformations

Michael Flanders, Reshabh K. Sharma, Alexandra E. Michael, Dan Grossman and David Kohlbrenner

Conference
ASPLOS 2024
Published
2024
Pages
120-136
Citations
4

Future processors may skip a store whose value is already in memory, or shortcut a multiply by 1, and each such shortcut leaks the secret operand that triggered it, in places (register spills, address arithmetic) that source-level constant-time rules cannot reach — cio, a compiler-plus-binary-analysis tool, finds every instruction in a cryptographic library whose secret operand could trigger the shortcut, rewrites it into a proven-equivalent sequence that cannot trigger it, and then re-checks the final binary, hardening libsodium at a cost of 1.1 to 28 times slower code.

The argument in one glance

  1. Problem

    New hardware shortcuts make single instructions take a different time depending on secret operands

    A silent-store optimization drops a memory write whose value already sits at that address. Computation simplification shortcuts a multiply by 0 or 1, an add of 0, an AND with all-ones, and so on. Each shortcut is triggered by the operand values of one instruction, so an attacker who can see it fire (through timing, store-queue pressure or unit utilization) learns something about the operand. Cryptographic code written to the classic constant-time rules is defenseless: those rules forbid secret-dependent branches and memory addresses, not secret-dependent operands to arithmetic.

  2. What people did

    Vendor constant-time modes, or rewriting the code after each disclosure

    ARM DIT and Intel DOIT promise data-independent timing for a listed set of instructions when a mode bit is set. Otherwise the community updates its coding rules each time an optimization is revealed and reviews every library again. SynthCT can synthesize a replacement for an instruction known to be variable-time.

  3. Why it fell short

    Modes are partial and not user-controllable; rewrites cannot reach compiler-generated code

    DIT is only known to work on Apple M-series chips; DOIT can be set only by the kernel and no OS exposed it as of late 2023; AMD has no standard, and vendors decide what is covered. Source-level rewrites cannot fix register spills, stack argument pushes or address arithmetic the compiler inserts, and SynthCT treats every instance of an instruction as dangerous regardless of whether its operands could ever be secret.

  4. This paper's idea

    Find the leaky instructions in the compiled binary, rewrite each into an equivalent sequence that cannot trigger the shortcut, and prove the result

    cio compiles the library with a modified LLVM, runs static analyses on the binary to prune every instruction whose operands provably cannot hit a trigger value, recompiles with late machine-level passes that replace the remaining instructions by verified transforms (widen and set high bits, split into pieces and recombine, or conditional moves for stores, a blinding write of a value that differs from both old and new), and then re-runs the checkers on the output to confirm no trigger can fire on secret data.

Why this problem is worth solving

Constant-time Code whose running time, memory addresses and branches do not depend on secret inputs such as keys, so that timing measurements reveal nothing about them.
programming is how cryptographic libraries defend against timing attacks. The rules are well known: no branch on a secret, no memory index derived from a secret, no known variable-time instruction such as division on a secret. Those rules assume that an add or a mov to memory takes the same time whatever the operand values are.

Processor designers, pushed by the end of Dennard scaling, keep proposing

instruction-centric optimizations Hardware optimizations that trigger based only on the operand values of a single instruction, in registers or memory. Named by Sanchez Vicarte et al. at ISCA 2021, who surveyed the architecture literature for optimizations with security implications.
that break that assumption. Two examples this paper studies: a
silent store A proposed optimization that drops an in-flight store when the value being written equals the value already at that memory address, saving memory traffic. Mentioned by name in the RISC-V manual and studied in the architecture literature since 2000.
skips a write when the value is already in memory, and
computation simplification A family of optimizations that shortcut arithmetic with a trivial result at run time, for example a multiply by 1 becomes a move, an add of 0 does nothing, an AND with all-ones passes the other operand through.
shortcuts arithmetic with a trivial operand. Neither is deployed yet as far as the authors know, but relatives are: Intel CPUs already simplify 64-bit division by speculating that the upper half of the dividend is zero, and a data-dependent prefetcher the same survey predicted was found in Apple chips and exploited by Augury.

The consequences are nasty. Zeroing a key with silent stores issues a different number of writes depending on how many key bytes are already zero. Prior work showed, in a gem5 simulation, that a single attacker-prepared silent store leaks a key from a bitsliced AES that is constant-time by every classic rule. And because the compiler inserts stores (register spills, stack arguments) and arithmetic (address computation with lea) that never appear in source, no amount of careful C can avoid them. Every newly disclosed optimization would otherwise start the cycle again: attack paper, rules update, rewrite every library.

What people did before

Vendors have begun offering modes. ARM’s Data Independent Timing (DIT, since ARMv8.4-A in 2017) lets any privilege level set a bit that promises constant timing for most arithmetic and stores, but the only processors known to honor it are Apple’s M-series. Intel’s Data Operand Independent Timing (DOIT, 2022) can be set only by the kernel, is not guaranteed under speculation, and no operating system exposed it to user code as of December 2023. AMD has none. The authors also verified experimentally that Apple’s prefetcher still activates with DIT set, showing that vendors and software may disagree about what such a mode should cover. Vendors also ship microcode switches for disclosed attacks (Intel’s SSBD for speculative store bypass), but those are global, per-thread state bits that are awkward to manage.

On the software side, verification tools check the classic constant-time properties, domain-specific languages such as FaCT and CT-Wasm compile to guaranteed constant-time code, and compiler transforms such as Constantine linearize control and data flow. Closest is SynthCT, which synthesizes safe replacements for instructions outside a declared safe subset; it is complementary but coarser, because it replaces every instance of a flagged instruction rather than only those that could see secret trigger values.

  1. 1. Compile new

    A modified LLVM builds the library as usual, records which public API arguments the developer annotated as secret, and reserves scratch registers (r10 to r12) during register allocation so later transforms have space that is not memory.

  2. 2. Check new

    The Binary Analysis Platform lifts the binary to an intermediate form. Checkers assume every instruction leaks, then prune those that provably do not, with interprocedural taint analysis from the secret annotations, interval and memory-domain abstract interpretation, and for stores an SMT query over a 40-instruction backward slice. What remains is the list of potentially leaky instructions.

  3. 3. Mitigate new

    LLVM recompiles; a Machine IR pass placed just before assembly printing replaces each listed instruction with a transform sequence: arithmetic widening, split-and-recombine, conditional move, or a blinding store.

  4. 4. Double-check new

    The same checkers run on the transformed binary, this time expecting everything to be pruned. If any instruction still could leak, the build fails and no binary is produced.

  5. Offline: verify transforms new

    Serval on Rosette interprets the original instruction and its transform from symbolic CPU states, proving equivalence of observable behavior and absence of any trigger condition; also computes a safe ordering when transforms for different optimizations interact.

cio's four-step build wrapped around a normal make, plus one-time offline verification of the transform library.

The idea

The threat model is deliberately strong: the adversary sees every time the optimization fires, and the program is already constant-time in the classic sense. cio’s promise is that the hardened binary leaks nothing more on a processor with the optimization than on one without it.

Everything happens at the

Machine IR LLVM's lowest intermediate representation, a thin wrapper around target-specific machine instructions, produced after register allocation and just before assembly is printed.
level or below, because that is the only place register spills and the micro-operations of complex instructions are visible. The design has three pieces.

Prune, do not enumerate. Computation simplification can apply to almost every arithmetic and bitwise instruction in a binary; transforming them all would be ruinous. So the checkers start by assuming everything leaks and use increasingly precise analyses to remove instructions that provably cannot see a trigger value. Taint analysis from the annotated secret arguments drops anything a secret never reaches. Interval analysis with machine-integer semantics drops instructions whose operand range excludes 0, 1 or all-ones; in ed25519, for example, values are often bit-masked before use, which incidentally makes them safe. A memory domain from Miné handles register-indirect operands. Any stopping point is safe, because whatever is not pruned gets transformed.

Transforms that make the trigger value unreachable. For computation simplification, three approaches ordered by cost. Arithmetic transforms widen a 32-bit or smaller operand into a 64-bit register and set a high bit, shifting its range from 0 to 2^32 minus 1 up to 2^32 to 2^33 minus 1 so it can never be 0 or 1; the operation runs on the wide register and the low bits of the result are correct unchanged. Split-and-recombine handles 64-bit operands and bitwise ops by splitting each operand into a 48-bit and a 16-bit piece (not 32/32, because writing the low 32 bits of an x86 register clears the upper 32, but writing the low 16 does not), setting spare bits in each piece to exclude unsafe values, operating, and recombining; for shifts the recombination is an addition that itself must be made safe. Conditional move transforms use the guaranteed constant-time cmov to build two straight-line branches, substituting a safe static operand in the trigger case and fixing up the result afterward; they cost many instructions and are used only for shift amounts that are dynamic.

For silent stores the trick is a

blinding store An extra store, inserted before the real one, of a value Z chosen so that Z differs from both the old memory contents X and the new value Y. Neither store can then be silent.
: given old value X and new value Y, write Z = NOT(high bits of X concatenated with low bits of Y) first. Z differs from X in its high part and from Y in its low part, so neither the blinding store nor the real store matches memory. Using x86 register aliasing (writing r11b leaves the upper bits of r11 intact) this takes four instructions for a 64-bit store. Transforms may not use scratch memory, since any added store must itself be silent-store safe, which is why registers are reserved at compile time.

Verify the transforms once, then verify the binary cheaply. Each transform is proven offline with Serval to be equivalent in observable behavior (x86 transforms need not preserve unused status flags) and free of trigger conditions from any starting state. Because the checkers verify transforms in isolation under the most general state, and every unpruned instruction in the final binary belongs to a transform, double-checking the whole library stays tractable.

Step by step

Following the paper's Figure 2 and Figure 3 on x86-64 with Intel syntax; r10 to r12 are the reserved scratch registers.

Step 1 of 6

Flag: sub ecx, eax with a secret in eax

Taint analysis finds eax derived from an annotated key. Interval analysis cannot exclude eax = 0, which under computation simplification would let the subtraction shortcut to a no-op. The instruction stays on the leaky list.

Why it works

  • Operand values, not instruction identity, decide safety. By reasoning about value ranges in the binary, cio transforms 4,858 of 13,198 candidate instructions for computation simplification in libsodium instead of all of them, and 1,879 of 2,695 stores for silent stores. Taint alone removed 4,940 CS candidates.
  • Range-shifting is cheap and provable. Setting a high bit is a one-line invariant an interval analysis can check, which is what lets the same checker both find leaks and verify the fix.
  • Transforms are self-contained. Every instruction a transform adds is itself chosen not to trigger the optimizations, so the safety of the whole binary reduces to the safety of each transform in isolation, which is checked once.
  • Late placement. Running just before assembly printing means no later compiler optimization can undo the protection.

What it costs

  • Run time. On libsodium’s reference implementations, all CS and SS transforms together slowed argon2id by 27.84 times, ed25519 by 20.32 times, aesni256gcm by 8.22 times and chacha20-poly1305 by 3.37 times. The single worst contributor is the transform for 64-bit multiply (116 instructions), which alone costs 15.71 times on argon2id and 8.79 times on ed25519. Silent-store mitigation alone is far cheaper: around 1.05 to 1.2 times on most primitives. Just reserving three scratch registers with no transforms costs under 6 percent.
  • Binary size. libsodium’s text grew from 363 kB to 588 kB with SS, 1,118 kB with CS and 1,327 kB with both.
  • Build time. A baseline libsodium build takes about 390 seconds; checking and mitigation each add a similar amount, but CS double-checking takes 11,836 seconds, about 3.3 hours, because Z3 times out on many large nonlinear bit-vector queries. Paid once per release build.
  • Coverage limits. Only x86-64; only libsodium’s reference C code (the faster assembly implementations cannot be seen by compiler passes); transient execution is not modeled, so secrets reaching instructions only speculatively may be missed; the silent-store transform assumes no store-buffer merging before the silence check. Some instructions are deliberately not transformed (rotates, double shifts, add-with-carry, most vector ops besides add, multiply and store).
  • Implementation. 16,503 lines of C++ for the LLVM passes, 10,293 lines of OCaml for checkers, 5,343 lines of Racket for the offline verifier, 474 lines of Bash for cio itself.

Evaluation

The evaluation hardens libsodium built with --disable-asm on an Intel Xeon Gold 6312U. Because no Intel chip implements the target optimizations, the baseline does not benefit from them, so the reported ratios may understate overhead on a real optimizing processor. Each primitive is run 1,000 times (100 for argon2id) with warmup and outlier removal.

PropertySilent stores onlyCS only (all cases)SS and CS
argon2id slowdownabout 1.06x26.08x
ed25519 slowdownabout 1.05x16.73x
aesni256gcm encrypt slowdownabout 1.2x7.08x
chacha20-poly1305 encrypt slowdownabout 1.1x2.67x
Text size (baseline 363 kB)588 kB1,118 kB
Instructions transformed1,879 of 2,695 stores4,858 of 13,198
Double-check time477 s11,836 s
libsodium reference implementations hardened by cio on x86-64; slowdowns are normalized to an unmodified clang build. Per-primitive silent-store-only values read from the paper's Figure 4.Underlined values explain themselves when clicked.

Check your understanding

  1. 1. Why can a silent-store optimization leak a key even from code that follows every classic constant-time rule?

  2. 2. How does the arithmetic transform make sub ecx, eax safe against computation simplification?

  3. 3. What is the blinding store Z in the silent-store transform, and why does it work?

  4. 4. Why must the silent-store transform passes run before the computation-simplification passes?

  5. 5. Which analysis pruned the most computation-simplification candidates in libsodium, and what does that tell you?

Related papers on Security, Privacy & Trusted Execution

See all 153 →