Remix.run Logo
Everything in C is undefined behavior(blog.habets.se)
178 points by lycopodiopsida 4 hours ago | 199 comments
y42 a minute ago | parent | next [-]

shameless plug, it's part of the Nerd Encyclopedia: it's also called "nasal demons".

https://nickyreinert.de/2023/2023-05-16-nerd-enzyklop%C3%A4d...

muvlon 2 hours ago | parent | prev | next [-]

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 an hour ago | parent | next [-]

I 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.

sethev 29 minutes ago | parent | prev [-]

Yes, 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.

beeforpork 2 hours ago | parent | prev | next [-]

The UB in unaligned pointers is even worse: an unaligned pointer in itself is UB, not only an access to it. So even implicit casting a void*v to an int*i (like 'i=v' in C or 'f(v)' when f() accepts an int*) is UB if the cast pointer is not aligned to int.

It is important to understand that this is a C level problem: if you have UB in your C program, then your C program is broken, i.e., it is formally invalid and wrong, because it is against the C language spec. UB is not on the HW, it has nothing to do with crashes or faults. That cast from void* to int* most likely corresponds to no code on the HW at all -- types are in C only, not on the HW, so a cast is a reinterpretation at C level -- and no HW will crash on that cast (because there is not even code for it). You may think that an integer value in a register must be fine, right? No, because it's not about pointers actually being integers in registers on your HW, but your C program is broken by definition if the cast pointer is unaligned.

thomashabets2 an hour ago | parent | next [-]

Author here.

> an unaligned pointer in itself is UB

Yup. Per the "Actually, it was UB even before that" section in the post.

> UB is not on the HW, it has nothing to do with crashes or faults

Yeah. I tried to convey this too, but I'm also addressing the people who say "but it's demonstrably fine", by giving examples. Because it's not.

account42 an hour ago | parent | prev | next [-]

Which is totally fine and expected for any decent programmer. Casting pointers is clearly here be dragons territory.

simonask an hour ago | parent [-]

Many, many programmers come to C (and C++) with a lower-level understanding that actually gets in the way here. They understand that all types "are" just bytes and that all pointers "are" just register-sized integer addresses, because that's how the hardware works and has worked for decades.

It's perfectly reasonable to expect any load through `int*` to just load 4 bytes from memory, done and done. They get surprised that it is far from the whole story, and the result is UB.

Meanwhile, the actual computers we have been using for decades have no problems actually just loading 4 bytes through any arbitrary pointer with zero overhead. But no.

lelanthran an hour ago | parent | next [-]

> They understand that all types "are" just bytes and that all pointers "are" just register-sized integer addresses, because that's how the hardware works and has worked for decades.

I'd clarify this with "They understand that all values are just bytes".

> Meanwhile, the actual computers we have been using for decades have no problems actually just loading 4 bytes through any arbitrary pointer with zero overhead.

It's partly the standards fault here - rather than saying "We don't know how vendors will implement this, so we shall leave it as implementation-defined", they say "We don't know how vendors will implement this, so we will leave it as undefined".

A clear majority of the UB problems with C could be fixed if the standards committee slowly moved all UB into IB. It's not that there isn't any progress (Signed twos-complement is coming, after all), it's that there is (I believe) much pushback from compiler authors (who dominate the standards) who don't want to make UB into IB.

benj111 5 minutes ago | parent [-]

>It's partly the standards fault here - rather than saying "We don't know how vendors will implement this, so we shall leave it as implementation-defined", they say "We don't know how vendors will implement this, so we will leave it as undefined

I'd agree to a point. I still think it's unreasonable for compiler writers to get all lawyery about precise terminology. After all "implementation defined" could still be subject to the same lawyeriness (we implemented it, ergo we define it).

To me this is an issue of culture. We need to push back against the view that UB means anything can happen, therefore the compiler can do anything.

pjc50 an hour ago | parent | prev [-]

Except ARM32. ARM64 doesn't guarantee it to be valid in all cases either.

tovej an hour ago | parent | prev [-]

But that seems obvious. You can't load an integer from an unaligned address.

It's not only C-level is it. There's no (guarantee across architectures for) machine code for that either.

codeflo an hour ago | parent | next [-]

> You can't load an integer from an unaligned address.

You can, and the results are machine specific, clearly defined and well-documented. Ancient ARM raises an exception, modern ARM and x86 can do it with a performance penalty. It's only the C or C++ layer that is allowed to translate the code into arbitrary garbage, not the CPU.

matheusmoreira an hour ago | parent | prev | next [-]

Sure you can. In many architectures it works just fine. Works perfectly in x86_64, for example. It's just a little slower.

tovej 30 minutes ago | parent [-]

In many architectures does not mean you can. The standard is supposed to cover all architectures.

mbel an hour ago | parent | prev | next [-]

Unless your code targets some exotic architecture, like idk x86.

pjc50 an hour ago | parent | prev [-]

You missed the point: the pointer existing as a value of that type at all is UB, even if you never try to access anything through it and no corresponding machine code is ever emitted.

tovej 23 minutes ago | parent [-]

Yes? I agree with that. I don't really see the issue there. The computer will allocate data in aligned addresses, so you would have to be doing something weird to begin with to access unaligned pointers. And aligned access is always better anyway. I guess packed structs are a thing if you're really byte golfing. Maybe compressed network data would also make sense.

But then I would assume you are aware of unaligned pointers, and have a sane way to parse that data, rather than read individual parts of it from a raw pointer.

I am curious, what would be a legitimate reason for an unaligned pointer to int?

greysphere 3 hours ago | parent | prev | next [-]

The examples aren't really undefined behavior. They are examples that could become UB based on input/circumstances. Which if you are going to be that generous, every function call is UB because it could exceed stack space. Which is basically true in any language (up to the equivalent def of UB in that language). I feel like c has enough actual rough edges that deserve attention that sensationalism like this muddies folks attention (particularly novices) and can end up doing more harm than good.

guerby 2 hours ago | parent | next [-]

Ada 83 has no UB on call stack overflow, from the reference manual :

http://archive.adaic.com/standards/83lrm/html/lrm-11-01.html

"STORAGE_ERROR This exception is raised in any of the following situations: (...) or during the execution of a subprogram call, if storage is not sufficient."

veltas 2 hours ago | parent [-]

So it's just as useful as when your stack area ends with a page that will segfault on access, or your CPU will raise an interrupt if stack pointer goes beyond a particular address?

It's not safe though because throwing an exception, panicking, etc, is still a denial of service. It's just more deterministic than silently overwriting the heap instead. If the program is critical then you need to be able to statically prove the full size of the stack, which you can do with C and C++ with the right tools and restrictions.

simonask an hour ago | parent [-]

Deterministic, well-defined behavior is inherently safer than undefined behavior. It allows you to diagnose the problem and fix it. UB emphatically does not, and I don't dare to think of how many millions of person-hours are wasted every year dealing with the results.

eru 2 hours ago | parent | prev | next [-]

That's not true at all.

