Live data from Hacker News

Everything in C is undefined behavior

blog.habets.se

561–570 of 748 posts

Re: Everything in C is undefined behavior

#561

Earlier quoted context omitted.

It's a fix that removes the most pointy part of UB. "Going past the end of the array results in addressing arbitrary values" I can live with. "Going past the end of an array results in anything happening" is a hard sell.

Is that really a meaningful distinction? Once you are addressing arbitrary values you are firmly in the realm of "anything happening" in practice, but you've now given up optimization opportunities. As has been repeatedly demonstrated over the years, once memory safety breaks it is practically impossible to make any guarantees about program behavior.

Yes, it's a meaningful distinction. No you are not into "anything happening" in practice.

Your compiler emitting a load operation and it failing isn't "anything". The failure being handled by code that the compiler authors can't predict doesn't make it "anything".

And if you lose optimization opportunities because of this it's because your optimization is broken. By the way, if you lose optimization opportunities because of this, that means both codes are meaningfully different and you knew it all the time.

Re: Everything in C is undefined behavior

#562

Earlier quoted context omitted.

I'm not saying that the result of the dereference be known, I'm saying that the instructions to do the dereference be always emitted. Right now, if a dereference results in UB, the compiler may omit it entirely.

Wouldn't that make a compiler that emitted bounds checks violate the standard, since it would not be emitting the actual memory operations if you deref out of bounds?

No, because it's UB so there is no standard.

Re: Everything in C is undefined behavior

#563
post #8

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

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.

There is that famous example where when you write an infinite loop last thing in your main, a function that you never called runs instead.

Re: Everything in C is undefined behavior

#564

Earlier quoted context omitted.

“Implementation defined behaviour”: compiler author chooses, and documents the choice. A lot of UB should be implementation defined behaviour instead; this would much better match programmers’ intuitions as they reason about their code - you can even see it in the comments of this post: it’s always things like “this hardware supports / doesn’t support unaligned accesses”, it’s never nasal demons.

I told someone at a conference that UB actually means "implementation-defined, no documentation required". He started to refute me and then stopped.

That isn't true, for UB the compiler is allowed to assume the UB can never happen. For example if you dereference a pointer and only after check if it is NULL, the compiler can remove the NULL check, since it is clearly impossible (nevermind that you might be on a microcontroller where NULL is a valid address).

The fallout of this are quite large! If behaviour is implementation defined the compiler has to stick to one consistent behaviour. No such need for UB, you can get different behaviour bu changing unrelated code, by changing between debug and release or just because of what garbage happened to be on the stack.

Since the compiler is allowed to assume the UB doesn't happen it will also sometimes look like the compiler miscompiled your code elsewhere, but what actually happened was some inlining followed by extrapolating "this can never happen".

UB is often surprising: I have seen unaligned loads crash on x86 due to it bring UB in C (even though x86 is generally fine with it). But once a newer compiler decided that it was fine to vectorise that code (since it clearly aligned) the CPU was no longer happy with it.

Re: Everything in C is undefined behavior

#565

Earlier quoted context omitted.

Not really. Wait until the compiler starts vectorizing your code and using instructions requiring alignment (like the ones with A or NT in the mnemonic).

Usually the compiler will probably not generate those

> Usually...probably...

you're betting against the compiler ever improving.

Re: Everything in C is undefined behavior

#566
The problem is incorrectly assuming that the spec is meaningful in some kind of rigorous way.

It’s not. All that matters is what C compilers actually do and what real C programs expect.

This is a good thing. It creates a culture where the two sides meet each other where they’re at

Re: Everything in C is undefined behavior

#567
post #518

Earlier quoted context omitted.

More like, "if you do this, what happens depends on your particular combination of hardware, operating system, and compiler. Don't ask us."

No, that would be implementation defined.

No, that's actually UB. The important bit here is "compiler defined" -- UB means the compiler is allowed to assume it never happens while compiling.

Consider, for example, an implementation defined function f() -- which can also diverge/crash horribly, etc.

If I write

    if p {
      print("p is true")
    } else {
      g()
    }

    if p {
      f()
    }
Then either we: - print p is true and execute f - do nothing

This is true regardless of if f immediately crashes the computer, nasal demons, whatever -- that's implementation defined.

UB means f may never happen.

And that means the compiler may optimize this to just:

    g()
Notice the difference here -- the print never happens!, and g always happens.

You can see why this is concerning when you write code like

    if dry_run {
      print("would run rm -rf /")
    } else {
      run("rm -rf /")
    }

    if dry_run {
      // oops: some_debug_string is NULL and will segfault!
      print(some_debug_string);
    }

Re: Everything in C is undefined behavior

#569
post #478

Earlier quoted context omitted.

The problem is that in the quest to win benchmark games, compilers started to take advantage of UB for all kinds of possible optimizations, which is almost as deterministic as LLM generated code, across compiler version updates.

Soooo… Pay attention to updates changelog?

This isn't an answer. UB is not only code dependent, but in many cases value-dependent as well. Changing anything about a program has the potential to cause UB anywhere in the code graph affected. So even the smallest possible change requires you to fully understand that entire graph, as well as the entire compiler history and how it interacts with your program. Remember, UB isn't diagnostic and runtime sanitizers don't catch everything, nor does exhaustive testing and static analysis.

Re: Everything in C is undefined behavior

#570

Earlier quoted context omitted.

Can you unravel this further (for those of us who don’t know compilers)? I’ve always assumed access past the end of an array can’t always be detected in C, so I don’t see how those instructions could be eliminated. For example, a dynamically linked library that takes in a pointer, and then writes to the 10 ints after it—whether or not this behavior is defined is determined after that library is compiled, right?

> I’ve always assumed access past the end of an array can’t always be detected in C, so I don’t see how those instructions could be eliminated. "Can't always be detected" is jut a different way of saying "Can sometimes be detected". Upon detection, I'd rather that the compiler still emit the instructions, not elide the code altogether.

Now the behavior of your compiler/runtime stack is dependent on the sophistication of your compiler or runtime relative to the particular code at issue + the specific information available statically or dynamically in the instance.

That does not seem like an improvement if your goal is predictable, consistent behavior.

Post reply on HN