
Undefined Behavior in 2017 - ingve
https://blog.regehr.org/archives/1520
======
the8472
> Loops that Neither Perform I/O nor Terminate

> Summary: This UB is probably not a problem in practice (even if it is
> moderately displeasing to some of us).

Since LLVM is mostly based around C/C++ semantics this actually turned out to
be a problem for rust, because rust can encode diverging functions in its type
system and assume certain code sections to be unreachable. If the compiler
then optimizes out code and the unreachable (thus not-compiled) section is
reached things explode.

[https://github.com/rust-lang/rust/issues/28728](https://github.com/rust-
lang/rust/issues/28728)

~~~
userbinator
I think that's a great example of how optimisers have gone off the deep end:
no reasonable human programmer would think "I can't figure out if this loop
terminates, so let's just leave it out", but that appears to be the default
behaviour in this case --- when what should happen when the optimiser "gives
up" is that it should just translate the code verbatim.

~~~
adrianN
But the optimizer doesn't give up in this case. It successfully figures out
that the loop has no side effect except heating up your processor and
eliminates it.

~~~
iainmerrick
What's the advantage of eliminating the loop?

If the compiler is able to do that analysis, why not flag the endless spin
loop as a compilation error?

~~~
kazagistar
Loop is inside a function, that can have no side effect conditionally.

Function is used in multiple places and inlined.

With inlined context, compiler can eliminate loop sometimes.

------
pjmlp
The main message from my point of view:

> Be knowledgeable about what’s actually in the C and C++ standards since
> these are what compiler writers are going by. Avoid repeating tired maxims
> like “C is a portable assembly language” and “trust the programmer.”

> Unfortunately, C and C++ are mostly taught the old way, as if programming in
> them isn’t like walking in a minefield. Nor have the books about C and C++
> caught up with the current reality. These things must change.

~~~
userbinator
I think it's the _compilers_ that have perverted the language to such an
extent that it's become ridiculously difficult to understand, and so it's the
compilers that must change back to being less obtuse and adversarial.

The C standard even suggests, when defining undefined behaviour, that one of
the possible options is "behaving during translation or program execution in a
documented manner characteristic of the environment". If signed int arithmetic
on the hardware wraps around upon overflow, then take that into account when
optimising and don't assume it can't overflow. This is how C programmers want
the language to work; compiler writers and standards be damned.

I've written plenty of Asm and also looked at as much if not more compiler
output; and while the latter sometimes pleasingly surprises me, it's usually
pretty evident that it was not generated by an intelligent entity. (And
countless times, I've obtained size and/or speed savings by replacing the
latter with what I would otherwise write.) My general impression is that there
aren't enough highly-skilled Asm programmers working on compiler development
--- there are many reasons I can think of for that, but one of the things I've
always wished to exist is a simple, straightforward C compiler with
"understandable" output and optimisation that creates results closer to what a
human Asm programmer might write; with the associated optimisation advantages
and predictability. This means _not_ assuming anything just because the
standard says so, but taking a more holistic (for lack of a better word)
approach to compilation and optimisation.