First, you can define what happens when stack space is exceeded. Second not all programs need an arbitrary amount of stack space, some only need a constant amount that can be calculated ahead of time. (And some languages don't use a stack at all in their implementations.)

Your language could also offer tools to probe how much stack space you have left, and make guarantees based on that. Or they could let you install some handlers for what to do when you run out of stack space.

pjc50 2 hours ago | parent | prev | next [-]

UB based on input can be an exploit vector.

layer8 2 hours ago | parent [-]

Unvalidated input can always be an exploit vector.

Ygg2 2 hours ago | parent [-]

Except in C, validation of user input can in itself be an exploit vector.

layer8 2 hours ago | parent | next [-]

That’s true in other languages as well. Any programmatic task can end up being an exploit vector.

pjc50 an hour ago | parent [-]

No? That's the whole point of formal verification?

You can even kind of retrofit this to C. The classic example is "sel4". You just need a set of proofs that the code doesn't trigger UB. This ends up being much larger and more complicated than the C itself.

greybeard69 2 hours ago | parent | prev [-]

Turtles all the way down.

stevenhuang 2 hours ago | parent | prev | next [-]

The examples are unequivocally UB. Full stop.

How to think of this properly is that when you have UB, you are no longer under the auspices of a language standard. Things may work fine for a time, indefinitely even. But what happens instead is you unknowingly become subject to whimsies of your toolchain (swap/upgrade compilers), architecture, or runtime (libc version differences).

You end up building a foundation on quicksand. That's the danger of UB.

flohofwoe 2 hours ago | parent [-]

> The examples are unequivocally UB. Full stop.

Tbh, already the first example (unaligned pointer access) is bogus and the C standard should be fixed (in the end the list of UB in the C standard is entirely "made up" and should be adapted to modern hardware, a lot of UB was important 30 years ago to allow optimizations on ancient CPUs, but a lot of those hardware restrictions are long gone).

In the end it's the CPU and not the compiler which decides whether an unaligned access is a problem or not. On most modern CPUs unaligned load/stores are no problem at all (not even a performance penalty unless you straddle a cache line). There's no point in restricting the entire C standard because of the behaviour of a few esoteric CPUs that are stuck in the past.

PS: we also need to stop with the "what if there is a CPU that..." discussions. The C standard should follow the current hardware, and not care about 40 year old CPUs or theoretical future CPU architectures. If esoteric CPUs need to be supported, compilers can do that with non-standard extensions.

account42 an hour ago | parent | next [-]

Not having unaligned access in the language allows the compiler to assume that, for basic types where the aligment is at least the size, if two addresses are different then they don't alias and writes to one can't change the result of reads from the other. That's a very useful assumption to be able to make for optimization - much more useful than yolocasting pointers in a way that could get you unaligned ones.

leni536 an hour ago | parent | prev | next [-]

Undefined means that the ISO C doesn't define the behavior. An implementation is free to do so.

simonask an hour ago | parent [-]

If they do, that is no longer an implementation of C. It is a dialect of C, and there are many (GNU C being the most popular), but there are real drawbacks to using dialects.

This is in contrast to the other category that exists, which is "implementation-defined".

1718627440 8 minutes ago | parent [-]

> If they do, that is no longer an implementation of C.

This is plain wrong. Undefined behaviour, means the C standard specifies no restriction on the behaviour of the program, which is what the implementation chooses to emit. An implementation can very well choose to emit any program it pleases, including programs that encrypt your harddisk, but also programs that stick to well defined rules.

stevenhuang 2 hours ago | parent | prev | next [-]

I agree. I meant to elaborate more on how to think of UB.

For most C software on x86_64, UB is "fine" with very strong bunny ears. But it is preferable for one to, shall we say, write UB intentionally rather than accidentally and unknowingly. Having an awareness of all the minefields lends for more respect for the dangers of C code, it makes one question literally everything, and that would hopefully result in more correct code, more often.

On that note, on some RISC-V cores unaligned access can turn a single load into hundreds of instructions.

I think the problem is just that C is under specified for what we expect a language to provide in the modern age. It is still a great language, but the edges are sharp.

IshKebab 2 hours ago | parent | prev [-]

There are still modern CPUs that don't support misaligned access. It would be insane for C to mandate that misaligned accesses are supported.

However I do agree that just saying "the behaviour is undefined" is an unhelpful cop-out. They could easily say something like "non-atomic misaligned accesses either succeed or trap" or something like that.

> In the end it's the CPU and not the compiler which decides whether an unaligned access is a problem or not.

Not just the CPU - memory decides as well. MMIO devices often don't support misaligned accesses.

1718627440 7 minutes ago | parent | next [-]

> They could easily say something like "non-atomic misaligned accesses either succeed or trap" or something like that.

That means that the compiler must emit the read, even if the value is already known or never used, as it might trap. There is a reason for the UB!

thayne 2 hours ago | parent | prev | next [-]

On hardware that doesn't support it, misaligned loads could be compiled to multiple loads and shifts. Probably not great for performance, and it doesn't work if you need it to be atomic, but it isn't impossible.

gizmo686 2 hours ago | parent [-]

That still requires detecting when a misaligned load happens.

account42 an hour ago | parent | prev [-]

For x86 SSE there are aligned instructions that will trap on unaligned access.

account42 an hour ago | parent | prev [-]

Yes, this article is pretty much the definition of FUD.

parasti an hour ago | parent | prev | next [-]

I have never in my 20 years of writing C heard so much about undefined behavior as I have in the past 6 months on Hacker News. It has never entered the conversation. You write the code. If it doesn't work, you debug it and apply a fix or a workaround. Why does the idea of undefined behavior in C get to the front page so consistently?

simonask 11 minutes ago | parent | next [-]

Excuse me, what? I was writing both C and C++ 20 years ago, and UB was a huge part of the conversation (and the curriculum) back then as well.

There were a few high-profile "scandals" around GCC 3.2 (IIRC) because the compiler finally started much more aggressively using UB in optimizations, which was a reason that lots of people stayed on GCC 2.95 for a very long time. GCC 3.2 came out in 2002.

keyle 21 minutes ago | parent | prev | next [-]

Computers used to be cool; now they're dangerous.

Every company keep harping on about safety and being exposed (being in the news): so the narrative against 'unsafe' is up the wazoo.

The new world is basically a bunch of city dwellers who haven't seen raw nature and you show them a lawn mower, they freak out. Blades that spin?!?!?! Madness!!

pjc50 5 minutes ago | parent [-]

If everything is going to be dependent on computers, it's probably important that they work and remain under their owner's control rather than whichever NK or Chinese hacker group gets to them first.

Can't talk about C without CVE.

keyle a few seconds ago | parent [-]

Yeah, npm, all the yaml state machines, & now MCP --yolo gemini entered the chat.

If you think C is the problem, you'll come to the eventual conclusion that humans are the problems, and greed. Don't hate the player, hate the game etc.

Etheryte an hour ago | parent | prev | next [-]

Because the production environment might be a completely different architecture, these details matter a lot. Works on my machine is not useful if your actual target is a small embedded system on top of a cell tower in the middle of nowhere. Granted, most people don't work on stuff like that, I imagine the vast majority of devs here are web developers, but even still it's an interesting discussion even if you haven't run into it yourself. Maybe even more so in that case.

spacedcowboy 8 minutes ago | parent [-]

Um, as an embedded developer, you don't develop the code to run on your machine, you develop it to run on the same target as you expect to deploy to, sitting on your desk next to you.

I have lots of my code running day-in, day-out on literally hundreds of millions of machines. The approach to "getting it working" is exactly OP's.

I'll admit to being pretty defensive and anal in checking values and return-codes (more so than most, I suspect), and I'm a firm believer in KISS principles in software engineering ("solving hard problems with complicated code is easy, solving them with simple, understandable algorithms is the hard bit") but generally there's no real difference in approach to the code I write to work on my workstation, and the code I write to work in the field.

sethev 14 minutes ago | parent | prev | next [-]

I wonder if it’s just the colorful metaphors and an opportunity to bring out examples of surprising behavior. Plus it’s a topic that can always stir up debates.

jakobnissen 31 minutes ago | parent | prev | next [-]

I would guess that the continued success of Rust have shown that we don’t have to live with the user-hostility of C in order to write system programs. Therefore, people are understandably growing less and less patient with C and its unending bullshit.

Although I haven’t noticed a spike the last 6 months, just a slowly increasing realization that C isn’t fit for humans and should go the way of asbest: Don’t use it for anything new, and remove it where it already exists, unless doing so would be too expensive or disruptive.

account42 43 minutes ago | parent | prev [-]

There are a lot of Rust/whatever hipsters here that have defined their whole identity around hating C and C++.

hnarn 19 minutes ago | parent [-]

Ironically, by stereotyping ”Rust hipsters” you are painting yourself out as a stereotype as well. Knee-jerk comments like yours add nothing to the discussion. Rust exists for a reason, it solves real problems, but it’s not suitable for everything. These are indisputable facts and by discarding every mention of Rust as coming from ”hipsters” with no understanding, you are doing the exact same thing that you would accuse them of. ”Use Rust for everything” and ”Rust is useless for everything” are equally vapid and meaningless statements designed for nothing but trolling and showing ignorance.

quelsolaar 2 hours ago | parent | prev | next [-]

The 5 stages of learning about UB in C:

-Denial: "I know what signed overflow does on my machine."

-Anger: "This compiler is trash! why doesn't it just do what I say!?"

-Bargaining: "I'm submitting this proposal to wg14 to fix C..."

-Depression: "Can you rely on C code for anything?"

-Acceptance: "Just dont write UB."

1718627440 2 minutes ago | parent | next [-]

> -Denial: "I know what signed overflow does on my machine."

Or you just not skip the introductory pages, that tell you what the language philosophy of C is, and why there is UB. Yes, UB can be a struggle, but the first four steps are entirely unnecessary. It means that you do not actually understand the core concepts of the very same language you are using, which is kinda stupid.

matheusmoreira an hour ago | parent | prev | next [-]

What stage is the "just make the compiler define the undefined" stage?

Unaligned access? Packed structs. Compiler will magically generate the correct code, as if it had always known how to do it right all along! Because it has, in fact, always known how to do it right. It just didn't.

Strict aliasing? Union type punning. Literally documented to work in any compiler that matters, despite the holy C standard never saying so. Alternatively, just disable it straight up: -fno-strict-aliasing. Enjoy reinterpreting memory as you see fit. You might hit some sharp edges here and there but they sure as hell aren't gonna be coming from the compiler.

Overflow? Just make it defined: -fwrapv. Replace +, -, * with __builtin_*_overflow while you're at it, and you even get explicit error checking for free. Efficient too.

The "acceptance" stage is really "nobody sane actually cares about the C standard". The standard is garbage, only the compilers matter. And it turns out that compilers have plenty of extremely useful functions that let you side step most if not all of this. People just don't use this because they want to write "portable" "standard" C. The real acceptance is to break out of that mindset.

Somehow I built an entire lisp interpreter in freestanding C that actually managed to pass UBSan just by following the above logic. I was actually surprised at first: I expected it to crash and burn, but it didn't. So if I can do it, then anyone can do it too.

gpderetta an hour ago | parent | next [-]

> Unaligned access? Packed structs.

Packed structs are dangerous. You can do unaligned accesses through a packed type, but once you take the address of your misaligned int field, then you are back into UB territory. Very annoying in C++ when you try to pass the a misaligned field through what happens to be generic code that takes a const reference, as it will trigger a compiler warning. Unary operator+ is your friend.

matheusmoreira 5 minutes ago | parent [-]

> but once you take the address of your misaligned int field

Gotta work with the structure directly by taking the address of the packed structure itself.

  struct uu64 {
      u64 value;
  } __attribute__((packed));

  struct uu64 unaligned;
  struct uu64 *address = &unaligned;

  address->value; // this works

  u64 *broken = &address->value; // this doesn't
Taking the address of the field inside the structure essentially casts away the alignment information that was explicitly added to stop the compiler from screwing it up. So it should not be done.

Mercifully, both gcc and clang emit address-of-packed-member warnings if it's done. Turns silent nonsense code into sensible warnings. Major win.

lelanthran 41 minutes ago | parent | prev [-]

> What stage is the "just make the compiler define the undefined" stage?

It can be left as implementation defined, which means that the compiler can't simply do arbitrary things, it needs to document what it would do.

Take, for example, signed-integer overflow: currently a compiler can simply refuse to emit the code in one spot while emitting it in another spot in the same compilation unit! Making it IB means that the compiler vendor will be forced to define what happens when a signed-integer overflows, rather than just saying, as they do now, "you cannot do that, and if you do we can ignore it, correct it, replace it or simply travel back in time and corrupt your program".

> Somehow I built an entire lisp interpreter in freestanding C that actually managed to pass UBSan just by following the above logic. I was actually surprised at first: I expected it to crash and burn, but it didn't. So if I can do it, then anyone can do it too.

Same here; I built a few non-trivial things that passed the first attempt at tooling (valgrind, UBsan with tests, fuzzing, etc) with no UB issues found.

thomashabets2 an hour ago | parent | prev | next [-]

Author here.

> -Acceptance: "Just dont write UB."

The point of my article is that this is not possible. This cannot be our end state, as long as humans are the ones writing the code. No human can avoid writing UB in C/C++.

im3w1l 42 minutes ago | parent | prev | next [-]

In C, acceptance is "I will write UB and it will eventually lead to something bad happening"

Ygg2 2 hours ago | parent | prev [-]

> -Acceptance: "Just dont write UB."

Just switch to a saner language.

And before I get attacked for being a Rust shill, I meant Java :P

The bar is so low it's floating near the center of the Earth.

dns_snek 2 hours ago | parent | next [-]

> And before I get attacked for being a Rust shill, I meant Java :P

If all you want is C but less insane then the obvious answer here is Zig.

simonask an hour ago | parent | next [-]

Zig is cool, but it is not even close to being ready for prime-time. It will be pre-1.0 for a while, and major breaking changes are still happening.

dns_snek 41 minutes ago | parent [-]

Sure, maybe don't bet your entire company on mountains of Zig code just yet, but aside from the breaking changes it's been perfectly usable and suitable for every project I've ever wanted to work on.

psychoslave an hour ago | parent | prev [-]

If all somebody want is a programming language than C/C++ on these matter, there are plentiful options of the shelf to pick from.

If all somebody want is a turn key replacement to C/C++ ecosystem, then there is nothing like that in the world that I’m aware of.

ErroneousBosh an hour ago | parent | prev | next [-]

Okay, so Java compiles to machine code now?

Because the last time I looked it appeared to need some godawful slow bytecode interpreter that took up thousands of kilobytes of RAM.

elch an hour ago | parent | next [-]

If you don't like JIT/JVM there's GraalVM Native Image.

https://www.graalvm.org/latest/reference-manual/native-image...

In the past you could use e.g. Excelsior JET.

pjc50 an hour ago | parent | prev [-]

Java has been jitted for .. decades?

p2detar 2 hours ago | parent | prev [-]

> Just switch to a saner language.

And where's the fun in that?

psychoslave an hour ago | parent [-]

That’s a taste matter. Being recalled that what is expressed is always depending on some technical details on every move, this is great when one is loving technical details and have all the leisure time to pay attention to them. This is going to be hell compared to sound defaults for someone willing to focus on delivering higher order feature/functionality which will most likely work just fine.

Unedefined behaviour means "we couldn’t settle on a best default trade-off with fine-tuning as a given option so we let everyone in the unknown".

bestouff 3 hours ago | parent | prev | next [-]

The problem of UB is not really that it may crash in some architecture. The real problem is that the compiler expects UB code to NOT happen, so if you write UB code anyway the compiler (and especially the optimizer) is allowed to translate that to anything that's convenient for its happy path. And sometimes that "anything" can be really unexpected (like removing big chunks of code).

inkysigma 3 hours ago | parent | next [-]

One example along this path as an example is that every function must either terminate or have a side effect. I don't think one has bitten me yet but I could completely see how you accidentally write some kind of infinite loop or recursion and the function gets deleted. Also, bonus points for tail recursion so this bug might only show up with a higher optimization level if during debug nothing hit the infinite loop.

account42 an hour ago | parent [-]

Infinite loop without side effects == program stuck and not responding on user input and not outputting anything. That's not something a useful program will ever want to do.

Certhas 33 minutes ago | parent | next [-]

Not true, C++ made it so trivial infinite loops are not UB because it turns out they do have legitimate uses.

https://lists.isocpp.org/std-proposals/2020/05/1322.php

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p28...

account42 26 minutes ago | parent [-]

Yes, the C++ committee has been making some stupid decisions lately. This is not the only one.

Low level platform-specific code that needs to hot spin until an interrupt happens can use assembly for that part which it will need to do for the interrupt handler anyway.

zarzavat 30 minutes ago | parent | prev | next [-]

https://9p.io/sources/plan9/sys/src/libc/9sys/abort.c

account42 24 minutes ago | parent [-]

This is already UB without an infinite loop.

xigoi 35 minutes ago | parent | prev [-]

The problem is when you accidentally write an infinite loop. In a different language, you run the code, see that it gets stuck and fix it. In C, the compiler may delete the function, making it hard to realize what is happening.

account42 25 minutes ago | parent [-]

This is not a problem that C or C++ programmers actually encounter, ever.

eru 2 hours ago | parent | prev | next [-]

Yes, a crash is about the most benign UB: at least it's highly visible.

In worse scenarios, your programme will silently continue with garbage, or format your hard disk or give attackers the key to the kingdom.

rando1234 2 hours ago | parent | prev | next [-]

The point in the article that 'It's not about optimisations' really got my attention. I've previously done some work where we wrote an analysis pass under the assumption that it executed last in the transformation pipeline and this was needed for correctness. The assumption was that since no further optimisations happened it was safe. Now I'm not so sure...

account42 an hour ago | parent | prev | next [-]

That's a feature, not a problem.

anilakar 3 hours ago | parent | prev [-]

Removing code paths that the programmer has explicitly laid out in the source code should be made a hard compile error unless the operation has been tagged with an attribute (anyone who wants to add the unsafe keyword to C? ).

Another commenter suggested using LLMs, but I disagree. Having clangd emit warning squiggles for unchecked operations (like signed addition) would be a good start.

flohofwoe 3 hours ago | parent | next [-]

> Removing code paths that the programmer has explicitly laid out in the source code should be made a hard compile error unless the operation has been tagged with an attribute (anyone who wants to add the unsafe keyword to C? ).

Dead code elimination is essential for performance, especially when using templates (this is basically what enables the fabled "zero cost abstraction" because complex template code may generate a lot of 'inactive' code which needs to be removed by the optimizer).

The actual issue is that the compiler is free to eliminate code paths after UB, but that's also not trivial to fix (and some optimizations are actually enabled by manually injecting UB (like `__builtin_unreachable()` which can make a measurable difference in the right places).

amoss 3 hours ago | parent | prev | next [-]

Dead code elimination is run multiple times, including after other optimizations. So code that is not initially dead may become dead after propagating other information. Converting dead code into an error condition would make most generic code that is specialized for a particular context illegal.

gpderetta 37 minutes ago | parent | prev | next [-]

Consider:

   enum op_t{ add, mul };
   int exec(op_t op, int a, int b) {
       if(op == add) { return a+b; }
       if(op == mul) { return a\*b; }
   }

   c = exec(add, a,b);
Should be the compiler be prevented from inlining exec and constant-propagating op and removing the mul branch? What about if a and b are constants and the addition itself is optimized away?
4gotunameagain 3 hours ago | parent | prev [-]

This is trickier than it initially seems. Using preprocessor directives to include or exclude swaths of code is a very common thing, and implementing a compiler error as you described would break the building of countless C codebases.

debugnik 3 hours ago | parent | prev | next [-]

As much as I agree with the intro, these examples aren't good and the overall article is just a veil for pushing LLM coding.

gblargg 19 minutes ago | parent | next [-]

Agreed. One after another these are standard things you avoid when writing portable code (or don't need, like accessing the object at address 0). They come across like from someone who wants to write whatever they want and have it work the same on everything. To make it into a language that allows this would remove its advantage of being able to write to the platform when you want to.

boxed 3 hours ago | parent | prev [-]

Not good how? Are they TRUE? If so that's super bad.

IshKebab 2 hours ago | parent | next [-]

They are true but I agree it's not a great article. C has an unending list of UB and given the title I was expecting a more comprehensive survey, but they actually just picked a few that are both fairly well known and not very interesting.

thomashabets2 44 minutes ago | parent [-]

Author here.

As I stated:

> 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/C++ code has UB.

It's about that point, not about how to avoid it. Because you can't.

HelloNurse an hour ago | parent | prev [-]

Some of the examples are somewhat formally true in theory and bullshit in practice; some are quite hallucinatory.

  - Creating a potentially troublesome misaligned int pointer is a precisely localized and completely explicit user mistake, not something that just happens because it's C.
  - Passing signed char to character classification functions that expect an unsigned char (disguised as an int) is a very specific dumb user error. The C standard could specify that all negative inputs, including EOF and invalid signed char values, are classified as not belonging to the character class, but I doubt the current undefined behaviour in isxdigit() etc. implementations ever went beyond accepting invalid inputs.
  - Casting floating point values to integer values in general requires taking care of whether the FP values are small enough to be represented and what to do with NaN and Inf values: not the language's responsibility. C offers a toolbox of tests, not ready-made application specific error handling.
  - Expecting C to handle "address zero" in physical memory in ways that conflict with NULL in source code denotes a complete lack of understanding of what a program is. Where stuff in an executable is loaded in memory, in the rare cases when it matters, can surely be affected with platform specific extensions, possibly at the level of linker commands with nothing appearing in the C source code.
thomashabets2 an hour ago | parent [-]

Author here.

So I see your counter points are all "so just don't do that, then".

And the point of my post is that this particular "just don't do that, then" has never been achieved by humans.

If if there's no example of a program without these bugs in a language, then I do think it's fair to blame the language. A knife with 16 blades and no handle.

> Expecting C to handle "address zero" in physical memory in ways that conflict with NULL in source code denotes a complete lack of understanding of what a program is.

Like the post says, it's rare that programmers actually want a pointer to memory address zero. But in my experience most programmers who even encounter that have this "complete lack of understanding", as you put it.

HelloNurse 18 minutes ago | parent [-]

"Just don't do that" is the correct approach to errors, even when they are easy to overlook and the programming language provides many opportunities for mistakes.

For example, you seem to underestimate how wrong placing negative values in a signed char is: ordinary character encodings do not use negative codes, so either those negative values are not characters and they have no business being treated as such, or something strange and experimental is going on.

maple3142 an hour ago | parent | prev | next [-]

Is this a correct understanding of UB in C? A program P has a set of inputs A that do not trigger UB, and a complementary set of inputs B that do trigger UB. A correct compiler compiles P into an executable P'. For all inputs in A, P' should behave the same as P. However, for any input in B, the is absolutely no requirements on the behavior of P'.

simonask an hour ago | parent [-]

Intuitively yes - the program will be compiled as if B-inputs are never passed to the program, and that can include eliminating code that tries to detect B-inputs.

justmarc 6 minutes ago | parent | prev | next [-]

The art is actually making sure it all stays defined behavior

keyle 17 minutes ago | parent | prev | next [-]

When talking UB, putting C and C++ in the same basket is basically like comparing drunk driving a car and riding a bicycle sober... Both means of transport, very different experience.

mjs01 an hour ago | parent | prev | next [-]

Integer promotion seems to be the source of many signed integer overflow UB. Why does C have it? Does integer promotion ever have a good part?

rom1v an hour ago | parent | prev | next [-]

A concrete example of undefined behavior caused by an unaligned pointer: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...

codeflo an hour ago | parent | prev | next [-]

> The compiler, and really the underlying hardware too, is playing a game of telephone with your UB intentions.

The part about hardware is wrong BTW. In all the cases about null pointers and out-of-bounds access and integer overflow and whatnot, the hardware semantics are clearly defined, and the assembler code does exactly what is written. The way modern compilers act on your code makes C less safe than assembler in that sense.

thomashabets2 31 minutes ago | parent [-]

Author here

> The part about hardware is wrong BTW

Could you be more specific? I think by "wrong" you may mean "not actually relevant to UB", and you're right about that. If that's what you mean then that part is not for you. It's for the "but it's demonstrably fine" crowd.

> the hardware semantics are clearly defined

Yup. The article means to dive from the C abstract machine to illustrate how your defined intentions (in your head), written as UB C, get translated into defined hardware behavior that you did not intend.

I'm not saying the CPU has UB, and I wonder what part made you think I did.

That's what I mean game of telephone. The UB parts get interpreted as real instructions by the hardware, and it will definitely do those things. But what are those things? It's not the things you intended, and any "common sense" reading of the C code is irrelevant, because the C representation of your intentions were UB.

__0x01 3 hours ago | parent | prev | next [-]

> A problem with this is that in order to confirm the findings, you’ll need an expert human. But generally expert humans are busy doing other things.

The article suggests using LLMs to identify and fix UB. However as per the above, I think the issue is that we need more expert humans.

LLM generated code will eventually contain UB.

EDIT: added "eventually"

thomashabets2 38 minutes ago | parent | next [-]

Author here.

> The article suggests using LLMs to identify and fix UB. However as per the above, I think the issue is that we need more expert humans.

Yup. But the point of the article is that even expert humans cannot do this alone. And as I wrote, LLM+junior won't suffice either. We need LLM+senior experts.

And it's a problem that we have way more existing UB than expert capacity.

Now, will LLMs and experts both miss UB in some cases? Of course. There's no 100% solution. But LLMs, I claim, will find orders of magnitude more, with low false positive, than any expert. Even if these expert humans (like in the OpenBSD case for the two bugs I found, one of which was UB) are given more than three decades to do it.

I didn't even use the best model, complex code target, or time. I just wanted to choose a target that has a high chance of having very good experts already having audited it.

flohofwoe 2 hours ago | parent | prev | next [-]

It would already help a lot when the C and C++ standards start to clean up the list of Undefined Behaviour (e.g. there's a lot of nonsense UB currently in the C standard which could easily become Defined Behaviour - like the "file doesn't end in a new-line character" thing):

https://gist.github.com/Earnestly/7c903f481ff9d29a3dd1

layer8 2 hours ago | parent [-]

The easy cases like you cite are also those that don’t cause problems in practice. I’m not sure that would help all that much, other than to slightly reduce internet criticism.

talkin 2 hours ago | parent [-]

Fixing easy cases makes the list shorter, so enables more focus on harder cases.

And it also signals that you actually do want to improve, just a little bit of boy scout rule goes a long way.

gpderetta 33 minutes ago | parent [-]

The issue is that the list is infinite (anything not specified is UB), so actually removing any finite amount of UB from the list won't make it shorter.

(only slightly tongue-in-cheek, I do believe that removing silly things is worthwhile).

eru 2 hours ago | parent | prev | next [-]

Our LLM powered coding assistance are pretty good at doing lots of busywork that doesn't require all that much smarts. So they can supervise running our UB checks, like Valgrind, and making the linters happy.

lelanthran 3 hours ago | parent | prev [-]

> LLM generated code will eventually contain UB.

Yes.

Even in languages other than C (i.e. you will get behaviour that nothing in the input specified).

When LLMs generate code, all languages have UB.

eru 2 hours ago | parent [-]

That's a bit silly.

UB means literally no restrictions. So if you standard says 'you have to crash with an error message' that's already no longer UB.

lelanthran 2 hours ago | parent [-]

> So if you standard says 'you have to crash with an error message' that's already no longer UB.

Sure. For crashes. But when you instruct an LLM to do something, the output is probablistic, so you may get behviour that is unexpected and/or unwanted.

Like storing security tokens in code. Or nuking the production database.

rurban 2 hours ago | parent | prev | next [-]

Very bad advice. Of course good new LLM's know about UB, but you still need to use ubsan (ie - fsanitize=undefined), and not your LLM.

formerly_proven 2 hours ago | parent [-]

Coding agents write unsound Rust any day, too. unsafe impl Send … is much easier than fixing a bad design and it might even work momentarily.

fjfaase 44 minutes ago | parent | prev | next [-]

Is comparing a signed integer with an unsigned integer UB? I resently wrote some code and compiled it with gcc to x86_64 (without optimization) that returned an incorrect answer.

Karliss 28 minutes ago | parent | next [-]

No UB, but the integer promotions rules apply.

When comparing signed and unsigned integers of same size the signed one will be converted to unsigned. In a reasonably configured project compiler will warn about it.

In case of integers smaller than int, promotion to int happens first.

In case of signed and unsigned integers of different size, the smaller one will be converted to bigger one.

benchloftbrunch 28 minutes ago | parent | prev [-]

It's not UB. Integer promotion applies, the signed int is implicitly coerced to unsigned (or the other way around - don't remember which.)

lelanthran an hour ago | parent | prev | next [-]

I read through this in detail... Is it just me, or are these things that are invoked by intentionally bypassing the typing?

I mean, you have to go out of your way and use a cast to get the UB in the first example.

For the `isxdigit` implementation, using a parameter to index into an array without a length check is pretty suspect already. I don't think any of my code actually indexes an array without checking the length in some way.

For the float -> int conversion, converting a float to an int without picking a conversion does not make sense in the first place - math.h has rounding and ceiling functions.

> For all you know the compiler has no internal way to even express your intention here.

I'm human, not a compiler, and even I cannot tell what the intention is behind trying to call NULL as a function. What exactly is expected to happen?

> Because the argument needs to be a pointer, and the NULL macro may be misinterpreted as an integer zero.

I don't think this is true for C. The NULL macro is defined to be a pointer in the C standard, AFAIK. Just because comparisons with zero are allowed, does not imply that the standard implicitly promotes NULL to `int`.

I think only the final one is of note (the 24-bit shift assigned to a uint64_t).

account42 39 minutes ago | parent [-]

> I don't think this is true for C. The NULL macro is defined to be a pointer in the C standard, AFAIK. Just because comparisons with zero are allowed, does not imply that the standard implicitly promotes NULL to `int`.

Probably confusion with C++ where NULL is 0 which is a special case that can be implicitly cast to both integers and pointers, unlike non-zero constants. C doesn't need this because it doesn't require explicit casts from void pointers to others.

alper 28 minutes ago | parent | prev | next [-]

Isn't the article mostly saying that SPARC sucks?

akiarie 2 hours ago | parent | prev | next [-]

C is still, by far, the simplest language that we have.

Although many newer languages are safer (with the exclusion of Rust, primarily by being slower) the same kinds of issues that are there in C are there in these languages, their effects are just harder to see.

People complain about C as though they know how to fix it.

simonask an hour ago | parent | next [-]

C is not a simple language in the sense that writing software in C is simple, and I think that's the only useful way to understand the word "simple" in this context.

Brainfuck is "simple" by any other definition as well, but that's not a useful quality.

dns_snek an hour ago | parent | prev [-]

Can you elaborate what do you think C has in terms of simplicity that Zig doesn't, and which "same kinds of issues" do you think it has?

I'm not an expert in either language but my anecdotal experience disagrees with this - writing Zig has been far simpler and less error-prone than writing C.

weinzierl 3 hours ago | parent | prev | next [-]

A fun one that'd fit list be sequence point violations like

    i = i++
radiospiel 3 hours ago | parent | next [-]

Fun, sure, but also GCC and Clang will both warn with -Wall (-Wsequence-point / -Wunsequenced).

account42 an hour ago | parent | prev | next [-]

This would also be a code smell even if it was well defined.

leni536 an hour ago | parent | prev [-]

Only in C, that one is defined in C++.

benj111 12 minutes ago | parent | prev | next [-]

The issue for me with posts like this is that it misses the issue.

Unaligned pointer accesses are UB because different systems handle it differently. This 'should' be to allow the program to be portable by doing what the system normally does.

Instead it's been highjacked by compiler writers, with the logic that "X is UB, therefore can't happen, therefore can be optimised away."

Int c = abs(a) + abs(b); If (a > c) //overflow

Is UB because some system might do overflow differently. In practice every system wraps around.

That should be a valid check, instead it gets optimised away because it 'can't' happen.

C gives you enough rope to hang yourself. The compiler writers don't trust you to use the rope properly.

bvrmn an hour ago | parent | prev | next [-]

I really like Zig's approach to UB. Especially alignment is a part of type. And all this wordy builtins for conversions. Starring to it makes you think what you doing wrong with data model it requires now 3 lines of casting expression.

raluk 3 hours ago | parent | prev | next [-]

In C / C++ there are two kinds of undefined behaviour. One is where there is written in standard what UB is. Another one is everthing else that is not in standard.

wiseowise 3 hours ago | parent | next [-]

https://en.wikipedia.org/wiki/There_are_unknown_unknowns

thaumasiotes 3 hours ago | parent | prev [-]

Technically, that's only one kind, because it's written in the standard that anything not mentioned in the standard is undefined behavior.

cepepe 3 hours ago | parent [-]

One kind, but two different classes of undefined behaviour.

logicchains 3 hours ago | parent | prev | next [-]

The concept of undefined behaviour is also a very useful lens for understanding LLM-based coding. Anything you don't explicitly specify is undefined behavior, so if you don't want the LLM to potentially pick a ridiculous implementation for some aspect of an application, make sure to explicitly specify how it should be implemented.

mbrock 2 hours ago | parent | prev | next [-]

most languages don't even HAVE a specification so in most languages literally EVERYTHING everything is undefined behavior

oersted 2 hours ago | parent [-]

UB doesn't mean that it is not specified (actually it is often very well specified), it means that compilers can and do assume that such code patterns will not be present. Those cases may not be considered and can lead to unexpected behaviour.

Additionally, some (most?) UB is intentionally UB so that optimisers are free to do fancy tricks assuming that certain cases will never happen. Indeed, this is required for high performance. If they do happen, again, it can lead to unexpected behaviour.

PS: Most languages that don't have a specification declare their primary implementation to be specification-as-code. Rust is an example of that, and it does still have UB: the cases that the compiler assumes will not happen.

mbrock an hour ago | parent [-]

undefined behavior is the behavior of code patterns "for which this International Standard imposes no requirements" and the behavior is in fact almost always predictable and agreed upon by compiler vendors and the users of the language, which is why you are able to use programs that rely on undefined behavior probably every single second you are using the computer

edit: for example I'm typing this into Safari which means probably every key press and event is going through JSC JIT compiled functions—which have, structurally and necessarily and intentionally, COMPLETELY undefined behavior according to the spec—and yet it miraculously works, perfectly, because the spec doesn't really matter

veltas 3 hours ago | parent | prev | next [-]

From the ANSI C standard:

  3.16 undefined behavior: Behavior, upon use of a nonportable or erroneous program construct, of erroneous data, or of indeterminately valued objects, for which this International Standard imposes no requirements.  Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message).
Is it just me or did compiler writers apply overly legalistic interpretation to the "no requirements" part in this paragraph? The intent here is extremely clear, that undefined behavior means you're doing something not intended or specified by the language, but that the consequence of this should be somewhat bounded or as expected for the target machine. This is closer to our old school understanding of UB.

By 'bounded', this obviously ignores the security consequences of e.g. buffer overflows, but just because UB can be exploited doesn't mean it's appropriate for e.g. the compiler to exploit it too, that clearly violates the intent of this paragraph.

thomashabets2 25 minutes ago | parent | next [-]

Author here.

I touched on this in the "it's not about optimizations" section. It's not the compiler is out to get you. It's that you told it to do something it cannot express.

It's like if you slipped in a word in French, and not being programmed for French, it misheard the word as a false friend in English. The compiler had no way to represent the French word in it's parse tree.

So no, it's not overly legalistic. Like if the compiler knows that this hardware can do unaligned memory access, but not atomic unaligned access, should it check for alignment in std::atomic<int> ptr but not in int ptr? Probably not, right?

dataflow 3 hours ago | parent | prev | next [-]

> but that the consequence of this should be somewhat bounded or as expected for the target machine.

Aren't "unpredictable results" and "no requirements" contrary to the idea that the behavior would be "somewhat bounded"?

veltas 3 hours ago | parent [-]

Notice though "ignoring the situation" thru "documented manner characteristic of the environment". Even though truly you can read this in an uncharitable way, you could also try and understand the intent of this paragraph, and I think reading it for its intents is always the best way to interpret a language standard when the wording is ambiguous or soft, especially if you're writing a compiler.

I don't think you could sincerely argue that this definition intends to allow the compiler to totally rewrite your code because of one guaranteed UB detected on line 5, just that it would be good to print a diagnostic if it can be detected, and if not to do what's "characteristic of the environment". Does that make sense?

gpderetta 3 hours ago | parent | next [-]

Ex falso quodlibet.

Bounding UB would be a nice idea, or at least prohibiting time-traveling UB (and there is an effort in that direction). But properly specifing it is actually hard.

account42 34 minutes ago | parent [-]

Prohibiting "time-travelling" UB would be horrible as that's a very important mechanism for dead code elimination.

cracki 3 hours ago | parent | prev [-]

Reading for intent is pragmatic.

Reading adversarially is what people do who are looking for ways that something can be abused, from an offensive or defensive position.

Personally I am tired of the entire topic.

veltas 2 hours ago | parent [-]

What's bad is when your compiler writers and most of the people involved in standardisation are reading it adversarially.

account42 32 minutes ago | parent [-]

It's bad when compiler writers want to optimize correct code as much as possible, which is something their actual customers keep asking for?

lelanthran 3 hours ago | parent | prev [-]

> Is it just me or did compiler writers apply overly legalistic interpretation to the "no requirements" part in this paragraph?

I've (fruitlessly) had this discussion on HN before - super-aggressive optimisations for diminishing rewards are the norm in modern compilers.

In old C compilers, dereferencing NULL was reliable - the code that dereferenced NULL will always be emitted. Now, dereferencing NULL is not reliable, because the compiler may remove that and the program may fail in ways not anticipated (i.e, no access is attempted to memory location 0).

The compiler authors are on the standard, and they tend to push for more cases of UB being added rather than removing what UB there is right now (for exampel, by replacing with Implementation Defined Behaviour).

my-next-account 3 hours ago | parent | prev | next [-]

Hello, it's me. I'm not afraid of UB.

my-next-account 3 hours ago | parent [-]

To be honest, miscompilations because of UB is exceedingly rare, and we do a lot of weird shit in our code.

fithisux 3 hours ago | parent | prev | next [-]

UB can also have impact in logical cohesion of codebase.

cracki 3 hours ago | parent | prev | next [-]

We know. This is not news.

boxed 3 hours ago | parent [-]

It seems to be to many many programmers who keep using C++

VimEscapeArtist 2 hours ago | parent | prev | next [-]

Wait until he discovers PowerShell ;D

SanjayMehta an hour ago | parent | prev | next [-]

I used to teach C programming and one time I got anonymous feedback: "when this instructor doesn't know the answer he says "it's compiler dependent.""

Shrug.

jraph 3 hours ago | parent | prev | next [-]

Yet another push to use LLMs after casting fear. Now it should be illegal not to use LLMs. A good start of the day.

(I hope casting fear is not UB)

raverbashing 3 hours ago | parent | next [-]

> (I hope casting fear is not UB)

I'm sure that's UB in C

In C++ just use <reinterpret_cast>

wg0 3 hours ago | parent | prev [-]

The irony is unmistakable.

stevenhuang 3 hours ago | parent [-]

There is nothing ironic in letting an llm have a pass at identifying potential UB and other correctness issues in C code.

I say this as an experienced C developer.

wg0 2 hours ago | parent [-]

It is ironic because the behaviour of an LLM itself is UB. Guaranteed.

nokeya 3 hours ago | parent | prev | next [-]

Ok, and?

wg0 3 hours ago | parent [-]

"Rewrite everything in Rust. OMG universe is written in Rust so memory safe with zero allocations"

stackghost 3 hours ago | parent | prev | next [-]

Anyone who uses the construction "C/C++" doesn't write modern C++, and probably isn't very familiar with the recent revisions despite TFA's claims of writing it every day for decades.

Far from being just "C with classes", modern C++ is very different than C. The language is huge and complex, for sure, but nobody is forced to use all of it.

No HN comment can possibly cover all the use cases of C++ but in general, unless you have a very good reason not to:

- eschewing boomer loops in favor of ranges

- using RAII with smart pointers

- move semantics

- using STL containers instead of raw arrays

- borrowing using spans and string views

These things go a long way towards, shall we say, "safe-ish" code without UB. It is not memory-safe enforced at the language level, like Rust, but the upshot is you never need to deal with the Rust community :^)

