Reducing undefined behavior in the C language

6 points by fanf


olliej

Undefined behavior exists for a number of reasons. It allows implementations to support extensions, manage interactions with hardware-based safety mechanisms

No.

No. No. No.

UB has nothing to do with supporting different hardware behaviour. At all.

Let's take signed integer overflow. That is UB. This does not mean that you can write a + b < a as an overflow check if that's the behaviour on your hardware. Being UB means that if you code does this, you code is wrong. It does not check for overflow, it either:

This is ostensibly useful for vectorizing loop induction variables, but I'm not super convinced.

There are

  1. Defined behavior
  2. Unspecified behavior
  3. Implementation defined behavior
  4. Undefined behavior

(1) is obvious. (2) and (3) are what support different implementations and hardware, etc. The behavior is still defined, the code that triggers them is correct (per the AM, you're still free to have logic bugs), the compiler is constrained, it cannot assume it doesn't exist.

And UB is behavior that is invalid on the AM, and anything invalid on the AM can be assumed to not happen.

(C++ also has Erroneous behavior, but that basically just exists because the standard wants to be able to say a common error is an error, but recognize the failure mode is so catastrophic that it requires the compiler to have safe-ish behavior)

Another misconception is that the compiler is detecting and optimizing UB it finds. What actually happens is the compiler encounters some code, and assumes that UB does not happen. e.g lets look a different case of ub: null dereferences.

void do_thing(int a) {
  precondition(a > 0);
  userid = a;
}

Can a call to this function set userid to 0? let's look at that precondition call. It turns out to be a macro:

#define precondition(x) if (!(x)) *(char*)0=0;

From the compiler's point of view, that macro means that if a < 0 a null dereference occurs. That is UB, so it can't happen, and the branch disappears, and the precondition does nothing. This is not hypothetical: there was a period where dereferencing a nullptr was considered a valid way to force a trap. Then one day gcc started optimizing on the basis of it being UB, and a whole bunch of security bugs went from theoretical to real.

And to be even more explicit about what UB permits:

void do_thing(int a) {
  a = -1;
  precondition(a > 0);
  userid = a;
}

can call an arbitrary function.