C and C++ specify that bool can only be true or false , which Clang translates to the numeric values 1 and 0, respectively. In most ABIs that LLVM supports, nevertheless, bool has 8-bit storage, which creates “opportunity” for 254 invalid states.
It’s not possible to assign a bool to one of these invalid states because Clang will convert the assigned value to either true or false . However, if you memcpy something on top of a bool , or if you cast some pointer into a pointer to bool , you may well find yourself with a bool that isn’t in a valid state.
Due to hard-dying habits of C and C++ developers, we have found that copying a value that isn’t 0 or 1 in a bool can lead to memory corruption. This happens because at all optimization levels except -O0, Clang adds range metadata to bool values indicating that they can only be 0 or 1.
For instance:
struct foo {
int buf[4];
};
struct request {
bool which_one;
};
void do_call(foo &f, const request &r) {
if (r.which_one) {
f.buf[2] = 100;
} else {
f.buf[1] = 100;
}
}
This code may lead to memory corruption if request::which_one is not 0 or 1. This should only ever set buf[1] or buf[2] , but when which_one is in an invalid state, this function can also write to buf[3] , or out of bounds of buf entirely. See the third execution window for a case which sets f.buf[2] .
The same pattern may lead to the compromise of control flow integrity. Take this example:
class foo {
public:
virtual void if_false();
virtual void if_true();
virtual void cannot_be_called();
};
struct request {
bool which_one;
};
void do_call(foo &f, const request &r) {
if (r.which_one) {
f.if_true();
} else {
f.if_false();
}
}
At -Oz targeting x86_64, you get the following:
do_call(foo&, request const&):
movzx eax, byte ptr [rsi]
mov rcx, qword ptr [rdi]
jmp qword ptr [rcx + 8*rax]
Effectively, the compiler looked at which_one , declared it would only ever be 0 or 1, and decides to call f.vtable[r.which_one]() . If which_one had the illegal bit pattern “2”, this would call cannot_be_called .
One of the worst aspects of this behavior is that engineers who think of C as a high-level assembler don’t know how to fix this issue. In one instance that we reported, the engineer’s proposed fix was to add r.which_one = !!r.which_one , which does nothing. Confronted with that information, they next sought r.which_one = !!(int)r.which_one , which also does nothing. Curing a bool that has been “poisoned” with a value other than 0 or 1 is not possible robustly because the standard has no provision for fixing undefined behavior after it has already happened.
In March, the Exploiting Undefined Behavior in C/C++ Programs for Optimization: A Study on the Performance Impact paper was accepted for PLDI ’25. One of the core tenets of the paper is that several optimizations that exploit undefined behavior, in practice, increase security exposure without demonstrated performance benefits in real-world programs. Assuming that bool is always 0 or 1 is one of the specifically called-out behaviors. Our experience matches those findings: not much is gained by assuming that all bool values are 0 or 1.
-fno-strict-bool
Clang supports -fstrict-enum , which controls whether it can be assumed that all values of an enum type compare equal to at least one of its enumerators. We have implemented -fstrict-bool and its inverse -fno-strict-bool following the same model and we propose upstreaming this change.
In the current implementation, -fstrict-bool is enabled by default (whereas -fstrict-enum is disabled by default), except when compiling with either -mkernel or -fapple-kext , which are Apple-centric. Our implementation of -fno-strict-bool does not take a stance on how to interpret bool values that are neither 0 nor 1: the emergent behavior is that only the lowest bit is considered. This is correct when evaluated in the context of the C and C++ standards: if a bool is a value other than 0 or 1, the behavior is undefined, so no interpretation is off-limits. At this time, the purpose of -fno-strict-bool is only to remove the memory unsafety implications of a bool in an invalid state.
As we upstream -fstrict-bool , we are seeking input on both of these initial choices:
- Should the default really be
-fstrict-bool? Our experience is that the performance gain is negligible, but we only verified on the small part of the system where we chose to enable-fstrict-boolby default, and we are open to the idea that some platforms benefit from-fno-strict-boolmore than others. - What is the desirable behavior for
boolvalues that are in an invalid state? Users could find it more intuitive that all non-zero values are truthy, since that’s how it works for integers, but this is not the current implementation..
Please let us know what you think.