Compiler Can Undo Your Security Checks
summary
Writing secure C source code does not guarantee a secure final binary because compilers can optimize away critical security checks. This happens because compilers follow the C abstract machine rules rather than the programmer's intent, leading to the removal of memory-clearing operations or the creation of time-of-check to time-of-use vulnerabilities. Consequently, security depends on the final machine code rather than the source code alone. Developers must verify the compiled output to ensure protections remain intact.
key points
- Compilers may delete code that appears redundant to the abstract machine, such as clearing sensitive data from memory before a function returns. This results in secrets remaining in RAM where they can be accessed by attackers.
- Small changes in data size or register pressure can lead to vastly different binary outputs, potentially introducing vulnerabilities that were not present in the source code. This means that code which is safe at one data size may become vulnerable at another.
- To mitigate these risks, developers should use compiler warnings, sanitizers, and rigorous testing of the optimized binary that will be shipped. Analyzing the final machine code is the only way to confirm that security protections were not optimized away.
who should read it
C and C++ developers, security researchers, and software engineers who write low-level code. Those interested in how compiler optimizations impact software security will find this highly valuable.
discussion positive
consensus
Optimizing compilers across various languages can remove code that developers consider essential for security, such as memory zeroing or constant-time comparisons, because they prioritize preserving observable behavior over security-specific hyper-properties. While some specialized functions exist to prevent this in C, the general consensus is that compilers treat internal state changes as irrelevant if they do not affect the final output. The discussion also shifts toward the language-level protections of Rust versus C, with agreement that Rust's design inherently prevents many classes of undefined behavior that must be manually managed in C.
top insight
The fundamental conflict arises because compilers are designed to preserve 'trace properties' (sequences of observable behaviors) but not 'hyper-properties' (the relationship between different traces), which is why security properties like confidentiality cannot be guaranteed by standard optimization passes. This distinction explains why a compiler may optimize away a constant-time comparison, as it preserves the result but destroys the timing relationship between different inputs.
Generated by an LLM from the article and thread. Check the sources before relying on it.