veltas 3 hours ago | parent | next [-]

Although some people, like Bjarne Stroustrup, object to the term C/C++, it's a bit like Richard Stallman objecting to the term "Linux". The fact is it can mean "C or C++", and I wouldn't assume the author thinks they're the same, but they're talking about both of them together in the same sentence. This seems reasonable given this is about undefined behavior, and it's trivial to accidentally write UB-inducing code in C++ even with modern style (although I'd say you should catch most trivial cases with e.g. ubsan, and a lot of bad cases would be avoided with e.g. ranges, so I think the article is exaggerating the issue).

stackghost 3 hours ago | parent [-]

Well, the author explicitly refers to "C/C++" as one language:

>After all, C/C++ is not a memory safe language.

thomashabets2 20 minutes ago | parent [-]

That is a typo, that I think I introduced when I went back to clarify that it applies to C++ too.

Will fix it.

thomashabets2 21 minutes ago | parent | prev | next [-]

Author here.

In the context of UB discussion, the arguments apply equally to C and C++.

How would you write that?

I entirely agree with all your points that C and C++ are completely different languages at this point. And yet I wanted to write this post about something that is true for both.

SpaceNugget 3 hours ago | parent | prev | next [-]

I totally agree that modern c++ is pretty robust if you are both a well seasoned developer and only stick to a very blessed subset of it's features and avoid the historical baggage.