A long-time favourite post on this: [http://blog.metaobject.com/2014/04/cc-
osmartass.html](http://blog.metaobject.com/2014/04/cc-osmartass.html)

~~~
clarry
> If signed int arithmetic on the hardware wraps around upon overflow, then
> take that into account when optimising and don't assume it can't overflow.
> This is how C programmers want the language to work;

Don't speak for all of us.

Some of us may take the language for what it is, instead of assuming it to be
a "high level portable assembler" which it is not, but which a particular
implementation of it _may_ be, thanks to the standard leaving much up to the
implementation.

It is this same leeway that (thankfully) allows smart optimizing compilers
that can generate performant code for programs that are not omgwtf-optimized
at the source level.

And it is this same leeway that allows very simplistic, naive implementations
that do not go out of their way to perform crazy analysis to try and warn you
about some corner case of undesirable behavior your code might hit.

You're right that it is the compilers can and need to change to accommodate
different types of users. But I don't think every compiler should just go back
to being a 1991 compiler; not every compiler needs to accommodate every user.

~~~
runevault
They CAN accommodate all users, just have compile time options you can enter
in your make file/on the command line/etc to say what level of bounds
checking/etc you want compiled into your code. Hell that way even within a
given project you can choose "this library I'm nervous about so I'll eat the
hit on bounds checking".

~~~
pjmlp
They do it, but the performance culture among C devs doesn't appreciate
enabling them.

[https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Object-Size-
Che...](https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Object-Size-
Checking.html#Object-Size-Checking)

[https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Pointer-
Bounds-...](https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Pointer-Bounds-
Checker-builtins.html#Pointer-Bounds-Checker-builtins)

I bet not much UNIX software compiled with gcc enables them.

Android is probably the only OS that does make use of them.

[https://android-developers.googleblog.com/2017/04/fortify-
in...](https://android-developers.googleblog.com/2017/04/fortify-in-
android.html)

~~~
wahern
The performance culture isn't just C. Far from it.

Rust was susceptible to Stack Clash because they ripped out their existing
stack probing to gain a few percent increase in performance.

The article for this thread literally says, "For many use cases ASan is the
better choice because it has much less overhead." IME Valgrind does a _much_
better job of detecting memory issues, partly because ASan only detects issues
directly triggered in the code that you compile, whereas Valgrind can detect
memory issues directly triggered by external code, but indirectly caused by
your code.

~~~
pjmlp
> The performance culture isn't just C. Far from it

Sure, but it is where it is more visible, specially micro-optimizing code as
it is being written, without validating if it really matters to the
application's use case with a profiler.

The school of systems programming languages from Algol side, was that
correctness was much more relevant than pure performance, Algol dialects for
systems programming already had Rust like unsafe blocks in the mid-60's.

In Burroughs B5500 the administrator could enable which applications with
unsafe modules were allowed at all to be executed, 10 years before C was
invented.

~~~
wahern
I've never met a C programmer or C compiler author who felt correctness should
take a backseat to performance. And as I demonstrated, the Rust designers are
hardly immune to poor decision making on this score.

Someone else corrected me regarding the stack probes--Rust didn't have probes
but actually checked the size of the stack. It's also worth pointing out that
often times decisions are premised on maintenance burdens. Performance was one
excuse for removing the stack checking in Rust; the other was the maintenance
burden of maintaining a feature outside of LLVM.

That goes directly to the heart of the undefinedness issue in C. Doing the
"obviously correct" thing wrt to choosing between possible undefined- or
implementation-defined behaviors isn't as easy as we think. Good compilers are
built on abstractions and code reuse, like any other piece of well-written
software. Doing the correct thing in some obvious cases can result in having
to make non-obvious tradeoffs elsewhere, possibly having to choose between
adding (or maintaining) complexity, or removing a powerful feature, such as
deterministic stack overflow prevention.

The history of C (indeed, Unix and the whole "worse is better" school of
design) is about favoring less complexity in the tooling while shifting more
of the burden onto the user. That's a judgment not about runtime performance,
but a very unintuitive judgment about the performance and efficiency of
programmer time; that you quickly reach a point of diminishing returns, much
sooner than most people believe, putting complexity into, and relying on
intrinsic features of, the tooling; that it's often better to make it easier
to implement and integrate specialized tools, and easier to reuse existing
tools as part of a more focused solution. From that perspective the design of
C makes a ton more sense, even if reasonable people can still disagree about
the results.

The notion that C--the language design, the ecosystem--is principally about
performance is something mostly held by inexperienced programmers. It's a
strawman of critics. There were plenty of other languages from the 1970s that
were capable of generating faster code than C. Certainly performance was and
continues to be of significant concern, but C is hardly unique in that sense.
If the debate about how to balance performance with other issues stands-out,
I'd argue that's primarily because of C's ubiquitous position and how it and
the computing industry (including hardware industry) has co-evolved.

When something seems so "obvious" (security vs performance), yet everybody
continues to seemingly repeat the same dumb mistakes, often times what we're
missing is that our premises are flawed. Maybe C didn't become ubiquitous
because of it's performance, but more so because it was easy to port and easy
to build upon. After several years Rust still has a single implementation.
Even Go has two production-quality implementations. That's not a criticism of
Rust, just an observation that the "security vs performance" axis is hardly
the only relevant dimension for judgment and explanation.

------
chrisdew
How many of these 200+ undefined behaviours exist in more modern low-level
languages, such as Rust and D?

~~~
lukego
LuaJIT is another modern low-level language. It has some remarkable undefined
behavior like the evaluation order for function arguments. Scares me a bit.

[https://github.com/LuaJIT/LuaJIT/issues/238](https://github.com/LuaJIT/LuaJIT/issues/238)

~~~
readittwice
The same is true in C/C++: Function argument evaluation order is unspecified.
BUT: that doesn't mean this is undefined behavior, it is just unspecified.

unspecified != undefined behavior

See this Stack overflow answer for an explanation:
[https://stackoverflow.com/questions/2397984/undefined-
unspec...](https://stackoverflow.com/questions/2397984/undefined-unspecified-
and-implementation-defined-behavior)

------
jpfr
I wonder why he mentions this TIS interpreter but not the development of
complete semantics for C and specifically the k framework
([https://github.com/kframework/c-semantics](https://github.com/kframework/c-semantics)).

Afaik, kcc is also really good at finding undefined behavior. Regehr even
wrote about it in the past
([https://blog.regehr.org/archives/523](https://blog.regehr.org/archives/523)).

~~~
tom_mellior
AFAIR from the last presentation on the K framework I attended (a year ago),
the publicly available version of their C interpreter is incomplete: It only
runs programs that don't need the standard library. That's pretty great for
many things! But it's not a turnkey solution for analyzing real applications.
For that they had a proprietary commercial version. (Again, this is from
memory, and it may have changed.)

Another reason to mention TIS-interpreter specially is that Pascal Cuoq (the
co-author of this blog post) is one of its developers, and his livelihood
depends on it being used.

~~~
ellisonch
Original developer of the C semantics in K here! I just wanted to add a
pedantic clarification (since pedantry is what semantics is all about).

The semantics of the language itself is more or less complete. On top of that,
a sizable portion of the standard library is given semantics directly (e.g,
malloc, setjmp, stdargs, I/O including input/output to FILEs, much of printf,
etc. are all formalized in the semantics). There's even basic C11 thread
support. All of this runs and can be reasoned about formally. I tried to give
direct semantics to the things that couldn't be (easily) written in C, because
much of the standard library CAN be written in C and so it isn't so critical
that it's formalized.

To round out the library you can provide provide C implementations of any
missing library functions at compile time (we provide some by default), and it
will KCC those just like any other code. The resulting system is pretty good
at catching undefined behavior, but it is super slow.

I'm not involved with the commercial version, but they offer a trade off:
don't do any semantic checking inside standard library calls in exchange for
faster execution times. My understanding is that instead of chugging through
the semantics of printf for every bottle of beer on the wall, they just call
out to a native of printf. This makes things a lot faster, but it's possible
you won't catch as many problems. In general, the commercial version is trying
to make using the semantics practical instead of "academically slow", which
was all I managed :)

Any questions welcome.

~~~
tom_mellior
Thanks for the clarification! That's kind of what I remembered, but you
explained it much better.

------
Cryptoholic
Mitigation: Stop using languages that let you run roughshod over memory like C
and C++ do.

------
nikanj
I wish there was a GCC switch that made the optimizer go "This is clearly
undefined behaviour. I'll stop this compile right here", as opposed to "This
is clearly undefined behaviour. Sweet jackpot, it means I can do whatever the
heck I want. Drop those NULL checks below, I've got the golden UDB ticket now"

~~~
dom0
The optimizer doesn't work that way. It _assumes_ the code does not do UB _and
then_ removes branches that can never be taken if the code executes no UB.

That's different from "This is UB, and now I do ...".

~~~
cousin_it
I think we deserve a compiler warning whenever a _source_ line of code (not a
line generated by the preprocessor) gets removed due to UB exploitation. All
such cases are worth the programmer's attention, because they are either
useless lines or bugs.

~~~
quantdev
It's more complicated than this, unfortunately. Imagine an int variable x and
two expressions f(x) and g(x) of x. Now imagine an if statement with condition
f(x) < g(x). Precisely because incrementing the largest int is undefined
behavior, the compiler may be able to simplify the if condition because, for
example, it can assume x+1 > x is always true. So it's not necessary for the
line to be useless or wrong for undefined behavior rules to trigger compiler
optimizations.

------
wereHamster
Why don't compilers have an option to reject UB with an compiler error? Surely
the compiler must know when it comes across a piece of source code whose
semantic is undefined.

~~~
dom0
Because a lot of UB cannot be efficiently determined at compile time. Hence,
UBSan.

~~~
pjmlp
It is very easy to be efficiently determined at compile time, other systems
programming languages do it, by not having UB on their language standard to
start with.

If people prefer "performance trumps correctness" and "winning compiler
benchmarks is what counts" then they should happily keep getting exploited.

~~~
jabot
"Very easy" \- well, if you think garbage collection is easy...

And yes, if you want to allow manual allocation of objects and setting
pointers to arbitrary objects, then your two choices are: 1) garbage
collection 2) undefined behavior (use-after-free, double-free, dangling
pointers, etc.)

~~~
pjmlp
> garbage collection is easy...

Garbage collection isn't the only way of writing _mostly_ safe systems
programs.

ESPOL, NEWP are 10 year older than C.

PL/8 was the systems programming language used by IBM for their first RISC
mainframe, followed by PL/S.

Mesa used at Xerox PARC.

Modula-2 used at ETHZ and many 16 bit systems.

Object Pascal used to write the first versions of Mac OS and Lisa.

Ada and SPARK used by high integrity systems where human lives can be at risk.

All of them with not even a quarter of C's UB collection.

~~~
jabot
Well, I don't know about half of these programming languages. However:

\- ADA requires garbage collection

\- Modula-2 and Object pascal have manual memory management, i.e.: "new" and
"delete" \- together with double-free, use-after-free, and dangling
pointers...

I agree that C is by far worse, but please recognize that at least some of the
undefined behavior problems are really hard to solve.

EDIT: Oh, and we haven't started talking about multi threading yet, have we?

~~~
pjmlp
What about learning to understand English?

"writing mostly safe systems programs."

Do you understand what _mostly_ means?

Ada and Modula-2 have multi-threading as part of their ISO/ANSI language
standard.

Also you don't seem to know much about Ada given your GC remark, but here is
some learning.

[https://archive.fosdem.org/2016/schedule/event/ada_memory/](https://archive.fosdem.org/2016/schedule/event/ada_memory/)

Of course there are still use cases where Algol derived languages are still
unsafe, however those use cases are a tiny portion of what happens in C land.

~~~
jabot
> What about learning to understand English?

That's uncalled for, man...

> Of course there are still use cases where Algol derived languages are still
> unsafe, however those use cases are a tiny portion of what happens in C
> land.

On that I agree - there are many problems we wouldn't have if we were using,
say, pascal or modula.

> Also you don't seem to know much about Ada given your GC remark, but here is
> some learning.

I'm sorry, I don't have time to watch that. However, I know that ADA has
either GC or manual memory management (with the deallocation method explicitly
marked as unsafe). Thus, as far as i know, constructing "use-after-free" in
Ada is possible - which is undefined behavior (and that was my point).

