20 comments

[ 2.8 ms ] story [ 58.6 ms ] thread
> What is missing is a way forward for getting secret types and the corresponding semantics into LLVM.

Seems crazy that this hasn't happened already given how prevalent and old this issue is.

I think one reason is that it actually entails a pretty big lift from LLVMs side since it would introduce a new invariant that needs to be upheld by optimization passes and code generators.

I’ve heard rumours that Google have done some work internally in this area, but it was never open sourced or upstreamed.

The thing is that this issue predates LLVM. It just feels like the sort of thing that would have been brought up really really early in the process, like as soon as anyone used it for any real world project.

I imagine it's not straightforward, just seems like the effort would have started earlier.

Most projects targeting binaries provide coarse escape hatches that work well enough (in the form of asm volatile). This is a new space where we now need to persist the optimization to work correctly through one AOT pass and then a totally separate JIT which is a new thing and I’m not surprised there’s work to be done.
That's true - we're definitely working with much more complex compilation pipelines now.
If it’s not clear from the title, this article is about maintaining the constant-time property of Rust cryptographic libraries when compiler optimizations tend to compromise that property.

There should probably be some kind ——constant-time compiler flag to prevent the compiler from compromising that property when it’s a higher priority than raw performance.

There’s so many optimization passes that happen in a compiler that I doubt such a flag is feasible. The CT types work is probably a better avenue.
> packaging a library as an npm module using wasm-pack (...) the code would be optimized twice before being executed, first by LLVM and then by the Turbofan JIT compiler

Actually, that's not entirely true; if running wasm-pack, there's another optimizer `wasm-opt`, running on output of llvm and wasm-bindgen.

I've found it able to produce measurable improvements in speed and size of the produced .wasm - which is quite surprising to me, given it's basically a set of simple (compared to llvm) optimization passes running on top of whatever llvm already produced, with none of the metadata. However, this also means it can easily do unexpected things like inlining a `#[inline(never)]` function (because again, it only sees the .wasm file, with none of rust or llvm metadata) - I stumbled upon this several times when trying to profile wasm code in browsers.

This also means that if llvm ever supported constant-time semantics, wasm-opt would happily ignore these too.

You really do need to look at the whole chain. Of course, if Wasm had constant-time annotations as suggested by the article author, you’d expect wasm-opt to respect those.

Then there are steps below that layer to consider as well. Maybe not so relevant when targeting Wasm, but x86 code is increasingly executed inside emulators on ARM chips. Do those respect the constant-timeness?

And what about processor microcode and fused instructions, can a future CPU include an optimization that will un-constant-time your code? (In other words: I really don’t know, are there constant-time guarantees made by the ISA?)

> (In other words: I really don’t know, are there constant-time guarantees made by the ISA?)

Not on AMD64, at least: neither Intel nor AMD will guarantee the timing behavior or timing complexity of an instruction between processors. REP prefixes with string/data operations exemplify this -- they historically had data-dependent timings, but have become increasingly decoupled as both Intel and AMD have shoved "fast string" modes into ucode.

We pay a very heavy price for the complexity of modern compilers: slow compilation times even on crazy fast hardware, real-world bugs from undefined behavior propagation, and now here, leaking cryptographic secrets unless heroic engineering efforts are made to prevent it.

What we get in return for all that pain is slightly faster execution — in theory. A recent post on HN actually showed that at least in some cases, modern LLVM compiles to code that actually runs 1-2% slower than an ancient LLVM version.

Shouldn’t we conclude from all this that what we need is a movement to go back to simple, deterministic and fast compilers?

> What we get in return for all that pain is slightly faster execution

We also get protections against whole classes of security bugs, ala Spectre and other vulns. Some of those protections make speed tradeoffs.

I don’t agree that exploit mitigation is where the bulk of the complexity in the compiler comes from. And if anything, with all the UB inference going on, modern compilers might be more likely to compile exploitable bugs into your code than old ones were. Several well-known examples of this happening exist.

(To explain: A decade ago, if there was a code path where you dereference null, you would get a clean crash. Not pretty, and potentially a denial of service, but usually not further exploitable. Nowadays, the compiler will infer that this path is not taken, eliminate the if, potentially eliminate other checks as well because they are then reasoned to be redundant, and then randomly execute something. This is practically more dangerous in a lot more situations than Spectre ever was.)

It's pretty easy to disable the spectre mitigations, and pretty easy to measure the performance impact (~10% in some workloads).
It would seem to me that your comment only really applies to C or C++ or other similar languages. Modern JavaScript compilers can compile and run code orders of magnitude faster than what was possible fifteen years ago. Many languages have seen dramatic compiler improvements.

Maybe the problem is that making big improvements to a compiler for a fifty year old language that sits pretty close to the metal is getting increasingly difficult because the people writing the compilers are running out of clever tricks.

With all respect, we're discussing an article about the difficulty of balancing performance and security in modern compilers in the context of implementing cryptographic primitives. You might use those primitives from JavaScript (Node.js links OpenSSL, for example), not typically implement them in JavaScript.
One of the two compilers discussed in the article is Turbofan because the code in question is being invoked from JavaScript, so I'm not sure how you think JavaScript isn't relevant. We live in an age where not everything, including cryptography, needs to traverse C or C++ to reach machine code (see: Java, Go, etc).

Plenty of JavaScript and WASM exists for cryptography purposes, for what it's worth, because Node isn't the only runtime and "native" code isn't easily portable.

Am I the only one who clicked on this expecting to read about the barrier method for interior point algorithms?
The article says that Rust does not have a formally defined memory model. What is an example of a formally defined memory model?