However, that's obviously not the point? Ignoring the idea that people can/should just "git gud" and write perfect code in a language with lots of old traps, you can't control how everyone else writes their code, even on your own team once it gets big enough. And there will always be junior devs stumbling into the bear traps of c/c++ (even if the rest of the codebase is all modern c++). So no matter how many great new features get added to C++, until (never) they start taking away the bad ones, the danger inherent to writing in that language doesn't go away.

Also, safe != non-UB. TFA isn't so much about memory safety anyway.

rectang 3 hours ago | parent | prev | next [-]

> the upshot is you never need to deal with the Rust community

In the end, everything comes down to culture war.

stackghost 3 hours ago | parent [-]

Perhaps we should rewrite our culture in Rust.

flohofwoe 3 hours ago | parent | prev | next [-]

"C/C++" is still a useful term for the common C/C++ subset :)

As far as stdlib usage is concerned: that's just your opinion. The stdlib has a lot of footguns and terrible design decisions too, e.g. std::vector pulling in 20k lines of code into each compilation unit is simply bizarre.

Also:

- eschewing boomer loops in favor of ranges

Those "boomer loops" compile infinitely faster than the new ranges stuff (and they are arguably more readable too): https://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/

- borrowing using spans and string views

Those are just as unsafe as raw pointers. It's not really "borrowing" when the referenced data can disappear while the "borrow" is active.

