Skip to content

Cannot cancel then join a thread using os_generic.h #124

@dylwhich

Description

@dylwhich

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;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions