The Hidden Dangers of Disabling Assertions: Why Production Code Needs Its Safety Nets
#Security

The Hidden Dangers of Disabling Assertions: Why Production Code Needs Its Safety Nets

Tech Essays Reporter
4 min read

A compelling argument against the common practice of disabling assertions in production environments, examining how this seemingly innocuous choice can lead to subtle vulnerabilities and misbehavior that may be more dangerous than the performance gains it provides.

The assertion mechanism in programming languages represents one of the most fundamental yet underappreciated tools for ensuring code correctness. In his thought-provoking article, "You Must Fix Your Asserts," Loris Cro challenges a deeply entrenched practice in software engineering: disabling assertions in production environments. This practice, while common, represents what Cro considers an "irredeemably bad choice" with potentially severe consequences for software reliability and security.

At its core, an assertion serves as a contract between the programmer and the code—a statement about what must be true at a particular point in execution. Whether expressing that "this argument can never be null" or "this integer can never be even," assertions encode the programmer's understanding of invariants that should hold throughout the program's execution. When these invariants fail, the program either crashes (in safe modes) or continues in an undefined state (in optimized modes).

What makes Zig particularly interesting in this discussion is its approach to assertions. Unlike C/C++ where assertions are typically implemented as macros that can be completely disabled, Zig's std.debug.assert is a proper function. This seemingly small difference has profound implications: in Zig, assert expressions are always evaluated, even in release builds. This prevents the common C pitfall where expressions with side effects disappear when assertions are disabled. As Cro demonstrates, this allows for more powerful assertions like assert(my_map.remove("expected-to-exist")) which both checks a condition and performs an operation.

The article's central argument hinges on a critical observation: disabling assertions in production doesn't eliminate the risk of misbehavior—it merely transforms it. When an assertion fails in a checked build, the program crashes immediately, making the failure obvious. When disabled, the program continues running under false assumptions, potentially entering states that were never anticipated or tested. This misbehavior, while less immediately catastrophic than a crash, can be far more dangerous in complex systems, as it may not manifest immediately or may manifest in subtle ways that are difficult to trace.

Perhaps most compelling is Cro's concept of "assertion gaslighting." When developers disable assertions in production, they create a false sense of security. An incorrect assertion that would have been caught in development continues to appear correct in testing, leading developers to build additional code that depends on this false invariant. The example provided—where a developer adds an assertion about thing.is_fooed based on the incorrect assumption that thing.is_started implies thing.is_fooed—illustrates how this can introduce exploitable vulnerabilities that might go undetected for extended periods.

The article's conclusion offers a nuanced perspective rather than a one-size-fits-all recommendation. Cro acknowledges that different applications have different requirements: a static site generator like Zine might benefit from ReleaseFast builds for performance, while a communication platform like Awebo might require ReleaseSafe builds for security. The key insight is that the choice should be deliberate and based on a clear understanding of trade-offs, rather than a reflexive disabling of assertions.

The examples provided—TigerBeetle keeping asserts on for its financial database, Ghostty using ReleaseFast for its terminal emulator—demonstrate that even in performance-critical applications, different approaches can be valid. Notably, Ghostty's CVE history reveals that vulnerabilities can occur without memory corruption, suggesting that the absence of undefined behavior doesn't guarantee security.

This article serves as an important reminder that software engineering is fundamentally about managing complexity and uncertainty. Assertions represent one of our most powerful tools for making complexity manageable by explicitly stating our assumptions. When we disable them in production, we're not just optimizing performance—we're choosing to operate in a state of willful ignorance about whether our code behaves as we expect.

The path forward, as Cro suggests, is not to blindly enable or disable assertions, but to develop a deeper understanding of our code's invariants and test them rigorously. This means treating assertion failures not as annoyances to be suppressed, but as valuable information that points to gaps in our understanding. Only by embracing this mindset can we build software that is both performant and reliably correct.

For developers working with Zig or any language that supports assertions, the article offers a valuable perspective on how to think about these constructs. It challenges us to consider what our assertions are really saying about our code and what we stand to lose when we silence them. In the end, as Cro argues, "you must fix your damn asserts and strive for program correctness, not just for a subset of it."

Comments

Loading comments...