m-schuetz 3 hours ago | parent | prev [-]

C/C++ is a perfectly fine term for C or C-style C++. The languages can be very close, and personally I prefer C-style C++ miles over some of the half-baked modern nonsense. I mean, I do use C++23 since it has some great additions, but I'm ditching like 90% of the stuff that only adds complexity without much benefit.

dmitrygr 3 hours ago | parent | prev | next [-]

I stoped reading about here:

    > bool parse_packet(const uint8_t* bytes) {
    >   const int* magic_intp = (const int*)bytes;   // UB!
Author, if you are reading this, please cite the spec section explaining that this is UB. Dereferencing the produced pointer may be UB, but casting itself is not, since uint8_t is ~ char and char* can be cast to and from any type.

you might try to argue that uint8_t is not necessarily char, and while it is true that implementations of C can exist where CHAR_BIT > 8, but those do not have uint8_t defined (as per spec), so if you have uint8_t, then it is "unsigned char", which makes this cast perfectly safe and defined as far as i can tell. Of course CHAR_BIT is required to be >= 8, so if it is not >8, it is exactly 8. (In any case, whether uint8_t is literally a typedef of unsigned char is implementation-defined and not actually relevant to whether the cast itself is valid -- it is)

raphlinus 3 hours ago | parent | next [-]

The issue is not type punning (itself a very common source of UB), but the fact that the `bytes` pointer might not be int-aligned. The spec is clear that the creation (not just the dereferencing) of an unaligned pointer is UB, see 6.3.2.3 paragraph 7 of the C11 (draft) spec.

Of course, this exchange just demonstrates the larger point, that even a world-class expert in low level programming can easily make mistakes in spotting potential UB.

flohofwoe 2 hours ago | parent | next [-]

> Of course, this exchange just demonstrates the larger point, that even a world-class expert in low level programming can easily make mistakes in spotting potential UB.

A "world-class expert in low level programming" knows that unaligned memory accesses are no problem anymore on most modern CPUs, and that this particular UB in the C standard is bogus and needs to fixed ;)

formerly_proven 2 hours ago | parent [-]

… it’s only UB if the pointer is actually misaligned. It’s not possible to tell from these two lines whether that’s the case.

gritzko 3 hours ago | parent | prev | next [-]

C of course is ancient. It remembers the Cambrian explosion of CPU architectures, twelve-bit bytes and everything like that. I wonder if it is possible to codify some pragmatic subset of it that works nicely on currently available CPUs. Cause the author of the piece goes back in time to prove his point (SPARCs and Alphas).

dmitrygr 3 hours ago | parent [-]

Fun story: even the latest C spec doesn’t require CHAR_BIT == 8, but it does now codify 2s complement int representation. (IIRC)

eru 2 hours ago | parent [-]

For unsigned ints, or also for signed ints?

account42 10 minutes ago | parent | next [-]

Two's complement is a representation specifically for signed integers.

dmitrygr 2 hours ago | parent | prev [-]

For signed. Unsigned overflow was defined for a while now.

dmitrygr 3 hours ago | parent | prev [-]

That cast is valid. Spec does not guarantee same bit sequence for resulting pointer and source pointer. But as the cast is explicitly allowed, it is not UB. Compiler is free to round the pointer down. Or up. Or even sideways. All ok. Dereferencing it — indeed not ok. But the cast is explicitly allowed and not UB.

