3 ms·
tldr; the UB was reading uninitialized data in a struct. The C++ rules for when default initialization occurs are crazy complex. I think a sanitizer probably w
by titzer 10mo ago
tldr; the UB was reading uninitialized data in a struct. The C++ rules for when default initialization occurs are crazy complex.
I think a sanitizer probably would have caught this, but IMHO this is the language's fault.
Hopefully future versions of C++ will mandate default initialization for all cases that are UB today and we can be free of this class of bug.
- trueismywork 10mo agoFor now, best strategy is to initialize everything explicitly.
- torstenvl 10mo agoYeah... but I wouldn't characterize the bug itself (in its essential form) as UB. Even if the implementation specified that the data would be indeterminate depending on what existed in that memory location previously, the bug would still exist. Even if you hand-coded this in assembly, the bug would still exist. The essence of the bug is uninitialized data being garbage. That's always gonna be a latent bug, regardless of whether the behavior is defined in an ISO standard.
- forrestthewoods 10mo agoYeah I agree. This is a classic “uninitialized variable has garbage memory value” bug. But it is not a “undefined nasal demons behavior” bug. That said, we all learn this one! I spent like two weeks debugging a super rare desync bug in a multiplayer game with a P2P lockstep synchronous architecture. Suffice to say I am now a zealot about providing default values all the time. Thankfully it’s a lot easier since C++11 came out and lets you define default values at the declaration site!
- titzer 10mo agoI prefer language constructs define that new storage is zero-initialized. It doesn't prevent all bugs (i.e. application logic bugs) but at least gives deterministic results. These days it's zero cost for local variables and near-zero cost for fields. This is the case in Virgil.
- kevin_thibedeau 10mo agoC & C++ run on systems where it may not be zero cost. If you need low latency startup it could be a liability to zero out large chunks of memory.
- ablob 10mo agoI think it's acceptable to leave an escape hatch for these situations instead of leaving it to easy to misunderstand nooks and crannies of the standard. You don't want to zero out the memory? Slap a "foo = uninitialized" in there to have that exact behavior and get the here be demons sign for free.
- forrestthewoods 10mo agoYeah this issue is super obvious and non-controversial. Uninitialized state is totally fine as an opt-in performance optimization. But having a well defined non-garbage default value should obviously be the default. Did C fuck that up 50 years ago? Yeah probably. They should have known better even then. But that’s ok. It’s a historical artifact. All languages are full of them. We learn and improve!
- deleted 10mo ago[deleted]
- 1718627440 10mo agoI don't know, I expect all variables to be uninitialized until proven otherwise. It makes it easier for me to reason about code, especially convoluted code. But I also like C a lot and actually explicitly invoke UB quite often, so there is that.
- forrestthewoods 10mo agoI like C and it's great. I wish more people wrote C instead of C++. But there's a reason that literally no modern language makes this choice. If uninitialization was opt-in you would still be free to "assume uninitialized until proven otherwise". But uninitialized memory is such a monumental catastrophic footgun that really is not a justifiable reason to make that default behavior. Which, again, is why no modern languages make that (terrible) design choice.
- tialaramex 10mo agoIn C++ 26 reading an uninitialized variable is by default Erroneous Behaviour, which means your compiler is encouraged to diagnose this (it's an error) but if it happens anyway (perhaps because the compiler can't tell before runtime) there's a specified behaviour, it isn't Undefined Behaviour. The compiler will have chosen some value for that uninitialized variable and if it can't just diagnose that what you wrote was nonsense, it has some arbitrary value, perhaps configurable or perhaps described in your compiler's documentation. So these variables will be more or less what the current "defanged" Rust std::mem::uninitialized() function gets you. A bit slower than "truly" uninitialized variables, but not instant death in most cases if you made a mistake because you're human. Those C++ people who feel they actually need uninitialized variables can tell the compiler explicitly [for that particular variable] in C++ 26 that they opt out of this safeguard. They get the same behaviour you've seen described in this thread today, arbitrary Undefined Behaviour if you read the uninitialized variable. This would be similar to modern Rust's MaybeUninit::uninit().assume_init() - you are explicitly telling the compiler it's OK to set fire to everything, you should probably not do this, but we did warn you.
- koyote 10mo ago> Hopefully future versions of C++ will mandate default initialization for all cases that are UB today and we can be free of this class of bug. I have production code where we specifically do not initialise some data in order to be more performant (it gets set before it is read, but not at declaration as we do not have the values at that point). I do agree that this (and numerous other footguns) make C++ a pain to work with. I also think it's too late to fix. Ideally, all values would be initialised by default and instead you could forcefully construct something that is not initialised (e.g. something like `no_init double x[100];`). Instead, we have the bug-prone default and twenty different ways to initialise something, each with their own caveats. C++ would be a much better language if most if not all defaults were reversed.
- simonask 10mo agoEvery C++ compiler is perfectly able to optimize stale writes, so I'm always skeptical of code that leaves uninitialized fields around. I would always strongly prefer rearranging the code to be easier on the optimizer.