I have a clarification: Dereferencing a null pointer in C++ doesn’t reliably crash anymore, unfortunately, and if you still believe it does, then I do not want to run your C++ code. I assume that the author understands this and is only trying to simplify. My problem is that this is already such a widespread and dangerous myth that I’m sad to see it perpetuated in an otherwise great article.
For anyone who’s wondering, I’m referencing “UB” here (which is short for Undefined Behavior, but don’t be confused by the English language meaning, it’s a precise technical term in the spec). Skipping the details, there’s a surprising (and growing) amount of situations where a null pointer access leads to silent incorrect code execution instead of a crash already, with standard compilers and CPUs. C++ programmers need to deal with that on any platform. As I’m sure the author is aware, what the WASM compilers do here is well within the spec.
Compilers following the spec is necessary, but not sufficient. Compilers are expected to be useful, and in practice, that means being able to fail reliably when spec violations occur. Let’s highlight what I personally expect, from my experience:
1. Pretty much every non-embedded CPU architecture has a page-based MMU.
2. Pretty much every compiler treats NULL as a zero value.
3. Every operating system using an MMU will leave the zero page unmapped, and load/stores at those addresses will cause a segfault.
4. If the pointer address cannot be inferred at compile time (e.g. via address of stack variables, or constant propagation), the compiler cannot optimize away the dereference.
Now WASM is a little bit different because it’s a virtual bytecode, but 99% of WASM builds use emscripten, and emscripten’s primary use case is porting C programs to the web with minimal changes, which means that in order to be useful, either emscripten (via instrumentation) or WASM (via memory protection feature) needs to be able to accommodate common target specific behaviors like memory protection.
>If the pointer address cannot be inferred at compile time (e.g. via address of stack variables, or constant propagation), the compiler cannot optimize away the dereference.
It can't optimize away the dereference, but it can optimize away any NULL checks, so for example the compiler can transform this:
if(y != NULL) {
printf("Hello");
}
int x = *y;
Into this:
printf("Hello");
int x = *y;
It's possible that you never expect "Hello" to be printed if y is NULL, since there is a clear check for NULL before the printf, and yet... because of undefined behavior and compiler optimizations, the compiler is allowed to make assumptions about runtime behavior even if that behavior precedes an undefined operation.
I replaced the printf with a simple write to a global variable, since as-is, the optimization might actually be illegal on the grounds that the printf might never return (e.g. if stdout is a blocked pipe). But none of GCC, Clang, or MSVC elide the check at max optimization level.
If you put the dereference before the null check, then all of those compilers do elide it. This is pretty easy to justify, since any program without address 0 mapped will always crash before reaching the check. This optimization did cause a brouhaha a decade ago, when it was discovered that it was being applied to the Linux kernel. At the time, it was possible for a user program to map address 0 and have that be directly dereferenceable by the kernel, and an optimized-away null pointer check made an otherwise unexploitable kernel bug exploitable. [1] Linux now passes a flag to turn the optimization off (-fno-delete-null-pointer-checks), which solves that problem without denying user programs the benefits of the optimization.
That said, it appears that Clang elides the check in the dereference-before-check version even on WebAssembly; this is probably a bug.
It's dangerous to assume that you know better than someone like Raymond Chen, the author of the article the GP linked. This is not a Clang bug, it's just the nature of UB optimizations we're talking about. :)
I've found a simple example. Code order and compiler options are very finicky with this sort of thing, but as of posting this, clang optimizes away the null dereference here entirely: https://gcc.godbolt.org/z/zjhY1W67Y
In your example, even though the behavior is undefined according to the spec, the program will still reliably segfault as long as the target it runs on has the appropriate memory protections for the zero page. The crash will occur immediately after the elided null check, so the failure is localized to the problematic area and a debugger will show the problem.
First your statement is unfortunately just untrue, the idea that any behavior can be relied upon when performing an undefined operation is why to this day we have very buggy software written in C and C++ that lead to security vulnerabilities. There is nothing reliable about undefined behavior.
Second, even if we accept for the sake of argument that your statement is true, the entire point of my example is that by the time you get to the dereference which produces a segfault, it's already too late. Any operation prior to that segfault that assumed a non-null pointer (such as the print statement) will have already been executed and produced potential side-effects. There is nothing local about this, it's simply that for the sake of an example I wrote a small code snippet, but in the article I linked to there are examples of very non-local bugs both spatially and temporally that would not be something you could reliably track down in a debugger.
It won't occur immediately after the elided check, that's exactly the problem. It will allow the I/O to occur first. Now imagine if that was a database update.
WASM is a little more different than that, because 0 is a valid memory address -- the beginning of the heap.
As you would expect this has all kinds of wonderful ramifications for code that assumes 0 is nullptr.
Note that the constant 0 being the null pointer is not an assumption but it is guaranteed by the standard. Whether that maps to address 0 it is an implementation detail, but any implementation that doesn't do that is looking for trouble.
Yes - as mentioned in the article, 0 being a valid memory address is an unexpected gotcha. My point is that, even though the C spec allows for such an environment, it still causes lots of problems in practice, so it should be addressed.
It should be noted that such an environment has been extremely common back when C was designed, and for a couple decades thereafter. This isn't something new that has just been sprung up unexpected on C coders.
Dereferencing a null pointer in C++ doesn't reliably crash, but dereferencing a null pointer in GCC and Clang C++ compiled with -fsanitize=null does reliably crash, with useful error messages, and it is documented to be so.
You are not required to write standard C++. Making uses of implementation defined features is a valid engineering choice.
> Dereferencing a null pointer in C++ doesn’t reliably crash anymore
Anymore? I would say the opposite: these days it tends to crash more reliably if anything, as the size of the redzones increases. Even on Windows 9x 0 was a perfectly valid address.
0 is usually a 'valid' address. What is at that addr though can have some interesting bits that may or may not contain valid instructions/data. Now on some processors you can lock it out so it seg faults if you access it though.
Null pointer derefs in C/C++ reliable crash on anything that isn't an embedded device. The times when the compiler "avoids" it are just times when the deref isn't actually used, and thus dead code eliminated. Which doesn't tend to result in incorrect code execution, but rather crashes later on that are harder to understand (like having "this" end up being null inside a member function)
While you are right that in theory a C or C++ compiler is free to do anything when Undefined Behavior is specified in the standard, any careful programmer should always compile all C/C++ programs with options like "-fsanitize=undefined -fsanitize-undefined-trap-on-error".
This guarantees that null pointers or out-of-bounds array accesses will crash the program.
"Non-contrived" is a matter of definition; it's almost necessary to give toy examples, but I hope you can be convinced that the same structural pattern can appear in real code.
I hope Clang on x86_64 is common enough for you to make my point. In this example, Clang reasons (backwards in time) that the if couldn't have been taken and dereferences whatever was in p, instead of dereferencing null: https://gcc.godbolt.org/z/zjhY1W67Y
For anyone who’s wondering, I’m referencing “UB” here (which is short for Undefined Behavior, but don’t be confused by the English language meaning, it’s a precise technical term in the spec). Skipping the details, there’s a surprising (and growing) amount of situations where a null pointer access leads to silent incorrect code execution instead of a crash already, with standard compilers and CPUs. C++ programmers need to deal with that on any platform. As I’m sure the author is aware, what the WASM compilers do here is well within the spec.