DragonFlyBSD Kernel Audit
DF-3029 / lostwakeup.c
← back to finding ↓ download raw
/*
 * DF-3029 — fuse_io_thread lost-wakeup stress.
 *
 * fuse_io_thread (fuse_vnops.c:2013-2023) does:
 *
 *     while (fmp->dead == 0) {
 *         tsleep(&fmp->helper_td, 0, "fuse_wio", 0);   <-- no interlock
 *         spin_lock(&fmp->helper_spin);
 *         while ((bio = TAILQ_FIRST(&fmp->bioq)) != NULL) { ... }
 *         spin_unlock(&fmp->helper_spin);              <-- window opens
 *     }                                                 <-- tsleep (lost)
 *
 * A strategy() producer that inserts a bio and calls wakeup() between
 * the helper's final spin_unlock and its tsleep loses the wakeup; the
 * bio is then stranded and its waiter (user read / pageout) blocks
 * forever.  The correct pattern (tsleep_interlock + recheck + tsleep
 * PINTERLOCKED) is used by fuse_ipc_wait but not here.
 *
 * This program hammers the bio queue with concurrent random-offset reads
 * from many threads; a parent monitors progress.  If every worker's
 * progress counter freezes while the workers are still alive and blocked
 * in read(), the lost wakeup has (probably) been hit.
 *
 * usage: lostwakeup <file> <threads> <seconds>
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <pthread.h>

static volatile unsigned long progress;
static volatile int stop_flag;
static volatile int blocked_reads;

struct warg {
    const char *path;
    unsigned seed;
};

static void *
worker2(void *arg)
{
    struct warg *w = arg;
    char buf[4096];
    unsigned seed = w->seed;
    int fd;

    fd = open(w->path, O_RDONLY);
    if (fd < 0) { perror("worker open"); return NULL; }
    while (!stop_flag) {
        off_t off = (off_t)(rand_r(&seed) % (16u << 20)) & ~4095UL;
        ssize_t n;
        blocked_reads++;
        n = pread(fd, buf, sizeof(buf), off);
        blocked_reads--;
        if (n < 0) { perror("pread"); break; }
        __sync_fetch_and_add(&progress, 1);
    }
    close(fd);
    return NULL;
}

int
main(int argc, char **argv)
{
    const char *path;
    int nthreads, seconds, i;
    struct warg wargs[64];
    pthread_t th[64];
    unsigned long last_prog = 0, stable_secs = 0;

    if (argc != 4) {
        fprintf(stderr, "usage: %s <file> <threads> <seconds>\n", argv[0]);
        return 2;
    }
    path = argv[1];
    nthreads = atoi(argv[2]);
    seconds = atoi(argv[3]);
    if (nthreads > 64) nthreads = 64;

    for (i = 0; i < nthreads; i++) {
        wargs[i].path = path;
        wargs[i].seed = 0x1234 + i * 7919;
        pthread_create(&th[i], NULL, worker2, &wargs[i]);
    }

    for (i = 0; i < seconds; i++) {
        sleep(1);
        unsigned long p = progress;
        if (p == last_prog) {
            stable_secs++;
            fprintf(stderr,
                "t=%3ds NO PROGRESS (progress=%lu blocked=%d)\n",
                i, p, blocked_reads);
            if (stable_secs >= 20) {
                printf("STALL_DETECTED progress=%lu after %ds "
                    "(possible lost wakeup)\n", p, i);
                stop_flag = 1;
                for (i = 0; i < nthreads; i++)
                    pthread_join(th[i], NULL);
                return 3;
            }
        } else {
            stable_secs = 0;
            if ((i % 10) == 0)
                fprintf(stderr, "t=%3ds progress=%lu\n", i, p);
        }
        last_prog = p;
    }
    stop_flag = 1;
    for (i = 0; i < nthreads; i++)
        pthread_join(th[i], NULL);
    printf("NO_STALL progress=%lu in %ds\n", progress, seconds);
    return 0;
}