⬢ DragonFlyBSD Kernel Audit
DF-5276 / vpd_harness.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
/*-
 * vpd_harness.c -- driver for the verbatim-extracted VPD parser.
 *
 * Modes (argv[1]):
 *   t1  DF-5276: zero-length VPD identifier string -> kmalloc(1) then an
 *                unbounded sequential device-controlled write (guard page
 *                catches it at a precisely known offset).
 *   t2  DF-5277 + DF-5279: drive the real parser into the error cleanup
 *                after a COMPLETED VPD-R section and print the resulting
 *                cfg.vpd state (vpd_ros==NULL, vpd_rocnt stale != 0) that
 *                crashes pci_freecfg()/pci_get_vpd_readonly_method();
 *                then show the readonly lookup always fails (missing break).
 *   t3  DF-5278: VPD-R item whose length is smaller than its keyword header
 *                -> `remain` goes negative -> parse can never terminate;
 *                allocations grow without bound until the simulated device
 *                is capped (a real malicious device never stops).
 *   t2a DF-5278(a): oversize `remain` -> "invalid VPD data" is printed but
 *                the parser CONTINUES (the state=-1 is overwritten).
 *   t4  baseline: a well-formed VPD parses correctly (harness fidelity).
 */
#include "vpd_shim.h"
#include "vpd_structs.h"	/* VERBATIM pcivar.h:58-78 VPD structures */

/* map the kernel config struct onto a subset (parser touches only these) */
struct shim_pcicfg {
	uint32_t	domain;
	uint8_t		bus;
	uint8_t		slot;
	uint8_t		func;
	struct pcicfg_vpd vpd;
};
#define pcicfgregs	struct shim_pcicfg
struct shim_devinfo {
	struct shim_pcicfg cfg;
};
#define pci_devinfo	shim_devinfo
#define device_get_ivars(x)	((struct shim_devinfo *)(void *)(x))
#define device_get_parent(x)	((device_t)(x))
extern int	shim_bootverbose;
#define bootverbose	shim_bootverbose

#include "vpd_extracted.h"

int shim_bootverbose = 1;
uint8_t	sim_stream[1 << 20];
size_t	sim_len;
size_t	sim_repeat_off, sim_repeat_len;
int	sim_endless;
size_t	sim_serve_cap = (size_t)-1;
size_t	sim_reads, sim_addr_writes;

size_t	sh_nalloc, sh_nrealloc, sh_bytes, sh_maxblk;
const char *sh_phase = "init";

static struct sh_blk *sh_blocks;
static size_t sh_pagesz;

static uint8_t sim_byte(size_t off)
{
	if (off < sim_len)
		return (sim_stream[off]);
	if (sim_endless && sim_repeat_len > 0)
		return (sim_stream[sim_repeat_off +
		    (off - sim_len) % sim_repeat_len]);
	return (0xff);
}

static int sim_off;		/* latched VPD offset */
static int sim_stopped;		/* device refuses to complete */

void
sim_reset(void)
{
	sim_len = sim_repeat_off = sim_repeat_len = 0;
	sim_endless = 0;
	sim_serve_cap = (size_t)-1;
	sim_reads = sim_addr_writes = 0;
	sim_off = 0;
	sim_stopped = 0;
}

uint32_t
sim_read_config(int reg, int width)
{
	sim_reads++;
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_DATA) {
		uint32_t v = 0;
		int i, sh = 0;

		for (i = 0; i < 4; i++) {
			v |= (uint32_t)sim_byte((size_t)sim_off + i) << sh;
			sh += 8;
		}
		return (v);
	}
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_ADDR)
		return (sim_stopped ? (uint32_t)(sim_off & 0x7fff) :
		    (uint32_t)(sim_off | 0x8000));
	return (0xffffffff);
}

void
sim_write_config(int reg, uint32_t val, int width)
{
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_ADDR) {
		sim_addr_writes++;
		sim_off = val & 0x7fff;
		if ((size_t)sim_off >= sim_serve_cap ||
		    (!sim_endless && (size_t)sim_off >= sim_len &&
		    sim_repeat_len == 0))
			sim_stopped = 1;	/* cap reached / stream over */
		else
			sim_stopped = 0;	/* completes instantly */
	}
}

/* ---------------- allocator ---------------- */

void *
sh_kmalloc(size_t sz, int flags)
{
	struct sh_blk *b;

	if (sh_pagesz == 0)
		sh_pagesz = (size_t)sysconf(_SC_PAGESIZE);
	b = calloc(1, sizeof(*b));
	b->sz = sz;
	b->mapsz = ((sz + sh_pagesz - 1) / sh_pagesz + 1) * sh_pagesz;
	b->base = mmap(NULL, b->mapsz, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE | MAP_ANON, -1, 0);
	if (b->base == MAP_FAILED) {
		printf("shim: mmap failed\n");
		exit(70);
	}
	if (mprotect((char *)b->base + b->mapsz - sh_pagesz, sh_pagesz,
	    PROT_NONE) != 0) {
		printf("shim: mprotect failed\n");
		exit(70);
	}
	b->p = b->base;
	if (flags & M_ZERO)
		memset(b->p, 0, sz);
	b->next = sh_blocks;
	sh_blocks = b;
	sh_nalloc++;
	sh_bytes += sz;
	if (sz > sh_maxblk)
		sh_maxblk = sz;
	return (b->p);
}

void *
sh_krealloc(void *p, size_t sz)
{
	struct sh_blk *b;
	void *np;

	np = sh_kmalloc(sz, M_ZERO);
	if (p != NULL) {
		for (b = sh_blocks; b != NULL; b = b->next) {
			if (b->p == p) {
				memcpy(np, p, b->sz < sz ? b->sz : sz);
				sh_kfree(p);
				break;
			}
		}
	}
	sh_nrealloc++;
	/* keep accounting honest: the array growth is what matters */
	return (np);
}

void
sh_kfree(void *p)
{
	struct sh_blk **pp, *b;

	if (p == NULL)
		return;
	for (pp = &sh_blocks; (b = *pp) != NULL; pp = &b->next) {
		if (b->p == p) {
			*pp = b->next;
			munmap(b->base, b->mapsz);
			free(b);
			return;
		}
	}
}
struct sh_blk *
sh_find_block(const void *addr)
{
	struct sh_blk *b;

	for (b = sh_blocks; b != NULL; b = b->next) {
		if ((const char *)addr >= (const char *)b->base &&
		    (const char *)addr < (const char *)b->base + b->mapsz)
			return (b);
	}
	return (NULL);
}

/* ---------------- SIGSEGV reporting ---------------- */

static void
sh_segv(int sig, siginfo_t *si, void *uc)
{
	struct sh_blk *b;

	fflush(stdout);
	fprintf(stderr, "\n*** SIGSEGV writing %p during phase '%s' ***\n",
	    si->si_addr, sh_phase);
	b = sh_find_block(si->si_addr);
	if (b != NULL)
		fprintf(stderr, "    faulting allocation: base=%p kmalloc size=%zu "
		    "(guard page at +%zu)\n"
		    "    => %zd bytes were written past a %zu-byte "
		    "kmalloc allocation\n",
		    b->base, b->sz, b->mapsz - sh_pagesz,
		    (char *)si->si_addr - (char *)b->base, b->sz);
	else
		fprintf(stderr, "    (fault outside any tracked allocation)\n");
	fflush(stderr);
	_exit(77);
}

/* ---------------- tests ---------------- */

static struct shim_pcicfg cfg;
static struct shim_dev pcib_dev, child_dev;

static void
fresh_cfg(void)
{
	memset(&cfg, 0, sizeof(cfg));
	cfg.domain = 0;
	cfg.bus = 2;
	cfg.slot = 3;
	cfg.func = 1;
	cfg.vpd.vpd_reg = SIM_VPDBASE;
	cfg.vpd.vpd_cached = 0;
	sim_reset();
}

static uint8_t
pat(int i)
{
	return ((uint8_t)((i * 31) + 7));
}

static void
t1(void)
{
	size_t n = 0, i;

	sh_phase = "t1: zero-length VPD identifier string (DF-5276)";
	fresh_cfg();
	sim_stream[n++] = 0x82;		/* long-form item, name 0x2 String */
	sim_stream[n++] = 0x00;		/* length low  == 0 */
	sim_stream[n++] = 0x00;		/* length high == 0 */
	for (i = 0; i < 8192; i++)	/* device-controlled payload */
		sim_stream[n++] = pat(i);
	sim_len = n;

	printf("t1: launching verbatim pci_read_vpd(): String item length 0\n"
	    "t1: parser does kmalloc(remain+1) == kmalloc(1), then state 1\n"
	    "t1: writes one payload byte per VPD byte with no bound...\n");
	fflush(stdout);

	pci_read_vpd(&pcib_dev, &cfg);

	/* we only get here if the write stayed in bounds */
	if (cfg.vpd.vpd_ident != NULL) {
		int e = 0;
		while (e < 8192 && (uint8_t)cfg.vpd.vpd_ident[e] == pat(e))
			e++;
		printf("t1: returned; %d contiguous payload bytes at "
		    "vpd_ident (allocation was 1 byte)\n", e);
	} else {
		printf("t1: returned; vpd_ident==NULL (parser rejected the "
		    "zero-length item -- fixed kernel)\n");
	}
}

