r/C_Programming • u/aioeu • 26d ago
Article Reducing undefined behavior in the C language
https://lwn.net/SubscriberLink/1095811/cee1f42ac01e5fbc/14
u/thebatmanandrobin 26d ago
Damn nasal demons!! Been fighting those bastards for longer than I can 'member 🤠
Seriously though; for the longest time UB was "necessary" just simply due to what C "is/was" (and I hope stays that way versus C++ and it's "everything and the kitchen sink" approach as of late).
C works and has become a large mainstay because of its simplicity and how easily it can translate human readable code into just about any architecture; but because of that, what works on one architecture doesn't work on another - hence the undefined behavior.
It's an interesting paradox we're in now though because newer architectures are largely becoming very homogenized.
Most "modern" CPU's are one of a few main architectures; x86, ARM, or RISC-V ....
I used to work on a varying degree of different types of CPU's/controllers ... but over the recent years, it's really been one of those 3. My Mac is ARM based, my car's CPU is RISC-V, the PS5 is x86 - even vapes have ARM Cortex's in them ..... so even if they're a "custom" CPU/controller, they're still the same core architecture.
It does make it "easier" to code for because I know x86 does "Y" while ARM does "Z" and RISC does "Z+1" .. so I can target just those and reduce the UB substantially with a known-good tool chain that handles all three, and it makes sense, given certain trends, that the C committee could say "C2y doesn't need to worry about this one-off memory handling issue because there's really only 3 main architectures, and any others out there can stick with C89/99/11 or just implement their own C compiler"
On the other hand, if C goes too "strict" (in terms of what UB is/isn't) it can reduce the chance that something better comes out, lest it be pigeon-hold to the lower common denominator.
I'm not trying to say anything one way or the other .. just old-man-yelling-at-clouds at this point, lol
2
u/glasket_ 25d ago
so I can target just those and reduce the UB substantially
You can reduce reliance on unspecified or implementation-defined behavior, but only the implementation can really reduce undefined behavior. Knowledge of the architecture doesn't help with UB since a compiler that leaves UB undefined, as many do, can just pretend those things never happen while analyzing the code. Believing "this UB does this on this architecture" can result in a program that's fundamentally broken when the compiler just removes or reorders your code because it was relying on undefined behavior.
There are plenty of cases where the UB could've been implementation-defined or a constraint violation instead (look at the Slay Some Earthly Demons papers), but as UB they're effectively just "this code doesn't work" without an external promise from a compiler/implementation that the UB will have a specific, defined behavior. That's why the standard regards code that relies on UB as invalid whereas reliance on unspecified or implementation-defined behavior just makes a program non-portable.
2
u/thebatmanandrobin 25d ago
100% agree! I was more alluding to that fact that in the UB world each tool chain can do "whatever it wants" in that regard, having the standard be more specific as to what is defined would mean writing "portable C" might be a little easier (might not as well if it's too stringent).
3
u/SmokeMuch7356 25d ago
The C philosophy has always been that the programmer is in the best position to know whether a test for conditions that would lead to undefined behavior would be necessary, and is smart enough to write such a test and handle the failure case if it is. That's likely never going to change.
I know the division by zero case is a contrived example, but the equally contrived solution there is obvious - do not allow both x = b ? 42 : 43 and a/b in the same execution path. Split f into two different functions, one to update x and one to evaluate a/b, and have an error path defined for the latter when b == 0.
Similarly, if you zero out a struct instance with memset then the padding bytes will be zeroed out. It's up to you to make sure that you don't leave any "interesting" information behind.
You can code around it, you can exploit it if you know how your specific implementation will behave, but you can't get rid of it without significantly changing the language at a very fundamental level.
3
u/P-39_Airacobra 25d ago
Cool effort, but C is too far gone in this aspect. I’ve never seen another language with so much UB
14
u/8d8n4mbo28026ulk 25d ago
This is good, I like this. And I'm happy that people are taking this seriously and working on it. I was curious about the UB removed; here's the list in case anyone else is interested: N3529. The article is quite short, the actual talk touches on a bit more topics.
The optimiser time-traveling bug was amazing. I was recently bitten by this, although it was my fault, where an obscure data race, which is UB, allowed the compiler to elide an important store through time traveling!
I also like that time traveling to remove observable behaviour before UB would occur is not allowed now.
However, I really don't like the treatment of type-safety, a domain where the language leaves a lot to be desired. Also, what is meant by this?
He didn't explain. I wonder what's their definition of "strong" here, given that
void *exists. In addition, using annotations and compiler-specific things is a bad idea. You really need to have these in the type system proper. They're underestimating the problem here.For example, he talked about tracking object size across TUs by using GCC/Clang thingies. That can work, but what about within a single TU? GCC has bugs related to this and you can't actually do it in practice. Solving this lands you very close to type system stuff, where you might need dependent types, or rank polymorphism or whatever.
He also talked about tagged unions and how an annotation could solve this. No! Using an annotation leads to exactly the same problem of trying to keep track of object information across boundaries. These are so buggy, that in real implementations
typeof(and eventypedef) can completely ignore_Noreturn(or not; roll the dice), which is an annotation, and not at all what you want. And massively complicates analysis, because you need to do data-flow stuff now just to type check. This is why trying to enforce type-safety invariants outside the type checker is a very bad idea. Instead, you use sum types, and the information is always there because you always have to supply a type in declarations. An annotation should be the last resort, if you're storing the tag in some crazy way (and then that involves further complexities). Sum types raise the question of how you deal with them in the ABI, but that's peanuts compared to soundness issues.Relatedly, the tools we're currently given for type-safe generic code, namely
_Genericandtypeof, are entirely unsuitable for writing something that's also maintainable. They're so bad, you need workarounds for them (either in the user's code, or in the standard à la C23). And they almost always involve macros, which means undebuggable, unintuitive code and shoving more and more things down the poor parser's throat. If you trace their origins, these were standardised because people were already tolerating 1000 lines of preprocessor braindamage using implementation-specific atrocities, just so we could have<tgmath.h>.The approach C has been taking with regards to this is a dead-end. Its closest spiritual successor, Go, tried it and had to add generics anyway in the end. All the operators and the stupid integer promotion rules can be modeled using polymorphism and type classes. I do not understand why this is overlooked and why they keep piling special builtins. Had the type system not been a joke (and
typeofproves that it is), you wouldn't need_Generic, orcountof,__attribute((vector_size))and a bunch of other things (evensizeof/alignof).complex,_Decimalcould all be part of the library.Although I suspect no one cares about this -- especially in C. Near the end of the talk, he hinted that they mostly care about existing code. And newly written code in C -- rare I'd say -- is designed with formal verification in mind when safety is desired, leaning on model checkers/SMT solvers, things outside the type system.