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:

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

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.