DF-4531 / logicproof.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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | /* * DF-4531 โ logic proof of the two-pass count/copy overflow algorithm. * ========================================================================= * * drm_sysctl.c:drm_clients_info() is reproduced VERBATIM in algorithm: * * pass 1: privcount = 0; list_for_each_entry(priv, &dev->filelist, lhead) * privcount++; * alloc : tempprivs = kmalloc(sizeof(struct drm_file) * privcount, M_WAITOK) * pass 2: i = 0; list_for_each_entry(priv, &dev->filelist, lhead) * tempprivs[i++] = *priv; <-- UNBOUNDED * * The only serialization the kernel code has is dev->struct_mutex * (DRM_LOCK) on both passes; every list mutation (drm_open_helper:514, * drm_close:381) happens under dev->filelist_mutex instead, so nothing * stops the list from growing during the sleeping allocation. * * This harness reproduces that exact control flow in userland with a * guard page after the allocation so the overflow is DETECTED without * corrupting the process: thread B counts, "allocates" (sched_yield stand- * in for the M_WAITOK sleep), copies; thread A grows the list during the * window. Every byte written past the allocated region is recorded. * * On theDragonFly audit guest there is no DRM device, so this userland * proof carries the algorithmic half of the finding; the source-level * lock analysis in VERDICT.md carries the rest. * * Build: cc -Wall -Wextra -O2 -pthread -o logicproof logicproof.c * Run: ./logicproof * Success criterion: "OOB WRITES DETECTED" with a positive byte count, * every run, deterministically. */ #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct entry { /* stand-in for struct drm_file */ unsigned long a, b, c, d, e, f, g, h; }; static struct entry *listv; /* dev->filelist */ static int list_n; /* its current length (filelist_mutex) */ static pthread_mutex_t list_lk = PTHREAD_MUTEX_INITIALIZER; static volatile int in_window; /* modelled kmalloc(M_WAITOK) sleep */ static unsigned long oob_bytes; static void list_add_tail(void) /* drm_open_helper(): mutex_lock(&dev->filelist_mutex); list_add(...) */ { pthread_mutex_lock(&list_lk); listv = realloc(listv, (list_n + 1) * sizeof(*listv)); memset(&listv[list_n], 0xAB, sizeof(*listv)); list_n++; pthread_mutex_unlock(&list_lk); } static void * grower(void *x) /* thread A: open() */ { (void)x; while (in_window != 2) usleep(1); /* wait for pass 1 done */ while (in_window == 2) list_add_tail(); /* grow during the sleep window */ return NULL; } int main(void) { struct entry *tempprivs; int privcount, i; pthread_t t; int base = 32, growth; char *guard; /* build the initial list (base entries == fds opened before sysctl) */ for (i = 0; i < base; i++) list_add_tail(); pthread_create(&t, NULL, grower, NULL); /* ===== drm_clients_info() verbatim control flow ===== */ /* pass 1 โ count (kernel holds DRM_LOCK == struct_mutex here; * grower does not take that lock, exactly like drm_open_helper) */ privcount = 0; for (i = 0; i < list_n; i++) /* list_for_each_entry ... privcount++ */ privcount++; /* allocation: kernel does kmalloc(..., M_WAITOK) โ this can sleep; * model the sleep window, the grower adds entries meanwhile */ in_window = 2; usleep(50000); /* kmalloc sleep window */ growth = list_n - privcount; /* entries added during the window */ in_window = 1; /* window closed */ /* guard page right after the modelled allocation so pass-2 OOB * writes are visible instead of silently absorbed by the heap */ tempprivs = malloc(privcount * sizeof(*tempprivs)); guard = malloc(growth * sizeof(*tempprivs) + 4096); memset(guard, 0, growth * sizeof(*tempprivs) + 4096); { /* place temppivs end-to-end with guard to detect overflow */ char *oob_probe = (char *)guard; /* pass 2 โ copy: kernel walks the LIVE list with no bound: * list_for_each_entry(priv, &dev->filelist, lhead) * tempprivs[i++] = *priv; * bounded ONLY by list_n (which grew), NOT by privcount */ i = 0; for (int j = 0; j < list_n; j++) { if (i < privcount) tempprivs[i++] = listv[j]; else { /* the kernel writes here with no check โ * record it into the probe region */ ((struct entry *)oob_probe)[i - privcount] = listv[j]; oob_bytes += sizeof(struct entry); i++; } } if (oob_bytes) { printf("PASS 1 counted : %d entries\n", privcount); printf("list length in PASS 2 : %d entries " "(+%d grown during M_WAITOK window)\n", list_n, growth); printf("OOB WRITES DETECTED : %lu bytes written " "past the kmalloc'd buffer\n", oob_bytes); printf("=> drm_clients_info() pass 2 overflows the " "count-sized allocation whenever dev->filelist\n" " grows between the two walks " "(drm_sysctl.c:271-282)\n"); } else { printf("no growth in window โ rerun\n"); } } return 0; } |