Ch19.2: std::abort(), std::terminate() and __builtin_trap()
1. Some Failures Are Not Errors
An error is something a caller can do something about: the file was not there, the peer
reset the connection, the text was not a number. Those travel on the
throws channel, and that is what the rest of this chapter is about.
There is a second kind of failure, and the distinction is the most important rule in this book: a failure that leaves the abstract machine unable to continue is not an error, it is corruption, and the only correct response is to stop the process. Three things fall in that category:
-
Allocation failure. If
mallocreturnsnullptr, or the kernel refuses to give the process another page for its stack, there is no memory to report the error with, no memory to unwind with, and no memory to log with. Every avenue out is closed. - Size overflow. A request of 240 elements is not a resource problem, it is a caller that computed a size wrong. Reserving it cannot succeed on any machine that exists, and slicing the request into smaller ones would only turn a bug into a slow bug.
-
A broken invariant. A container's bounds were not checked, a
switchreached an impossible case, a length field disagreed with the buffer. Continuing means running code that no longer has a meaning.
Continuing from any of those is how a program becomes a security vulnerability instead of a crash: it keeps running with a corrupted heap, a corrupted size, or a corrupted index. The professional answer is to die immediately, at the point of detection, with the operating system recording why. That is what fast_io does, everywhere, and this page shows the three spellings of it and the one you should use.
2. The Three Spellings
#include <fast_io.h>
#include <cstdlib>
int main()
{
::fast_io::io::perr("about to die\n");
#if defined(USE_ABORT)
::std::abort();
#elif defined(USE_TERMINATE)
::std::terminate();
#else
::fast_io::fast_terminate();
#endif
}
Three builds of the same program, each selecting a different death:
$ ./crash -DUSE_ABORT
about to die
Aborted (core dumped)
$ ./crash -DUSE_TERMINATE
about to die
Aborted (core dumped)
$ ./crash
about to die
Illegal instruction (core dumped)
Note what is not in the output for the third case: the shell says it died from a
signal, and the signal is SIGILL (exit status 132 = 128 + 4), not
SIGABRT (134 = 128 + 6). fast_terminate() is not
abort with another name; it compiles to a single instruction:
401169: ud2
40116b: nopl 0x0(%rax,%rax,1)
ud2 is an architecturally guaranteed illegal instruction. The CPU raises the
trap itself, so no library call, no signal frame, no handler and no unwinder are involved,
and the instruction is emitted inline wherever the check fails.
::std::abort() |
::std::terminate() |
::fast_io::fast_terminate() |
|
|---|---|---|---|
| What it needs | libc | libc++ / libstdc++ and a terminate handler | Nothing |
| Signal / status | SIGABRT / 134 |
SIGABRT / 134 (its handler aborts) |
SIGILL / 132 |
| Usable freestanding, in a kernel, in a bootloader | No | No | Yes: __builtin_trap with a fallback to
__builtin_abort and then ::std::abort |
Runs destructors or atexit handlers |
No | No | No |
| When you reach for it | Legacy code, tests | You cannot choose it: the language calls it for you (an exception escaping
noexcept, a throw during unwinding, no handler for a legacy
exception) |
Every invariant check you write, and everything fast_io checks for you |
::std::terminate() is worth knowing but useless as a tool: you almost never
decide to call it. It is what the runtime calls when the legacy exception machinery has
nowhere to put an exception — which
Ch19.8 counts as one of the
defects of that machinery.
3. What fast_io Crashes On
The default allocator of every fast_io container, and every internal size computation,
treats the three cases above as corruption and calls
::fast_io::fast_terminate() on the spot. Here is a real one: reserving
240 ints, which is 8 TiB, on a machine with a few gigabytes.
#include <fast_io.h>
#include <fast_io_dsal/vector.h>
int main()
{
::fast_io::io::print(::fast_io::c_stdout(), "reserving 2^40 ints\n");
::fast_io::operations::output_stream_buffer_flush(::fast_io::c_stdout());
::fast_io::vector<int> v;
v.reserve(static_cast<::std::size_t>(1) << 40);
::fast_io::io::print("reserved ", v.capacity(), " ints\n");
}
reserving 2^40 ints
Illegal instruction (core dumped)
Note the second line of the transcript: the shell reports the signal, because the process is gone. There is no error value, no return code, no exception, and nothing to check. The program did not fail to reserve; it went out of existence at the fence.
That is the intended behaviour of ::fast_io::vector,
::fast_io::string and every other container in the library, and it is a
deliberate difference from std::vector. There is no try around an
allocation in your program, and there must not be: a program that can survive
bad_alloc is a program that has to be correct in a state where every
allocation can fail, including the ones inside its own error reporting. Nobody audits that,
so nobody is correct at it.
When you want the opposite policy — you are writing a library and your caller should get a herbception instead of a dead process — that is a per-allocator decision in fast_io, not a per-call decision, and it is Ch19.9.
4. The Output You Lose
A trap does not flush anything. Buffered output that is still in the process's user-space buffer is discarded with the process, so the last thing your program printed before dying may never be seen. The two streams behave differently, and the difference is worth measuring once:
#include <fast_io.h>
#include <fast_io_dsal/vector.h>
int main()
{
::fast_io::io::print("about to reserve 2^40 ints\n"); // buffered stdout
::fast_io::io::perr("about to reserve 2^40 ints\n"); // unbuffered stderr
::fast_io::vector<int> v;
v.reserve(static_cast<::std::size_t>(1) << 40);
}
about to reserve 2^40 ints
Illegal instruction (core dumped) <- the perr line; the print line is gone
$ ./lose 2>/dev/null
Illegal instruction (core dumped) <- neither line: stdout never flushed
The print line went into the buffered stdout stream and died with the process;
the perr line went to stderr, which is unbuffered, and survived. So the rule
is: the last words before a deliberate crash go to perr
(Ch11.1 explains why the two streams differ), and
everything else can stay on print.
5. What You Must Not Do Instead
-
Do not return
nullptr. A null pointer moves the failure from the allocator, which knows exactly what happened, to a caller that will find out by dereferencing it. Everymalloccheck in a legacy codebase is a place where an impossible situation was forwarded instead of handled (Ch18.1). -
Do not use
::std::nothrow. It is the same forwarding with a friendlier name. - Do not keep a counter of live objects to "detect leaks" after a corruption. The corruption already happened; the counter is now also wrong.
- Do not catch it. There is nothing to catch: the trap is not an exception, and a signal handler around it would only resume a program that has already established it cannot be correct.
-
Do not report it as an error and continue up the stack. That is the
design this book rejects: it makes every function's contract "may fail, even the ones
that cannot fail", and it is how a
vectorends up with abad_allocinside a message formatter inside a crash reporter.
Die at the fence, with a message on stderr if you can afford one, and let the core dump or the crash reporter tell you where. A program that stops is diagnosable. A program that continues after corruption is a mystery, a CVE, or both.