Newer
Older
minerva / Tests / Kernel / nanosleep-race-outbuf-munmap.cpp
@minerva minerva on 13 Jul 3 KB Initial commit
/*
 * Copyright (c) 2020, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <assert.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

static void signal_printer(int)
{
    // no-op
}

typedef struct yank_shared_t {
    timespec* remaining_sleep;
    // TODO: Be nice and use thread ID
    // pthread_t sleeper_thread;
} yank_shared_t;

static void* yanker_fn(void* shared_)
{
    yank_shared_t* shared = static_cast<yank_shared_t*>(shared_);

    timespec requested_sleep = { 1, 0 };
    int rc = clock_nanosleep(CLOCK_MONOTONIC, 0, &requested_sleep, nullptr);
    if (rc != 0) {
        printf("Yanker: Failed during sleep: %d\n", rc);
        return nullptr;
    }

    delete shared->remaining_sleep; // T2

    // Send SIGUSR1.

    // Use pthread:
    // pthread_kill(somewhere, SIGUSR1);
    // But wait!  pthread_kill isn't implemented yet, and therefore causes
    // a linker error.  It also looks like the corresponding syscall is missing.

    // Use normal IPC syscall:
    // kill(getpid(), SIGUSR1);
    // But wait!  If destination_pid == own_pid, then the signal is sent
    // to the calling thread, *no matter what*.

    // So, we have to go the very ugly route of fork():
    // (Thank goodness this is only a demo of a kernel bug!)
    pid_t pid_to_kill = getpid();

    pid_t child_pid = fork();
    if (child_pid < 0) {
        printf("Yanker: Fork failed: %d\n", child_pid);
        pthread_exit(nullptr); // See below
    }

    if (child_pid > 0) {
        // Success.  Terminate quickly.  T3
        // FIXME: LibPthread bug: returning during normal operation causes nullptr deref.
        // Workaround: Exit manually.
        pthread_exit(nullptr);
    }

    // Give parent *thread* a moment to die.
    requested_sleep = { 1, 0 };
    rc = clock_nanosleep(CLOCK_MONOTONIC, 0, &requested_sleep, nullptr);
    if (rc != 0) {
        printf("Yanker-child: Failed during sleep: %d\n", rc);
        return nullptr;
    }

    // Prod the parent *process*
    kill(pid_to_kill, SIGUSR1); // T4

    // Wait a moment, to ensure the log output is as well-separated as possible.
    requested_sleep = { 2, 0 };
    rc = clock_nanosleep(CLOCK_MONOTONIC, 0, &requested_sleep, nullptr);
    if (rc != 0) {
        printf("Yanker-child: Failed during after-sleep: %d\n", rc);
        return nullptr;
    }

    pthread_exit(nullptr);
    assert(false);
    // FIXME: return nullptr;
}

int main()
{
    // Chronological order:
    // T0: Main thread allocates region for the outvalue of clock_nanosleep
    // T1: Main thread enters clock_nanosleep
    // T2: Side thread deallocates that region
    // T3: Side thread dies
    // T4: A different process sends SIGUSR1, waking up the main thread,
    //     forcing the kernel to write to the deallocated Region.

    // I'm sorry that both a side *thread* and a side *process* are necessary.
    // Maybe in the future this test can be simplified, see above.

    yank_shared_t shared = { nullptr };
    shared.remaining_sleep = new timespec({ 0xbad, 0xf00d }); // T0

    pthread_t yanker_thread;
    int rc = pthread_create(&yanker_thread, nullptr, yanker_fn, &shared);
    if (rc != 0) {
        perror("pthread");
        printf("FAIL\n");
        return 1;
    }

    // Set an action for SIGUSR1, so that the sleep can be interrupted:
    signal(SIGUSR1, signal_printer);

    // T1: Go to sleep.
    timespec const requested_sleep = { 3, 0 };
    rc = clock_nanosleep(CLOCK_MONOTONIC, 0, &requested_sleep, shared.remaining_sleep);
    // Now we are beyond T4.

    if (rc == 0) {
        // We somehow weren't interrupted.  Bad.
        printf("Not interrupted.\n");
        printf("FAIL\n");
        return 1;
    }

    // nanosleep was interrupted and the kernel didn't crash.  Good!
    printf("PASS\n");
    return 0;
}