pthread_cancel and stack unwinding

Thread cancellation is a bit trickier than what one might expect. The reason is resources cleanup and maintaining a consistent state. Just as an example, a cancelled thread may need to unlock its mutex objects, free up memory, close file descriptors, delete named shared memory and the list can go on.

The idiom to make this work in pthread is cleanup handlers. These handlers work in a stack fashion, and can be pushed or popped at any time during run time. In case of thread cancellation by means of a call to pthread_cancel, handlers present in the stack will be automatically executed while the unwinding process occurs.

Here we have a piece of code showing how this API works:

Don’t get distracted by the number of lines because what this code does is pretty simple. The first thread, executing main, creates a second thread and waits for it to make some progress. The second thread executes thread_main, pushes a cleanup handler, calls thread_f and waits to be cancelled. The first thread will then call pthread_cancel. During the cancellation process, the pushed cleanup handler has to be executed.

pthread_cleanup_push, used to push the cleanup handler to the stack, looks like a function. But that’s not the case in Linux x86_64 -at least-. It’s a macro, and this is how the expanded assembly looks like:

When pushing the cleanup handler, a __sigsetjmp call is made (see address 0x400ce0). Information about the context (register values) is kept in a local variable (-0x80(%rbp)). As with any setjmp/longjmp, there are two possible paths to continue execution: 1) the path taken after calling setjmp and saving a context, and 2) the path taken after a context restore (longjmp call). Which path to take will depend on the RAX register value at 0x400ce5. The first path corresponds to a non-cancellation flow and the second to a cancellation one.

Let’s analyze both flows in more detail.

Non-cancellation flow

  • two local variables contain pointers to the cleanup handler function (-0x8(%rbp)) and to the clenaup handler argument (-0x10(%rbp))
  • setjmp is called and returns 0x0
    • the current context is saved in a local variable because it might be needed in the event of a cancellation
  • __pthread_register_cancel is called
    • the saved context is now part of a chain and there is a thread-local variable set to the latest context saved. See glibc’s nptl/cleanup.c for further information.
  • thread_f is called, going deeper into the call stack
  • __pthread_unregister_cancel is called, once returning from thread_f
    • the saved context is removed from the chain and the thread-local variable is updated accordingly

Cancellation flow

  • a thread calls __pthread_cancel (glibc – nptl/pthread_cancel.c)
  • a SIGCANCEL signal is sent to the thread being cancelled
  • the thread being cancelled handles the signal with __pthread_enable_asynccancel (glibc – sysdeps/unix/sysv/linux/x86_64/cancellation.S)
  • __pthread_unwind (glibc – nptl/unwind.c) is called and the thread-local variable pointing to the last context saved is sent as an argument
  • after a few calls, execution finally reaches _Unwind_ForcedUnwind_Phase2 (libgcc – libgcc/unwind.inc). The stop function is unwind_stop (glibc – ntpl/unwind.c). For every unwinded stack frame, unwind_stop will be called. Personality functions are not used at all, contrary to C++ or other languages unwinding.
  • unwind_stop decides whether or not the last context saved corresponds to the frame being unwinded. If it does not, unwinding moves to the next frame. If it does, longjmp is called and execution in our example goes straight to 0x400ce5; but this time with an RAX value of 0x1.
  • the cleanup handler is called at 0x400cfd
  • when the cleanup handler returns, __pthread_unwind_next is called for the unwinding process to continue.

Leave a Reply

Your email address will not be published.