8 ms·
Yes there is tons of surprising and weird UB in C, but this article doesn't do a great job of showcasing it. It barely scratches the surface. Here's a way weir
by muvlon 5mo ago
Yes there is tons of surprising and weird UB in C, but this article doesn't do a great job of showcasing it. It barely scratches the surface.
Here's a way weirder example:
volatile int x = 5;
printf("%d in hex is 0x%x.\n", x, x);
This is totally fine if x is just an int, but the volatile makes it UB. Why? 5.1.2.4.1 says any volatile access - including just reading it - is a side effect. 6.5.1.2 says that unsequenced side effects on the same scalar object (in this case, x) are UB. 6.5.3.3.8 tells us that the evaluations of function arguments are indeterminately sequenced w.r.t. each other.
So in common parlance, a "data race" is any concurrent accesses to the same object from different threads, at least one of which is a write. In C, we can have a data race on a single thread and without any writes!
- simonask 5mo agoI think the article's point is that you don't actually have to get weird at all to run into UB. Lots of people mistakenly think that C and C++ are "really flexible" because they let you do "what you want". The truth of the matter is that almost every fancy, powerful thing you think you can do is an absolute minefield of UB.
- 3form 5mo agoAt which point it feels like some sort of high-level assembly-like language, which is simple enough to compile efficiently and stay crossplatform, with some primitives for calls, jumps, etc. could find a nice niche. Maybe this already exists, even? A stripped down version of C? A more advanced LLVM IR? I feel like this is a problem that could use a resolution, just maybe not with enough of a scale for anyone to bother, vs. learning C, assembly of given architecture, or one of the new and fancy compiled languages.
- simonask 5mo agoWell, Zig is aiming to be a "saner C", and mostly succeeding so far. I hope they make it to production. Rust is a somewhat more thorough attempt to actually course-correct.
- pjmlp 5mo agoIt is basically what you can have today with Object Pascal or Modula-2, with a revamped syntax for C crowds.
- addaon 5mo agoThere's Vale [0] as a structured high-level assembly language, but pretty far from usable right now. I do hope it matures. Basically: All non-control-flow instructions can be directly supported. Control flow is lofted to a higher level and implemented in C-style structured blocks and keywords, which map directly to a subset of the ISA that modifies the program counter. This separation means it's not a proper superset of traditional assembly languages -- you can't paste in arbitrary blocks of existing code -- but a lot of interesting things (for them, implementations of cryptographic primitives) are pretty trivial to port over. And in exchange, you get a well defined Hoare logic that can talk about total correctness, not just [1]'s partial correctness. [0] https://github.com/project-everest/vale https://github.com/project-everest/vale [1] https://nickbenton.name/coqasm.pdf https://nickbenton.name/coqasm.pdf
- pjmlp 5mo agoYes, there have been quite a few C inspired Assembly languages for DSPs for example, TI had one.
- jstimpfle 5mo agoI would agree that C is "really flexible", but I would say it's primarily flexible because it lets you cast say from a void pointer to a typed pointer without requiring much boilerplate. It's also flexible because it lets you control memory layout and resource management patterns quite closely. If you want to be standards correct, yes you have to know the standard well. True. And you can always slip, and learn another gotcha. Also true. But it's still extremely flexible.
- simonask 5mo agoIt's not flexible in practice, because knowing the standard isn't optional. If you make the choice to not follow the standard, you're making the choice to write fundamentally broken software. Sometimes with catastrophic consequences.
- jstimpfle 5mo agoI'm making the choice to pass pointers as void to get low-friction polymorphism. I'm making the choice to control the memory layout of my data structures, including of levels and type of indirection. I'm making the choice to control my own memory allocators and closely control lifetimes, closely control (almost) everything that happens in the system. That has nothing to do with not following the standard.
- kzrdude 5mo agoMy go-to example of "UB is everywhere" is this one: int increment(int x) { return x + 1; } Which is UB for certain values of x.
- CodeArtisan 5mo agoC23 removed the whole stuff about indeterminate value and trap representation. Underflow/overflow being silent or not is implementation defined.
- saagarjha 5mo agoSigned overflow is just undefined.
- jstimpfle 5mo agoTBF that is the same as saying "signed overflow is UB".
- kzrdude 5mo agoyes but it is a 'picture' that makes you think about it in a different way.
- saghm 5mo agoI've long said that the value a programming language offers is as much about what it doesn't allow as what it does allow. Efficiency aside, most useful programs could be written in most languages, but there are an infinite number of programs you could write that aren't particularly useful. Ruling out the programs you might accidentally write that resemble the one you intended is a pretty useful feature of a language, and it's a metric that C and C++ rate quite poorly on IMO.
- sethev 5mo agoYes, there is a data race there. The value of a volatile can be changed by something outside the current thread. That’s what volatile means and why it exists. Edit: thread=thread of execution. I’m not making a point about thread safety within a program.
- mananaysiempre 5mo agoNot from the standard’s point of view. The traditional (in some circles) use of volatile for atomic variables was not sanctioned by the C11/C++11 thread model; if you want an atomic, write atomic, not volatile, or be aware of your dependency on a compiler (like MSVC) that explicitly amends the language definition so as to allow cross-thread access to volatile variables.
- jstimpfle 5mo agoNot sure why you're being downvoted. That's completely right. The example is silly. The code is obviously bad, doesn't matter if it's UB or not. I'm also not convinced (yet) that the example really is UB: I agree reading a volatile is "a side effect" in some sense, and GP cited a paragraph that says just that. But GP doesn't clearly quote that it's a side effect on the object (or how a side effect on an object is defined). Reading an object doesn't mutate it after all. But whatever language lawyer things, the code is obviously broken, with an obvious fix, so I'm not so interested in what its semantics should be. Here is the fix: volatile int x; // ... int val = x; // volatile read printf("%x %d\n", val, val);
- mananaysiempre 5mo agoAnd it makes sense as long as you allow the concept of unsequenced operations at all (admittedly it’s somewhat rare; e.g. in Scheme such things are defined to still occur in sequence, but which specific sequence is unspecified and potentially different each time). The “volatile” annotation marks your variable as being an MMIO register or something of that nature, something that could change at any point for reasons outside of the compiler’s control. Naturally, this means all of the hazards of concurrent modification are potentially there. That said, your “common parlance” definition of “data race” is not the definition used by the C standard, so your last sentence is at best misleading in a discussion of standard C. > The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior. (Here “conflicting” and “happens before” are defined in the preceding text.)
- tsimionescu 5mo agoYour first paragraph makes it sound as if the compiler will actually generate two reads of the value of some register, which might lead to unexpected effects at runtime for certain special registers. However, this is not at all what UB means in C (or C++). The compiler is free to optimize away the entire block of code where this printf() sequence occurs, by the logic that it would be UB if the program were to ever reach it. For example, the following program: int y = rand(); if (y != 8) { volatile int x; printf("%d: %d", x, x) ; } else { printf("y is 8"); } Can be optimized to always print "y is 8" by a perfectly standard compliant compiler.
- shakna 5mo ago"volatile" tells the compiler it is _not_ safe to optimise away any read or write, so it can't just optimise that section away at all. > An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously. A compliant compiler is only free to optimise away, where it can determine there are no side-effects. But volatile in 5.1.2.3 has: > Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects.
- berti 5mo agoReading a register from a microcontroller peripheral may well reset it as an example of a possible side-effect here, and that's exactly the kind of thing you use volatile for.
- RobotToaster 5mo agoWith volatile it could be changed by an interrupt service routine between reads, so it makes sense.
- nomel 5mo agoOr, it could be hardware that has a "clear flag on read" type behavior.
- tialaramex 5mo agoVolatile is a type system hack. They should have done a more principled fix, and certainly modern languages should not act as though "C did it" makes it a good idea. The reason for the hack is that very early C compilers just always spill, so you can write MMIO driver code by setting a pointer to point at the MMIO hardware and it actually works because every time you change x the CPU instruction performs a memory write. Once C compilers got some basic optimisations that obvious "clever" trick stops working because the compiler can see that we're just modifying x over, and over and over, and so it doesn't spill x from a register and the driver doesn't work properly. C's "volatile" keyword is a hack saying "OK compiler, forget that optimisation" which was presumably a few minutes work to implement, whereas the correct fix, providing MMIO intrinsics in the associated library, was a lot of work. Why should you want intrinsics here? Intrinsics let you actually spell out what's possible and what isn't. On some targets we can actually do a 1-byte 2-byte and 4-byte write, those are distinct operations and the hardware knows, so e.g. maybe some device expects a 4-byte RGBA write and so if you emit four 1-byte writes that's very confusing and maybe it doesn't work, don't do that. On some targets bit-level writes are available, you can say OK, MMIO write to bit 4 of address 0x1234 and it will write a single bit. If you only have volatile there's no way to know what happens or what it means.
- saagarjha 5mo ago> The reason for the hack is that very early C compilers just always spill, so you can write MMIO driver code by setting a pointer to point at the MMIO hardware and it actually works because every time you change x the CPU instruction performs a memory write. Source?
- skillina 5mo agoSource for what? The volatile keyword is explicitly telling the compiler "don't optimize read/write to this memory location". That's the whole point. Its use for manipulating hardware registers is covered in any intro embedded systems course. I don't know the history of C compilers but it would seem reasonable to assume that compilers started out plainly translating the C to machine code. Optimization would have happened later as the compilers became more mature. https://www.gnu.org/software/c-intro-and-ref/manual/html_node/volatile.html https://www.gnu.org/software/c-intro-and-ref/manual/html_nod...
- thomashabets2 5mo agoAuthor here. > It barely scratches the surface. I agree. The point of the post is not to enumerate and explain the implications of all 283 uses of the word "undefined" in the standard. Nor enumerate all the things that are undefined by omission. The point of the post is to say it's not possible to avoid them. Or at least, no human since the invention of C in 1972 has. And if it's not succeeded for 54 years, "try harder", or "just never make a mistake", is at least not the solution. The (one!) exploitable flaw found by Mythos in OpenBSD was an impressive endorsement of the OpenBSD developers, and yet as the post says, I pointed it at the simplest of their code and found a heap of UB. Now, is it exploitable that `find` also reads the uninitialized auto variable `status` (UB) from a `waitpid(&status)` before checking if `waitpid()` returned error? (not reported) I can't imagine an architecture or compiler where it would be, no. FTA: > The following is not an attempt at enumerating all the UB in the world. It’s merely making the case that UB is everywhere, and if nobody can do it right, how is it even fair to blame the programmer? My point is that ALL nontrivial C and C++ code has UB.
- muvlon 5mo agoFair enough! > And if it's not succeeded for 54 years, "try harder", or "just never make a mistake", is at least not the solution. And I 100% agree. UB is way overused by these standards for how dangerous it is, and as a consequence using C (and C++) for anything nontrivial amounts to navigating a minefield.
- rramadass 5mo agoThis has got nothing to do with data races etc. but everything to do with "Sequence Points and Single Update Rule" which is well described in C language specification. See my comment here - https://news.ycombinator.com/item?id=48205760 https://news.ycombinator.com/item?id=48205760
- deleted 5mo ago[deleted]
- imtringued 5mo agoMemory mapped IO sends a read request to a peripheral which is allowed have side effects in the background and return two different values upon a read. You can think of it as a synchronous RPC request. The lack of argument sequencing feels utterly petty however.
- HarHarVeryFunny 5mo ago> In C, we can have a data race on a single thread and without any writes! Well, sure, that's what volatile means - that the value may be changed by something else. If it's a global variable then the something else might be an interrupt or signal handler, not just another thread. If it's a pointer to something (i.e. read from a specific address) then that could be a hardware device register who's value is changing. The concept of a volatile variable isn't the problem - any language that is going to support writing interrupt routines and memory mapped I/O needs to have some way of telling the compiler "don't optimize this out" since reading from the same hardware device register twice isn't like reading from the same memory location twice. I think the problem here is more that not all of the interactions between language features and restrictions have been fully thought out. It's pretty stupid to be able to explicity tell the language "this value can change at any time", and for it to still consider certain uses of that value as UB since it can change at any time! There should have been a carve out in the "unsequenced side effect" definitions for volatile variables.
- vlovich123 5mo ago> There should have been a carve out in the "unsequenced side effect" definitions for volatile variables. As noted, there’s almost 300 usages of the word undefined in the standard. Believing that it’s possible to correctly define all the carve outs necessary correctly and have the compiler implement the carve outs successfully is about as logical as believing UB is humanly avoidable in written code.
- pron 5mo ago> In C, we can have a data race on a single thread and without any writes! You need to distinguish between a UB and a race, and I think that's something that discussions of UB miss. Take any C program and compile it. Then disassemble it. You end up with an Assembly program that doesn't have any UB, because Assembly doesn't have UB. UB is a property of a source program, not the executable. It means that the spec for the language in which the source is written doesn't assign it any meaning. But the executable that's the result of compiling the program does have a meaning assigned to it by the machine's spec, as machine code doesn't have UB. A race is a property of the behaviour of a program. So it's true to say that your C program has UB, but the executable won't actually have a race. Of course, a C compiler can compile a program with UB in any way it likes so it's possible it will introduce a race, but if it chooses to compile the program in a way that doesn't introduces another thread, then there won't be a race.
- redox99 5mo ago> because Assembly doesn't have UB To be pedantic, old hardware like 6502 family chips (Commodore 64, Apple II, etc) had illegal instructions which were often used by programmers, but it was completely up to the chip to do whatever it wanted with those like with UB.
- zahlman 5mo ago> illegal instructions... were often used by programmers Intentionally, with an expected effect? I'd need a citation for that.
- chuckadams 5mo agoSome desultory googling turned up: * https://www.nesdev.org/wiki/CPU_unofficial_opcodes#Games_using_unofficial_opcodes https://www.nesdev.org/wiki/CPU_unofficial_opcodes#Games_usi... * https://hitmen.c02.at/files/docs/c64/NoMoreSecrets-NMOS6510UnintendedOpcodes-20162412.pdf https://hitmen.c02.at/files/docs/c64/NoMoreSecrets-NMOS6510U... (doesn't name any software, but some copy protection schemes were already known to use them)
- 5mo ago
- rocketrascal 5mo agoAre you sure? >unsequenced side effects on the same scalar object are UB >6.5.3.3.8 tells us that the evaluations of function arguments are indeterminately sequenced w.r.t. each other. Read 5.1.2.4.3: "If A is not sequenced before or after B, then A and B are unsequenced." "Evaluations A and B are indeterminately sequenced when A is sequenced either before or after B, but it is unspecified which." With a footnote saying this: "9)The executions of unsequenced evaluations can interleave. Indeterminately sequenced evaluations cannot interleave, but can be executed in any order." I.e the standard makes a distinction between "unsequenced" and "indeterminately sequenced". And with no mention of side effects on "indeterminately sequenced" being UB it leads me to conclude that your example is not UB.
- zahlman 5mo ago> Here's a way weirder example: Well, yes; but when the C standard authors wrote like this, they surely had in mind "the reads could be in either order, therefore the output could display the polled values in either order". Not C++ nasal demons. And yeah, being able to say "reading is a side effect" is important when for example you interact with certain memory-mapped devices.
- netrikare 5mo ago[dead]
- smsm42 5mo agoI think C standard doesn't do itself any favors by using "undefined behavior" to signify both "anything can happen, including erasing all your data and setting your data center on fire" and "one of the very small and well defined set of things would happen, but we can not commit to which one". The latter is not exactly great, but significantly less dangerous than the former.
- drysine 5mo agoWhat's weird about it? If you are using volatile you are reading from a device port mapped to that address. Since C doesn't mandate in which order function arguments are evaluated, you don't know which argument will be read from port first. How can that be anything but UB?