Description
Because OGCancelThread()
immediately frees the thread handle (pthread_t
or the Windows handle), there's no way to reliably cancel a thread and wait for it to terminate using os_generic.h
. Just calling pthread_cancel()
does not also join the thread, so it may continue running even after OGCancelThread
returns. The pthread_cancel
manpage confirms this:
After a canceled thread has terminated, a join with that thread using pthread_join(3) obtains PTHREAD_CANCELED as the thread's exit status. (Joining with a thread is the only way to know that cancelation has completed.)
The effect of this is a race condition when canceling a thread that may cause a crash or invoke undefined behavior. Threads that encounter a cancellation point frequently will usually exit by the time the statement after OGCancelThread()
executes. Threads that are slower to reach a cancellation point (e.g. one with a tight loop and no function calls) may continue running after OGCancelThread()
returns.
This program consistently reproduces this behavior. Invoking ./thread
will run a 'slow' thread, which should almost always trip the address sanitizer; invoking ./thread f
will run a 'fast' thread, which should almost always exit normally.
// Build with:
// wget https://raw.githubusercontent.com/cntools/rawdraw/master/os_generic.h
// cc -o thread thread.c -fsanitize=bounds-strict -fsanitize=address -O0
// Run:
// To run the 'slow thread' (almost always trips address sanitizer and errors)
// ./thread s
//
// To run the 'fast thread' (exits successfully almost always)
// ./thread f
#include "os_generic.h"
#include <stdio.h>
#include <stdlib.h>
void* fastThread(void* arg)
{
while (1)
{
// Do a proper sleep which should create a cancellation point
OGUSleep(1);
*(int*)arg = *(int*)arg + 1;
}
}
void* slowThread(void* arg)
{
while (1)
{
// Spin-wait to sleep while avoiding creating a cancellation point
for (int i = 0; i < 100000; i++);
*(int*)arg = *(int*)arg + 1;
// Sleep anyway, but only after incrementing
OGUSleep(1);
}
}
int main(int argc, char** argv)
{
// Check the arg, 'f' to run fast thread, otherwise slow thread
int slow = (argc < 2 || *argv[1] != 'f');
// Allocate a value to be used by the thread
// The thread will increment this at the end of every loop
int* val = malloc(sizeof(int));
*val = 0;
og_thread_t thread = OGCreateThread(slow ? slowThread : fastThread, val);
printf("Running %s thread\n", slow ? "slow" : "fast");
puts("Press Enter to cancel thread\n");
// Wait for a character, usually doesn't happen until enter is pressed anyway
getchar();
// Try to cancel the thread!
OGCancelThread(thread);
// Save the result before we free it. Not really necessary but it's nce to see what it is.
int result = *val;
// Now, free the value. If the thread is still running, we'll see an invalid dereference soon!
free(val);
printf("Done! val = %d\n", result);
// Sleep for 100ms, just to give address sanitizer time to print a message
OGUSleep(100000);
return 0;
}