Pointer casts changing pointer bit sequences is common on weird platforms (eg: some TI DSPs, PIC, and aarch64+PAC). And it is valid as per spec. Pointer assignment is not required to be the same as memcpy-ing the pointer unto a pointer to another type.

You misunderstood the spec. No promises are made that that cast copies the pointer bit for bit (and thus creates an invalid pointer). Therefore, your objection to invalid pointers is null and void. :)

raphlinus 3 hours ago | parent [-]

I'm not assuming anything about bit representations. In this case, the spec language is quite clear and unambiguous.

6.3.2.3 paragraph 7: A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned[footnote 68]) for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

This is a subsection of section 6.3 which describes conversions, which include both implicit and conversions from a cast operation. This language is not saying anything about bit representations or derefencing.

I happen to be wearing my undefined behavior shirt at the moment, which lends me an extra layer of authority. I'm at RustWeek in Utrecht, and it's one of my favorite shirts to wear at Rust conferences. But let's say for the sake of argument that you are right and I am indeed misunderstanding the spec. Then the logical conclusion is that it's very difficult for even experienced programmers to agree on basic interpretations of what is and what isn't UB in C.

dmitrygr 2 hours ago | parent [-]

I do not see there a promise that the cast will produce an invalid pointer, nor anything prohibiting the compiler from rounding the pointer down, thus producing a valid one. “Converted” does not require bit copy. I don’t see how this interpretation is against any section of the spec.

