Skip to content

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

Open
@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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions