Security

rust-timing-shield has a full disclosure policy for vulnerabilities. Since rust-timing-shield intends to protect against adversaries with vast resources, it is assumed that any vulnerabilities are already known to attackers. Accordingly, this project prioritizes disclosing vulnerabilities to users over hiding vulnerabilities from adversaries.

To report a vulnerability: please open an issue on the public GitHub issue tracker. If you believe the issue is serious, I would appreciate it if you also email me separately with a link to the GitHub issue so that I can prioritize it above other issues.

Vulnerability research is an important part of making rust-timing-shield a high-quality framework. Please find bugs!

The rest of this page is a work in progress, but eventually it will be an overview of rust-timing-shield’s attack surface, and will serve as a guide to finding vulnerabilities.

Compiler optimizations

The general principle of an optimizing compiler is that it may transform the input code in any way that it likes, as long as the resulting code is “correct” – it produces the same outputs for the same inputs as the original code. Unfortunately, this principle says nothing about timing or resource usage, which effectively means that optimizing compilers are free to add timing leaks to any program they transform. Nice.

This problem is not just theoretical. There are many documented cases of compilers transforming code that was intended to be “constant-time” into code that, for instance, branches on a secret value.

For example, this Rust code is intended to choose either a or b, according to condition, without leaking the value of condition:

pub fn select_byte(condition: bool, a: u8, b: u8) -> u8 {
    let mask = (condition as u8).wrapping_sub(1);
    (!mask & a) | (mask & b)
}

However, looking at the LLVM IR generated for this function:

 %3 = select i1 %0, i8 %1, i8 %2
 ret i8 %3

LLVM knows exactly what we’re trying to hide from it. The corresponding x86 assembly uses a conditional jump:

  0:	40 84 ff             	test   dil,dil
  3:	75 03                	jne    8
  5:	40 88 d6             	mov    sil,dl
  8:	89 f0                	mov    eax,esi
  a:	c3                   	ret    

This isn’t coincidental. Amusingly, the LLVM “InstCombine” pass has code that pattern matches on exactly the and/or expression we used. Check out this excerpt from the LLVM source code:

/// We have an expression of the form (A & C) | (B & D). Try to simplify this
/// to "A' ? C : D", where A' is a boolean or vector of booleans.
static Value *matchSelectFromAndOr(Value *A, Value *C, Value *B, Value *D,

A proper, complete solution to this problem would likely involve deep changes to LLVM (e.g. adding a field to LLVM’s type system to indicate which values require timing protection). Further research in this direction is enthusiastically encouraged.

Although rust-timing-shield does not make such modifications to LLVM, no timing leak protection tool would be complete without some sort of mitigation for these risks.

An existing timing leak protection library, nadeko, takes advantage of the fact that inline assembly blocks are opaque to LLVM’s optimization passes. By implementing all operations in inline assembly, nadeko prevents the compiler from performing any optimizations whatsoever on timing sensitive code. Although great for security, this approach has a serious performance cost, since it inhibits even simple transformations like mov c, 1; add x, c –> add x, 1.

rust-timing-shield prioritizes performance, and correspondingly takes a different approach. As much as possible, rust-timing-shield interferes with the compiler only when a particular optimization might impact security. As a result, programs can still benefit from most optimization passes including constant folding, algebraic simplifications, and even auto-vectorization.

rust-timing-shield’s mitigations focus on boolean values (i.e. i1 values in LLVM IR), based on the observation that all known problematic transformations only apply when a value is derived from a boolean. For example, the (A & C) | (B & D) optimization described earlier only applies when both A and B are equivalent to a sign-extended i1.

In order to prevent LLVM from recognizing booleans, rust-timing-shield “launders” all protected boolean values through an empty inline assembly block. Because inline assembly blocks appear opaque to optimization passes, this is sufficient to prevent any pattern matching on values derived from booleans.

This approach seems to work well so far in practice, but it might turn out to be insufficient with future versions of the Rust compiler. More research this is needed into whether the current heuristics are sufficient.