The Little Bool of Doom (2025)
Fedora's latest GCC update to C23 unexpectedly broke chocolate-doom, revealing a decades-old undefined behavior where memset applied to a custom boolean enum later clashed with the C99 _Bool type. This deep dive meticulously traces the bug from compilation error to assembly, illustrating how an invalid boolean value could be both "true" and "false" simultaneously. It's a masterclass in low-level debugging, sparking nostalgic discussions on C's quirks and the pitfalls of implicit assumptions.
The Lowdown
The article details a Fedora Linux maintainer's journey to resolve a chocolate-doom compilation failure during a mass rebuild. The issue stemmed from GCC's default C standard moving to C23, which introduced bool, true, and false as keywords, clashing with chocolate-doom's custom boolean enum.
- The
chocolate-doomproject used a conditionaltypedefforboolean:boolin C++ mode and a customenum { false, true }in C mode. - GCC 15, defaulting to
-std=gnu23, caused a compilation error becausefalsebecame a keyword, clashing with the enum member. - The author initially proposed a fix to use the built-in
booltype for C23. - Upstream maintainers opted to explicitly target C99 and include
<stdbool.h>, which definesboolas_Bool. - This change, however, led to a runtime crash in
chocolate-doomwhere a sprite rotation check mysteriously evaluated totrueandfalsefor the same variable. - Debugging with
gdbshowed thatmemsetwas initializing thebooleanfield (now_Bool, a 1-byte type) with-1(all bits set), resulting in a value of255. - Using Godbolt compiler explorer, the author discovered subtle differences in how the compiler translated
== trueand== falsefor the enum vs._Booltypes. - For
_Boolwith255,== false(effectively!= 1) and== true(effectively!= 0) both passed, demonstrating the "true and false at the same time" paradox. - The root cause was identified as Undefined Behavior (UB), specifically loading a value (
255) into a_Booltype that doesn't represent a valid_Boolvalue, as per C99 section 6.2.6.1.5.
This intricate debugging journey highlights the dangers of undefined behavior, the evolution of C standards, and the unexpected consequences of low-level memory manipulation when combined with changing type semantics, ultimately solving the "little bool of doom."
The Gossip
Undefined Behavior Uncovered: `memset` and `_Bool` Blunders
The central culprit was `memset(sprtemp, -1, sizeof(sprtemp));`, which "sprayed -1s" into the `boolean` field. When `boolean` was an `enum`, this was quirky but often worked, as enums often behave like integers. However, when `boolean` became `_Bool` (a 1-byte type intended for only 0 or 1), `-1` became `255`, leading to undefined behavior as this value is not valid for `_Bool`. This explained how a variable could be `true` and `false` simultaneously, a classic UB trap.
Debugging Dilemmas: Assembly's Role vs. Sanitizer's Swiftness
There was a lively discussion on the debugging methodology. Some argued the author's deep dive into assembly via Godbolt was insightful but perhaps inefficient. They suggested that `UndefinedBehaviorSanitizer` (UBSan) should have been the first step, as it quickly flags the exact UB without needing to decode assembly. Others defended the assembly approach, emphasizing the value of understanding the underlying machine code, especially for experienced developers or those keen on compiler behavior.
C's Conundrums: Boolean Best Practices and Quirky Comparisons
Commenters weighed in on C's approach to booleans. Many reiterated the long-standing C idiom of using zero for false and non-zero for true, questioning the need for explicit `_Bool` types or custom enums. The practice of writing `if (something == true)` or `if (something == false)` was criticized as potentially masking underlying issues and being less idiomatic than `if (something)` or `if (!something)`. The paradox of `_Bool` `255` being both true and false highlighted the non-intuitive behavior when explicit comparisons interact with implicit boolean conversions.
Standard Shifting: C Compatibility and Versioning Challenges
The discussion touched upon the broader implications of language standard evolution and backward compatibility. Some argued that explicit `-std=c17` was the "sanest" initial fix for a package maintainer, rather than modifying source code, as older code shouldn't be expected to adhere to new language versions without issues. The idea of per-file or per-module versioning (`#pragma lang_ver`) was proposed as a solution, drawing comparisons to Go's build constraints and Rust's `edition` system, illustrating a desire for more robust mechanisms to handle evolving language features.