Segfault from Undefined Behavior

2025-03-17, Mon

1. Story

When I tried to run a harmless looking code snippet from Figure 11.5 Thread Cleanup Handler of book Advanced Programming in Unix Environment1, I kept getting this "segmentation fault" or "bus error" in macOS. This sample code demonstrates the usage of cleanup handlers for pthreads, and a simplified version is listed below for referene:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void
cleanup(void *arg)
{
  fprintf(stdout, "cleanup: %s\n", (char *)arg);
}

void *
thr_fn1(void *arg)
{
  fprintf(stdout, "thread 1 start\n");
  pthread_cleanup_push(cleanup, "thread 1 first handler");
  pthread_cleanup_push(cleanup, "thread 1 second handler");

  fprintf(stdout, "thread 1 push complete\n");
  if (arg) {
    return ((void *)1);
  }
  pthread_cleanup_pop(0);
  pthread_cleanup_pop(0);

  return ((void *)1);
}

int
main(void)
{
  int err;
  pthread_t tid1;
  void *tret;

  err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
  if (err != 0)
    fprintf(stderr, "Can't create thread 1: %s\n", strerror(err)), exit(1); 

  err = pthread_join(tid1, &tret);
  if (err !=0 )
    fprintf(stderr, "can't join with thread 1: %s\n", strerror(err)), exit(1);

  fprintf(stdout, "thread 1 exit code %ld\n", (long)tret);
  exit(0);
}

Run the code and the reslult looks like this:

thread 1 start
thread 1 push complete
cleanup: thread 1 second handler
Segmentation fault: 11

Since the error was thrown in the line of pthread_join(3), I initially thought there's something wrong with the way I invoke this library function. What makes things worse, is that the manual of these pthread functions in macOS all claim that they conform to the standard, e.g. The pthread_join() function conforms to ISO/IEC 9945-1:1996 (“POSIX.1”). So what exactly is going on?

As it turns out, those claims are indeed true. However, the POSIX.1 standard for pthread_cleanup_push and pthread_cleanup_pop includes the following words: "The effect of the use of return, break, continue, and goto to prematurely leave a code block described by a pair of pthread_cleanup_push() and pthread_cleanup_pop() functions calls is undefined." – And this undefined part depends on implementation: GNU/Linux and macOS handle it differently.

As expressed in the POSIX.1 standard and explicitly stated in the book1, the "portable way to return in between these two functions is to call pthread_exit". In order to make the code above work across different systems, the only change needed is to replace the first return ((void *)1); with pthread_exit((void *)1);. With this one line change applied, code run now results into the expected output:

thread 1 start
thread 1 push complete
cleanup: thread 1 second handler
cleanup: thread 1 first handler
thread 1 exit code 1

End of story.

2. Conclusion

This undefined behavior is a new concept to me, since I rarely saw them in my previous spec readings. I assume it will become more common when one is getting closer to system programming.

Also, I choose to put content of Figure 11.6 from the same book1 here for reference. More entries might get added here as I delve deeper into the topic.

Table 1: Comparison of process and thread primitives
Process Primitive Thread Primitive Description
fork pthread_create create a new flow of control
exit pthread_exit exit from an existing flow of control
waitpid pthread_join get exit status from flow of control
atexit pthread_cleanup_push register funtion to be called at exit from flow of control
getpid pthread_self get ID for flow of control
abort pthread_cancel request abnormal termination of flow of control

2.1. Pthread synchronization

Pthread synchronization objects are listed here as well for comparison:

Type Purpose Description
pthread_mutex_t Mutually-Exclusive Lock Lock, Unlocked
pthread_rwlock_t Shared-Exclusive Lock Read-Lock, Write-Lock, Unlocked
pthread_spin_t Spin Lock Wait for lock by spinning instead of sleeping
pthread_cond_t Condition Work with mutex and predicate
pthread_barrier_t Barrier with Count Barrier lifts only when count is met
pthread_once_t Once Indicator Ensure one-time execution of a routine
pthread_key_t Local Data Tracker Track thread specific data items

Notice the interesting terms used here? i.e. thread and spinning. I always wondered why the word spinlock is employed – until I dropped the "lock" part and think only about "spinning thread". Things start to make more sense now.

2.2. Missing Manual in Debian

Manual of a few pthread functions like pthread_mutext_init is available in the Linux manual page2 but missing from Debian GNU/Linux by default. The reason is that it is located in the "non-free" section as a separate package3 called manpages-posix-dev.

Footnotes:

1

Official site of the book: http://www.apuebook.com/code3e.html

3

Search result of manpages-posix in Debian: https://packages.debian.org/search?keywords=manpages-posix