An excellent reference (probably the best one) that can be consulted for this type of problem is The Linux Programming Interface, which includes 3 full chapters on the topic of signals:
- Signals: Fundamental Concepts
- Signals: Signal Handlers
- Signals: Advanced features
These chapters include example code as well as diagrams and clear explanations.
(A free pdf of the book can easily be found online.)
See also: Binary Obfuscation Using Signals
To your questions:
What would be the most "trap-able" segfault ? a call to a null function pointer ? a double-free ? ... ?
A signal handler, should it exist, will be called by the kernel on the basis of the signal (specifically, its number as defined in <signal.h>
), rather than the specific event that triggered the signal. This means that you are free to decide what kind of invalid memory reference to make in order to trigger a segmentation fault. Dereferencing a null pointer is probably the most reliable, since it guarantees program behavior will be deterministic, as a segmentation fault will always occur irrespective of stack contents or the layout of the process in memory.
What can i do in my signal handler ? It seems there are some harsh condition (reentrancy, async-signal-safe function,...).
You have several options. But first, a bit about non-reentrant library functions:
Functions can also be nonreentrant if they use static data structures for their
internal bookkeeping. The most obvious examples of such functions are the members of the stdio
library (printf()
, scanf()
, and so on), which update internal data structures for buffered I/O. Thus, when using printf()
from within a signal handler, we may sometimes see strange output—or even a program crash or data corruption—if the handler interrupts the main program in the middle of executing a call to printf()
or another stdio
function.
The important part is the last sentence. Implementing an exception handler for SIGSEGV
may result in unintended signal handler behavior if non-reentrant library functions are called within the exception handler, since it may be possible to trigger a segmentation fault outside of the specific conditions you anticipate. For example, when user input is read into a buffer via scanf
, unless bounds checking is properly implemented, a buffer overflow due to 1000 'A's being entered can result in SIGSEGV
being sent to the process, which will then trigger the exception handler during the execution of an stdio
function. If the signal handler also calls an stdio
function, undefined behavior may occur.
The most interesting approach for your case would probably be changing the uc_mcontext.gregs[REG_RIP]
(RIP here is the x86-64 instruction pointer) value in the signal handler's context
struct to point to a function somewhere else in the program. After the signal handler finishes, program execution would resume at that function. Alternatively, the uc_mcontext.gregs[REG_RIP]
value can incremented to skip/jump over the instructions that caused the signal handler to execute in the first place. This means that the signal handler could be designed to simply jump to a different location in the program upon receiving SIGSEGV
. This approach, (or alternatively, executing a nonlocal goto), eliminates the need for any I/O to be performed by the signal handler (no need for printf()
, etc.). The drawback is that this approach is architecture-dependent. Examples illustrating this technique can be found in the following article: Linux - Writing Fault Handlers. Some related example code can also be found here: In a signal handler, how to know where the program is interrupted?
Note that since it is possible for a process to send a signal to itself, it is not necessary to choose SIGSEGV
as the handled signal; getpid()
and kill()
can be used to send some other signal to the process in order to trigger the signal handler.
Additionally, it is possible to execute a non-local goto from within the exception handler itself via functions sigsetjmp()
and siglongjmp()
. Unlike using uc_mcontext.gregs[XXX]
to modify the instruction pointer, this approach appears to be portable:
In general, it is preferable to write simple signal handlers. One important reason for this is to reduce the risk of creating race conditions. Two common designs for signal handlers are the following:
- The signal handler sets a global flag and exits. The main program periodically
checks this flag and, if it is set, takes appropriate action. (If the main program
cannot perform such periodic checks because it needs to monitor one or more
file descriptors to see if I/O is possible, then the signal handler can also write a single byte to a dedicated pipe whose read end is included among the file
descriptors monitored by the main program. We show an example of this technique in Section 63.5.2.)
- The signal handler performs some type of cleanup and then either terminates
the process or uses a nonlocal goto (Section 21.2.1) to unwind the stack and
return control to a predetermined location in the main program.
[Executing a nonlocal goto] provides a way to recover after delivery of a signal caused by a hardware exception (e.g., a memory access error), and also allows us to catch a signal and return control to a particular point in a program. For example, upon receipt of a SIGINT signal (normally generated by typing Control-C), the shell performs a nonlocal goto to return control to its main input loop (and thus read a new command).
Supplementary info:
A signal is a notification to a process that an event has occurred. Signals are sometimes described as software interrupts. Signals are analogous to hardware interrupts in that they interrupt the normal flow of execution of a program; in most cases, it is not possible to predict exactly when a signal will arrive.
One process can (if it has suitable permissions) send a signal to another process.
In this use, signals can be employed as a synchronization technique, or even as a
primitive form of interprocess communication (IPC). It is also possible for a process to send a signal to itself. However, the usual source of many signals sent to a process is the kernel.
Reference: The Linux Programming Interface, chapters 20 and 21