static void
t2(void)
{
	size_t n = 0;
	const char *v;
	int r;
	struct shim_devinfo di2;
	struct shim_pcicfg cfg2;

	sh_phase = "t2: error cleanup after a completed section (DF-5277)";
	fresh_cfg();
	/* section A: VPD-R, len 4, keyword "XY" len 1 value 'Z'
	 *   -> completes: vpd_rocnt = 1, array shrunk to 1 entry */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'X'; sim_stream[n++] = 'Y'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'Z';
	/* section B: VPD-R again, keyword "RV" with length 0
	 *   -> "bad keyword length", cksumvalid=0, state=-1 -> cleanup
	 *      frees vpd_ros and NULLs it, but vpd_rocnt stays 1 */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x03; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'R'; sim_stream[n++] = 'V'; sim_stream[n++] = 0x00;
	sim_len = n;

	printf("t2: stage1 parse...\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg);
	printf("t2: stage1 done\n"); fflush(stdout);

	printf("t2: post-parse cfg.vpd state:\n"
	    "t2:   vpd_ros  = %p   (freed and NULLed by cleanup)\n"
	    "t2:   vpd_rocnt= %d   (STALE -- cleanup does not reset it)\n"
	    "t2:   vpd_ident= %p\n"
	    "t2:   vpd_w    = %p, vpd_wcnt = %d\n",
	    cfg.vpd.vpd_ros, cfg.vpd.vpd_rocnt, cfg.vpd.vpd_ident,
	    cfg.vpd.vpd_w, cfg.vpd.vpd_wcnt);
	if (cfg.vpd.vpd_ros == NULL && cfg.vpd.vpd_rocnt != 0)
		printf("t2: CRASH PRECONDITION REPRODUCED: pci_freecfg() will\n"
		    "t2: dereference vpd_ros[%d].value == *(NULL+8), and\n"
		    "t2: kfree(vpd_ros=NULL) panics on DragonFly.\n",
		    cfg.vpd.vpd_rocnt);

	/* DF-5279: the readonly lookup misses its break (valid table) */
	sh_phase = "t2: readonly keyword lookup, valid table (DF-5279)";
	memset(&cfg2, 0, sizeof(cfg2));
	cfg2.vpd.vpd_reg = SIM_VPDBASE;
	cfg2.bus = 4; cfg2.slot = 5; cfg2.func = 6;
	sim_reset();
	n = 0;
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_stream[n++] = 0x78;		/* End item: clean exit, table stays */
	sim_len = n;
	printf("t2: stage2a parse (valid NA keyword + End)...\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg2);
	printf("t2: stage2a done\n"); fflush(stdout);
	memset(&di2, 0, sizeof(di2));
	di2.cfg = cfg2;
	v = (const char *)(void *)-1;
	r = pci_get_vpd_readonly_method(&child_dev, (device_t)&di2, "NA", &v);
	printf("t2: vpd_ros[0] = {\"%c%c\", \"%s\"}; lookup(\"NA\") -> "
	    "ret=%d vptr=%p\n", cfg2.vpd.vpd_ros[0].keyword[0],
	    cfg2.vpd.vpd_ros[0].keyword[1], cfg2.vpd.vpd_ros[0].value, r, v);
	printf("t2: %s\n", (r == ENXIO && v == NULL) ?
	    "BUG DF-5279: keyword exists but ENXIO/NULL returned (missing break)"
	    : "lookup OK");

	/* DF-5277 (readonly flavor): completed section, then I/O error ->
	 * cleanup NULLs vpd_ros but leaves vpd_rocnt stale -> the REAL
	 * verbatim lookup dereferences vpd_ros[0].keyword at NULL. */
	sh_phase = "t2: readonly lookup after error cleanup (DF-5277)";
	memset(&cfg2, 0, sizeof(cfg2));
	cfg2.vpd.vpd_reg = SIM_VPDBASE;
	cfg2.bus = 4; cfg2.slot = 5; cfg2.func = 6;
	sim_reset();
	n = 0;
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_len = n;			/* no End: next read stalls -> ENXIO */
	printf("t2: stage2b parse (section completes, then device stops)..."
	    "\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg2);
	printf("t2: stage2b: vpd_ros=%p vpd_rocnt=%d -- now calling the "
	    "verbatim lookup on this state:\n", cfg2.vpd.vpd_ros,
	    cfg2.vpd.vpd_rocnt);
	fflush(stdout);
	memset(&di2, 0, sizeof(di2));
	di2.cfg = cfg2;
	v = (const char *)(void *)-1;
	r = pci_get_vpd_readonly_method(&child_dev, (device_t)&di2, "NA", &v);
	printf("t2: BUG: lookup returned %d vptr=%p without crashing\n", r, v);
}

static void
t2a(void)
{
	size_t n = 0, i;

	sh_phase = "t2a: oversize remain (DF-5278a dead check)";
	fresh_cfg();
	sim_stream[n++] = 0x82;		/* String */
	sim_stream[n++] = 0x00;		/* length 0x0500 = 1280 */
	sim_stream[n++] = 0x05;
	for (i = 0; i < 1280; i++)
		sim_stream[n++] = (uint8_t)('a' + (i % 26));
	sim_stream[n++] = 0x78;		/* End item */
	sim_len = n;

	pci_read_vpd(&pcib_dev, &cfg);
	printf("t2a: remain=0x500 exceeded the 0x7f*4 bound; the kernel\n"
	    "t2a: printed \"invalid VPD data\" but then CONTINUED:\n");
	if (cfg.vpd.vpd_ident != NULL)
		printf("t2a:   vpd_ident len = %zu (parse did not abort -- "
		    "state=-1 was overwritten)\n", strlen(cfg.vpd.vpd_ident));
	else
		printf("t2a:   vpd_ident NULL (parse aborted -- fixed kernel)\n");
}

static void
t3(void)
{
	size_t n = 0;
	size_t served;
	size_t before_bytes = sh_bytes, before_alloc = sh_nalloc;

	sh_phase = "t3: unterminatable VPD-R section (DF-5278)";
	fresh_cfg();
	/* VPD-R item with length 1: the 3-byte keyword header alone drives
	 * `remain` to -2; it can never again reach 0, so states 2<->3 cycle
	 * for as long as the device serves data. */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x01; sim_stream[n++] = 0x00;
	sim_len = n;
	sim_repeat_off = n;
	sim_repeat_len = 4;
	sim_stream[n++] = 'K'; sim_stream[n++] = 'W';
	sim_stream[n++] = 0x01; sim_stream[n++] = 'X';
	sim_endless = 1;
	sim_serve_cap = 20000;	/* the ONLY thing that stops us */

	printf("t3: malicious device serves an endless keyword stream;\n"
	    "t3: capping it at %zu bytes (guest VA limits the demo; a real device never stops).\n",
	    sim_serve_cap);
	fflush(stdout);

	pci_read_vpd(&pcib_dev, &cfg);

	served = sim_off;
	printf("t3: parser stopped ONLY because the simulator capped the\n"
	    "t3: device at %zu bytes (vrs.off=%zu, config reads=%zu).\n",
	    sim_serve_cap, served, sim_reads);
	printf("t3: allocations during the capped run: %zu kmalloc/krealloc,"
	    " %zu bytes, largest %zu bytes\n",
	    sh_nalloc - before_alloc, sh_bytes - before_bytes, sh_maxblk);
	printf("t3: state machine had not terminated: every keyword kept\n"
	    "t3: consuming bytes with remain<0; with an uncapped device the\n"
	    "t3: loop and the allocations grow without bound.\n");
}

static void
t4(void)
{
	size_t n = 0;

	sh_phase = "t4: well-formed VPD (baseline)";
	fresh_cfg();
	sim_stream[n++] = 0x82; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'T'; sim_stream[n++] = 'E'; sim_stream[n++] = 'S';
	sim_stream[n++] = 'T';
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_stream[n++] = 0x78;		/* End */
	sim_len = n;

	pci_read_vpd(&pcib_dev, &cfg);
	printf("t4: ident=\"%s\" rocnt=%d ros[0]=\"%c%c\"->\"%s\" "
	    "(parse OK, harness faithful)\n",
	    cfg.vpd.vpd_ident ? cfg.vpd.vpd_ident : "(null)",
	    cfg.vpd.vpd_rocnt,
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].keyword[0] : '?',
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].keyword[1] : '?',
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].value : "(null)");
}

int
main(int argc, char **argv)
{
	struct sigaction sa;

	sa.sa_sigaction = sh_segv;
	sa.sa_flags = SA_SIGINFO;
	sigemptyset(&sa.sa_mask);
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);

	if (argc < 2) {
		fprintf(stderr, "usage: %s t1|t2|t2a|t3|t4\n", argv[0]);
		return (2);
	}
	if (strcmp(argv[1], "t1") == 0)
		t1();
	else if (strcmp(argv[1], "t2") == 0)
		t2();
	else if (strcmp(argv[1], "t2a") == 0)
		t2a();
	else if (strcmp(argv[1], "t3") == 0)
		t3();
	else if (strcmp(argv[1], "t4") == 0)
		t4();
	else {
		fprintf(stderr, "unknown mode %s\n", argv[1]);
		return (2);
	}
	return (0);
}