dwattttt an hour ago | parent | next [-]

I also do not see any requirement in the quoted text that the casted pointer be dereferenced before noting "the behavior is undefined".

In practice performing a cast doesn't really do much until you dereference, but without a carve out in the spec, it really mean "the behavior is undefined".

pjc50 an hour ago | parent | prev | next [-]

> rounding the pointer down, thus producing a valid one

A "valid" pointer to the wrong object?

cyclopeanutopia 2 hours ago | parent | prev [-]

> Otherwise, when converted back again, the result shall compare equal to the original pointer.

Doesn't this part exclude the possibility of rounding down?

thomashabets2 11 minutes ago | parent | prev | next [-]

Author here.

> A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned71) for the referenced type, the behavior is undefined.

C23 6.3.2.3p7.

stevenhuang 3 hours ago | parent | prev [-]

Byte and int has different alignment requirements. It is UB the moment you make such a ptr.

Great way to demonstrate the point of the article.

gritzko 2 hours ago | parent | next [-]

That better be marked "historical". At least, Lemire says:

On recent Intel and 64-bit ARM processors, data alignment does not make processing a lot faster. It is a micro-optimization. Data alignment for speed is a myth. // https://lemire.me/blog/2012/05/31/data-alignment-for-speed-m...

(while in the olden days, a program may crash on unaligned access, esp on RISC)

eru 2 hours ago | parent [-]

Don't mix up what processors do with what the C standard allows you to get away with.

flohofwoe 2 hours ago | parent [-]

...and don't mix up the C standard with what actually existing compilers allow you to get away with ;) In the end the standard is merely a set of guidelines. What matters is how compiler toolchains behave in the real word, and breaking code which does unaligned memory accesses by 'UB exploitation' would be quite insane.

dmitrygr 2 hours ago | parent | prev [-]

Without memcpy there is no guarantee that that line produces an invalid pointer

I don’t see what spec part would prohibit that cast from validly compiling to

   BIC r3, r0, #3
Spec only guaranteed round-trip through char* of properly aligned for type pointers. This doesn’t break that.
grougnax 2 hours ago | parent | prev | next [-]

Use Rust!

reinhash an hour ago | parent | prev | next [-]

Rust.

liamd1988 3 hours ago | parent | prev | next [-]

When use C ,keep using char* not mess with int*

momo26 3 hours ago | parent | prev [-]

Debugging in C is soooo hard. When I was writing Malloc Lab in system course, there were uncountable undefined and out of range :(

flohofwoe 3 hours ago | parent [-]

Yet, debugging memory corruption issues in C and C++ code with modern compiler toolchains and memory debugging tools is infinitely easier than 25 years ago.

(e.g. just compiling with address sanitizer and using static analyzers catch pretty much all of the 'trivial' memory corruption issues).