⬢ DragonFlyBSD Kernel Audit
DF-5754 / 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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
/*
 * harness.c -- KLD harness for the pass-2 audit of sys/netbt/hci_unit.c
 * (findings DF-5752 / DF-5753 / DF-5754).
 *
 * The production files hci_unit.c / bt_input.c / hci_event.c / hci_link.c /
 * hci_misc.c / l2cap_lower.c / sco_upper.c are compiled into this module
 * VERBATIM (copied from /usr/src/sys/netbt by build.sh, md5s recorded).
 * This harness plays the role the controller driver (ubt/btuart) plays in
 * production: it attaches a unit via the production hci_attach() and
 * injects synthetic HCI packets through the production entry points
 *
 *	hci_input_event() / hci_complete_sco()  ->  netisr(NETISR_BLUETOOTH)
 *	  -> btintr() -> hci_intr() -> hci_event()/hci_sco_complete()
 *
 * Triggers (sysctl, as root):
 *
 *   hw.hciu_probe = 1
 *	 print struct offsets / cache facts used by the analysis
 *
 *   hw.hciu_setup = 1
 *	 attach fake unit + ACL link #5 (OPEN) + rig l2cap_channel
 *
 *   [DF-5752] netbt hands each rx mbuf to netisr_queue() (embedding a
 *   netmsg inside the mbuf at m_hdr.mh_netmsg, offset 40) AND keeps the
 *   same mbuf on the unit's private ifqueue; hci_intr() drains the
 *   private queue and hci_event() FREES the mbufs while 1..N-1 of their
 *   embedded netmsgs are still linked on the netisr thread's port queue.
 *   The port queue then operates through freed memory:
 *
 *   hw.hciu_baseline = n
 *	 inject n filler events back-to-back, passive only.  The harness
 *	 hci_mtap() hook (called by hci_intr() for every packet, before
 *	 hci_event() frees the previous ones) prints
 *	 "PROOF: mbuf freed, netmsg still port-queued" lines: the freed
 *	 mbuf's lwkt_msg still has MSGF_QUEUED set => the netisr port
 *	 will _lwkt_pullmsg()/dispatch() through freed memory.
 *
 *   hw.hciu_fire = n
 *	 same burst, but at the 3rd processed packet hci_mtap() also
 *	 re-injects 3 more events FROM NETISR CONTEXT (a controller rx
 *	 interrupt on the same CPU in production).  The m_gethdr()s pop
 *	 the just-freed chunks (objcache LIFO) whose netmsgs are STILL
 *	 QUEUED => netisr_queue() re-initialises a live/dangling netmsg
 *	 and TAILQ_INSERT_TAILs the SAME node a second time => the port
 *	 queue linkage is corrupted; the queue chain is dumped after.
 *
 *   [DF-5753] hw.hcius_fire = rounds
 *	 per round: production sco_attach() + SCO link; fire 200 SCO
 *	 packets of exactly sp_mtu bytes via production sco_send() (the
 *	 fake controller completes them instantly via production
 *	 hci_complete_sco() -> hci_scodone + netisr, exactly what a USB
 *	 completion interrupt does); then production sco_disconnect
 *	 (pcb, 0) -- what sco_sabort() calls on socket close -- which
 *	 NULLs link->hl_sco and runs hci_link_free() on the syscall
 *	 thread.  The netisr thread's hci_intr() scodone walk matches
 *	 the still-listed link and calls hci_sco_complete() ->
 *	 link->hl_sco->sp_pending-- with hl_sco == NULL -> NULL write.
 *
 *   [DF-5754] hw.hciud_fire = rounds
 *	 per round: hci_attach() a fresh unit, flood hci_input_event()
 *	 from a kthread (16-response inquiry events -> slow drain), and
 *	 call production hci_detach() concurrently -> hci_disable() +
 *	 TAILQ_REMOVE + kfree(unit) while btintr()/hci_intr() walk the
 *	 unit list / process the queues -> use-after-free.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/module.h>
#include <sys/proc.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/types.h>
#include <sys/endian.h>
#include <sys/msgport.h>
#include <sys/msgport2.h>

#include <net/netisr.h>

#include <netbt/bluetooth.h>
#include <netbt/hci.h>
#include <netbt/l2cap.h>
#include <netbt/sco.h>

struct hci_unit *g_unit;

static volatile int g_d_rx_on;	/* fake controller rx enabled (DF-5754) */
static volatile int g_d_inflight;

/* ------------------------------------------------------------------ */
/* fake controller driver: the role ubt(4)/btuart(4) play             */

static int	 fake_enable(device_t);
static void	 fake_disable(device_t);
static void	 fake_output_cmd(device_t, struct mbuf *);
static void	 fake_output_acl(device_t, struct mbuf *);
static void	 fake_output_sco(device_t, struct mbuf *);
static void	 fake_get_stats(device_t, struct bt_stats *, int);

static const struct hci_if fake_hci_if = {
	fake_enable,
	fake_disable,
	fake_output_cmd,
	fake_output_acl,
	fake_output_sco,
	fake_get_stats,
};

static int
fake_enable(device_t dev)
{
	return 0;
}

static void
fake_disable(device_t dev)
{
	/*
	 * A real controller driver stops its rx paths here and waits for
	 * in-flight rx callbacks to return before disable returns
	 * (usb transfer abort + drain).  The DF-5754 rig honours that
	 * contract; what remains racing hci_detach() afterwards is the
	 * KERNEL side: btintr()/hci_intr() processing queued netmsgs on
	 * the netisr thread -- exactly what the DF-5754 fix protects.
	 */
	g_d_rx_on = 0;
	while (g_d_inflight > 0)
		tsleep(&g_d_inflight, 0, "hciqsc", 1);
}

static void
fake_output_cmd(device_t dev, struct mbuf *m)
{
	m_freem(m);
}

static void
fake_output_acl(device_t dev, struct mbuf *m)
{
	m_freem(m);
}

static void
fake_output_sco(device_t dev, struct mbuf *m)
{
	/* the controller "sends" the packet and the USB completion
	 * interrupt fires immediately: hand the mbuf to the production
	 * completion entry point (ubt's callback -> hci_complete_sco) */
	hci_complete_sco(g_unit, m);	/* production */
}

static void
fake_get_stats(device_t dev, struct bt_stats *s, int arg)
{
	memset(s, 0, sizeof(*s));
}

/* ------------------------------------------------------------------ */

static struct hci_link *g_link;

#define TEST_ACL_HANDLE	0x0005
#define TEST_SCO_HANDLE	0x0009

/* rig L2CAP channel (not needed for the DF-5752 proof, kept for
 * completeness of the unit state) */
static struct l2cap_channel g_chan;

/* rig SCO upper layer */
static void rig_sco_complete(void *, int);
static void rig_sco_disconnected(void *, int);
static void rig_sco_input(void *, struct mbuf *);

static const struct btproto rig_sco_proto = {
	NULL,			/* connecting */
	NULL,			/* connected */
	rig_sco_disconnected,	/* disconnected */
	NULL,			/* newconn */
	rig_sco_complete,	/* complete */
	NULL,			/* linkmode */
	rig_sco_input,		/* input */
};

static int g_sco_completes, g_sco_disconnects;

static void
rig_sco_complete(void *arg, int num)
{
	g_sco_completes++;
}

static void
rig_sco_disconnected(void *arg, int err)
{
	g_sco_disconnects++;
}

static void
rig_sco_input(void *arg, struct mbuf *m)
{
	m_freem(m);
}

/* ------------------------------------------------------------------ */
/* DF-5752 tracking                                                    */

struct rig_ent {
	struct mbuf	*m;
	lwkt_msg_t	 msg;
	int		 seen;		/* hci_mtap called for it */
};

static struct rig_ent rig_tab[64];
static int rig_n;
static int rig_mtap_count;
static int rig_reinject_at = 3;	/* hci_mtap call # that re-injects */
static int rig_reinject_on;	/* only enabled by hciuu_fire=1 */
static int rig_dangling_proof;
static int rig_reused_hit;

/* in-memory capture of dangling observations (console kprintf from the
 * netisr thread can be dropped by the serial driver, so evidence is
 * recorded here and printed from the syscall thread afterwards) */
struct rig_dangling {
	struct mbuf	*m;
	lwkt_msg_t	 msg;
	uint32_t	 flags;
	int		 mtap_call;
};
static struct rig_dangling rig_dang[16];
static int rig_dang_n;

static void
rig_track(struct mbuf *m)
{
	if (rig_n < (int)NELEM(rig_tab)) {
		rig_tab[rig_n].m = m;
		rig_tab[rig_n].msg = &m->m_hdr.mh_netmsg.base.lmsg;
		rig_tab[rig_n].seen = 0;
		rig_n++;
	}
}

/*
 * harness stand-in for hci_socket.c's hci_mtap(): hci_intr() calls this
 * for EVERY packet it dequeues, before hci_event()/hci_sco_recv()/
 * hci_acl_recv() processes (and frees) it.  Runs in netisr context.
 */
void
hci_mtap(struct mbuf *m, struct hci_unit *unit)
{
	int i;

	rig_mtap_count++;

	/* all previously-seen packets have been processed and freed by
	 * hci_event() (m_freem at hci_event.c:229) when we get here */
	for (i = 0; i < rig_n; i++) {
		if (rig_tab[i].seen && rig_tab[i].m != m &&
		    (rig_tab[i].msg->ms_flags & MSGF_QUEUED)) {
			/* freed mbuf, netmsg still linked on the port */
			if (rig_dang_n < (int)NELEM(rig_dang)) {
				rig_dang[rig_dang_n].m = rig_tab[i].m;
				rig_dang[rig_dang_n].msg = rig_tab[i].msg;
				rig_dang[rig_dang_n].flags =
				    rig_tab[i].msg->ms_flags;
				rig_dang[rig_dang_n].mtap_call =
				    rig_mtap_count;
				rig_dang_n++;
			}
			rig_dangling_proof++;
		}
	}

	/* mark this packet seen (it will be freed right after we return) */
	for (i = 0; i < rig_n; i++) {
		if (rig_tab[i].m == m) {
			rig_tab[i].seen = 1;
			break;
		}
	}

	/*
	 * mid-drain re-injection (a controller rx interrupt landing on the
	 * same CPU in production): the allocations here pop the chunks the
	 * drain just freed (objcache LIFO).  If a popped chunk's netmsg is
	 * still port-queued, netisr_queue() re-initialises it and inserts
	 * the same node again => double-linked queue corruption.
	 */
	if (rig_reinject_on && rig_mtap_count == rig_reinject_at) {
		static const uint8_t fill[] = { 0x04, 0x02, 0x01, 0x00 };
		struct mbuf *nm;
		int k;

		kprintf("[harness] hci_mtap: mid-drain re-injection of 3 "
		    "events from netisr context (objcache LIFO reuse of "
		    "just-freed chunks)\n");
		for (k = 0; k < 3; k++) {
			nm = m_gethdr(M_NOWAIT, MT_DATA);
			KKASSERT(nm != NULL);
			if (m_copyback2(nm, 0, sizeof(fill), fill,
			    M_NOWAIT) != 0)
				panic("copyback");
			/* does this chunk alias a tracked netmsg? */
			{
				int j;

				for (j = 0; j < rig_n; j++) {
					if (rig_tab[j].seen &&
					    (void *)nm == (void *)rig_tab[j].m) {
						kprintf("[harness] REUSE HIT: "
						    "new mbuf %p == freed "
						    "tracked mbuf idx %d "
						    "(netmsg %p flags=0x%x "
						    "QUEUED=%d)\n", nm, j,
						    rig_tab[j].msg,
						    rig_tab[j].msg->ms_flags,
						    !!(rig_tab[j].msg->
						    ms_flags & MSGF_QUEUED));
						rig_reused_hit++;
					}
				}
			}
			hci_input_event(unit, nm);	/* production */
		}
	}
}

void
hci_drop(void *arg)
{
	/* no socket bookkeeping to drop */
}

/* ------------------------------------------------------------------ */
/* probe                                                               */

static int
hciuu_probe(SYSCTL_HANDLER_ARGS)
{
	int v = 0, err;
	struct mbuf *m;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	kprintf("[probe] sizeof(struct mbuf)     = %zu\n", sizeof(struct mbuf));
	kprintf("[probe] sizeof(struct pkthdr)   = %zu\n", sizeof(struct pkthdr));
	kprintf("[probe] sizeof(struct m_hdr)    = %zu\n", sizeof(struct m_hdr));
	kprintf("[probe] sizeof(lwkt_msg)        = %zu\n", sizeof(lwkt_msg));
	kprintf("[probe] sizeof(struct netmsg_packet) = %zu\n",
	    sizeof(struct netmsg_packet));
	m = m_gethdr(M_WAITOK, MT_DATA);
	KKASSERT(m != NULL);
	kprintf("[probe] m=%p mh_netmsg@%td lmsg@%td nm_dispatch@%td "
	    "nm_packet@%td\n", m,
	    (char *)&m->m_hdr.mh_netmsg - (char *)m,
	    (char *)&m->m_hdr.mh_netmsg.base.lmsg - (char *)m,
	    (char *)&m->m_hdr.mh_netmsg.base.nm_dispatch - (char *)m,
	    (char *)&m->m_hdr.mh_netmsg.nm_packet - (char *)m);
	kprintf("[probe]   ms_flags@%td (m_pkthdr.rcvif@%td -- separate "
	    "union: no overlay)\n",
	    (char *)&m->m_hdr.mh_netmsg.base.lmsg.ms_flags - (char *)m,
	    (char *)&m->m_pkthdr.rcvif - (char *)m);
	kprintf("[probe] m_pktdat - (char *)m    = %td (MHLEN=%d)\n",
	    (long)(m->m_pktdat - (char *)m), (int)MHLEN);
	m_freem(m);
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hciu_probe, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hciuu_probe, "I", "print struct offsets");

/* ------------------------------------------------------------------ */
/* setup                                                               */

static int
hciuu_setup(SYSCTL_HANDLER_ARGS)
{
	int v = 0, err;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	if (g_unit != NULL) {
		kprintf("[harness] unit already attached\n");
		return 0;
	}

	kprintf("[harness] === setup: attach unit + ACL link #%d ===\n",
	    TEST_ACL_HANDLE);

	g_unit = hci_attach(&fake_hci_if, (device_t)(uintptr_t)0xcafe0000UL, 0);
	KKASSERT(g_unit != NULL);

	g_unit->hci_flags |= BTF_RUNNING;
	g_unit->hci_num_cmd_pkts = 1;
	g_unit->hci_max_acl_size = 32;
	g_unit->hci_num_acl_pkts = 0;

	g_link = hci_link_alloc(g_unit);
	KKASSERT(g_link != NULL);
	g_link->hl_type = HCI_LINK_ACL;
	g_link->hl_state = HCI_LINK_OPEN;
	g_link->hl_handle = TEST_ACL_HANDLE;

	memset(&g_chan, 0, sizeof(g_chan));
	g_chan.lc_lcid = 0x0041;
	g_chan.lc_rcid = 0x0042;
	g_chan.lc_state = L2CAP_OPEN;
	g_chan.lc_link = g_link;
	g_chan.lc_txq.ifq_maxlen = 50;

	kprintf("[harness] unit %p link %p attached\n", g_unit, g_link);
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hciu_setup, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hciuu_setup, "I", "attach fake HCI unit + ACL link");

/* ------------------------------------------------------------------ */
/* DF-5752                                                             */

static void
inject_event(const uint8_t *pkt, int len)
{
	struct mbuf *m;

	m = m_gethdr(M_NOWAIT, MT_DATA);
	KKASSERT(m != NULL);
	if (m_copyback2(m, 0, len, pkt, M_NOWAIT) != 0)
		panic("copyback");

	rig_track(m);
	hci_input_event(g_unit, m);	/* production entry point */
}

static void
rig_reset(void)
{
	rig_n = 0;
	rig_mtap_count = 0;
	rig_dangling_proof = 0;
	rig_reused_hit = 0;
	rig_dang_n = 0;
}

static int
hciuu_fire(SYSCTL_HANDLER_ARGS)
{
	/* filler: Inquiry Result event with zero responses (cheap, no-op) */
	static const uint8_t fill[] = { 0x04, 0x02, 0x01, 0x00 };
	int v = 0, err, i;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	KKASSERT(g_unit != NULL);

	if (v == 2) {
		/* baseline: passive, no re-injection */
		rig_reinject_on = 0;
		kprintf("[harness] === DF-5752 baseline: %d filler events "
		    "back-to-back (passive dangling proof) ===\n", 8);
	} else {
		rig_reinject_on = 1;
		kprintf("[harness] === DF-5752 fire: 8 fillers + mid-drain "
		    "re-injection at packet #%d ===\n", rig_reinject_at);
	}

	rig_reset();
	for (i = 0; i < 8; i++)
		inject_event(fill, sizeof(fill));

	kprintf("[harness] burst injected; waiting 3s for netisr...\n");
	tsleep(&rig_n, 0, "hciuu", 3 * hz);
	kprintf("[harness] done: dangling_observations=%d reuse_hits=%d "
	    "mtap_calls=%d\n", rig_dangling_proof, rig_reused_hit,
	    rig_mtap_count);
	for (i = 0; i < rig_dang_n; i++)
		kprintf("[harness] DF-5752 PROOF: at hci_mtap call #%d: "
		    "mbuf %p was already freed by hci_event() "
		    "(hci_event.c m_freem) but its netmsg %p still had "
		    "MSGF_QUEUED set -- the netisr port queue is holding "
		    "and will _lwkt_pullmsg()/TAILQ_REMOVE/dispatch() "
		    "through freed memory (flags=0x%x)\n",
		    rig_dang[i].mtap_call, rig_dang[i].m, rig_dang[i].msg,
		    rig_dang[i].flags);
	if (rig_reused_hit)
		kprintf("[harness] REUSE: freed chunks whose netmsgs were "
		    "still port-queued were reallocated and re-inserted "
		    "(same node linked twice: port queue corrupted)\n");
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hciu_fire, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hciuu_fire, "I", "fire DF-5752 burst (2=baseline)");

static int
hciuu_dump(SYSCTL_HANDLER_ARGS)
{
	int v = 0, err, i;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	for (i = 0; i < rig_n; i++)
		kprintf("[dump] idx=%d m=%p msg=%p seen=%d flags=0x%x "
		    "QUEUED=%d\n", i, rig_tab[i].m, rig_tab[i].msg,
		    rig_tab[i].seen, rig_tab[i].msg->ms_flags,
		    !!(rig_tab[i].msg->ms_flags & MSGF_QUEUED));
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hciu_dump, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hciuu_dump, "I", "dump DF-5752 tracking table");

/* ------------------------------------------------------------------ */
/* DF-5753: scodone dispatch vs syscall-context link teardown          */

static int
hcius_fire(SYSCTL_HANDLER_ARGS)
{
	struct sco_pcb *pcb = NULL;
	struct hci_link *sco;
	struct mbuf *m;
	uint8_t buf[32];
	int v = 0, err, round, sent;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	KKASSERT(g_unit != NULL);

	kprintf("[harness] === DF-5753: %d rounds of [200 SCO sends + "
	    "production sco_disconnect(pcb,0)] ===\n", v);
	kprintf("[harness] each send: sco_send() M_SETCTX(m, link); fake "
	    "controller completes instantly via hci_complete_sco(); "
	    "sco_disconnect NULLs hl_sco then runs hci_link_free() on this "
	    "(syscall) thread while netisr hci_intr() walks hci_links "
	    "matching M_GETCTX -> hci_sco_complete() derefs hl_sco==NULL\n");

	for (round = 0; round < v; round++) {
		/* fresh pcb + SCO link each round */
		err = sco_attach(&pcb, &rig_sco_proto, NULL);	/* production */
		KKASSERT(err == 0 && pcb != NULL);

		sco = hci_link_alloc(g_unit);			/* production */
		KKASSERT(sco != NULL);
		sco->hl_type = HCI_LINK_SCO;
		sco->hl_state = HCI_LINK_OPEN;
		sco->hl_handle = TEST_SCO_HANDLE;
		sco->hl_sco = pcb;
		pcb->sp_link = sco;
		pcb->sp_mtu = sizeof(buf);

		/* pile up completions (each queues scodone + netisr kick) */
		for (sent = 0; sent < 200; sent++) {
			m = m_gethdr(M_NOWAIT, MT_DATA);
			KKASSERT(m != NULL);
			if (m_copyback2(m, 0, sizeof(buf), buf, M_NOWAIT) != 0)
				panic("copyback");
			err = sco_send(pcb, m);		/* production */
			if (err)
				panic("sco_send %d", err);
		}

		/* the socket-close path: sco_sabort() -> sco_disconnect(,0) */
		sco_disconnect(pcb, 0);			/* production */

		if (round % 25 == 0)
			kprintf("[harness] round %d ok (completes=%d "
			    "disconnects=%d)\n", round, g_sco_completes,
			    g_sco_disconnects);
	}

	kprintf("[harness] all rounds done without a hit (race not won; "
	    "rerun with more rounds)\n");
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hcius_fire, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hcius_fire, "I", "fire DF-5753 scodone teardown race");

/* ------------------------------------------------------------------ */
/* DF-5754: hci_detach unit lifetime vs btintr/hci_intr                */

static struct hci_unit *g_d_unit;
static int g_d_run;

static int hciud_mode = 1;	/* 1 = filler events (no memo allocs:
				 * isolates the unit-lifetime race from the
				 * known DF-0683 disable-drain race),
				 * 2 = inquiry-16 (baseline payload) */

static void
ud_injector_thread(void *arg)
{
	/* 16-response Inquiry Result: slow hci_event processing widens the
	 * hci_intr drain window */
	uint8_t pkt[3 + 1 + 16 * 14];
	uint8_t fill[] = { 0x04, 0x02, 0x01, 0x00 };	/* Inquiry, 0 resp */
	const uint8_t *send;
	int sendlen;
	int off = 0, resp, i;
	static uint16_t bdctr;

	if (hciud_mode == 2) {
		pkt[off++] = 0x04;
		pkt[off++] = 0x02;			/* INQUIRY_RESULT */
		pkt[off++] = 1 + 16 * 14;
		pkt[off++] = 16;
		for (resp = 0; resp < 16; resp++) {
			pkt[off++] = bdctr & 0xff;
			pkt[off++] = (bdctr >> 8) & 0xff;
			bdctr++;
			for (i = 0; i < 4; i++)
				pkt[off++] = 0xc0 + i;
			pkt[off++] = 0x01;
			pkt[off++] = 0x00;
			pkt[off++] = 0x00;
			pkt[off++] = 0x22;
			pkt[off++] = 0x22;
			pkt[off++] = 0x22;
			pkt[off++] = 0x11;
			pkt[off++] = 0x11;
		}
		KKASSERT(off == (int)sizeof(pkt));
		send = pkt;
		sendlen = sizeof(pkt);
	} else {
		send = fill;
		sendlen = sizeof(fill);
	}

	while (1) {
		struct mbuf *m;

		/* increment BEFORE checking the enable flag so that
		 * fake_disable()'s inflight drain cannot pass between our
		 * check and our call (real drivers serialize this the same
		 * way around their transfer-arming path) */
		atomic_add_int(&g_d_inflight, 1);
		if (!g_d_run || !g_d_rx_on) {
			atomic_add_int(&g_d_inflight, -1);
			break;
		}

		m = m_gethdr(M_NOWAIT, MT_DATA);
		if (m == NULL) {
			atomic_add_int(&g_d_inflight, -1);
			continue;
		}
		if (m_copyback2(m, 0, sendlen, send, M_NOWAIT) != 0) {
			m_freem(m);
			atomic_add_int(&g_d_inflight, -1);
			continue;
		}
		hci_input_event(g_d_unit, m);	/* production */
		atomic_add_int(&g_d_inflight, -1);
	}
	kprintf("[harness] injector stopped\n");
	lwkt_exit();
}

static int
hciud_fire(SYSCTL_HANDLER_ARGS)
{
	int v = 0, err, round;
	struct thread *td = NULL;

	err = sysctl_handle_int(oidp, &v, 0, req);
	if (err != 0 || req->newptr == NULL || v == 0)
		return err;

	hciud_mode = (v == 2) ? 2 : 1;
	v = (v == 2) ? 200 : v;	/* inquiry mode: 200 slow rounds */

	KKASSERT(g_unit != NULL);	/* ensures netisr registered */

	kprintf("[harness] === DF-5754: %d rounds of [attach + flood-inject "
	    "+ hci_detach] ===\n", v);
	kprintf("[harness] hci_detach() runs hci_disable + TAILQ_REMOVE + "
	    "kfree on this thread while btintr() walks hci_unit_list and "
	    "hci_intr() drains the unit's queues on the netisr thread\n");

	for (round = 0; round < v; round++) {
		g_d_unit = hci_attach(&fake_hci_if,
		    (device_t)(uintptr_t)(0xcafe1000UL + round), 0);
		KKASSERT(g_d_unit != NULL);
		g_d_unit->hci_flags |= BTF_RUNNING;
		g_d_unit->hci_num_cmd_pkts = 1;
		g_d_rx_on = 1;

		g_d_run = 1;
		err = lwkt_create(ud_injector_thread, NULL, &td, NULL,
		    TDF_NOSTART, -1, "hciud_inj");
		KKASSERT(err == 0);
		lwkt_schedule(td);

		/* let the flood start, then tear the unit down under it
		 * (fake_disable quiesces rx per the driver contract; the
		 * netisr-side btintr/hci_intr race remains fully live) */
		tsleep(&g_d_run, 0, "hciud", hz / 100 + 1);

		hci_detach(g_d_unit);		/* production */
		g_d_run = 0;
		g_d_unit = NULL;

		if (round % 20 == 0)
			kprintf("[harness] round %d survived\n", round);
		tsleep(&g_d_run, 0, "hciud", hz / 50 + 1);
	}

	kprintf("[harness] all rounds done without a hit\n");
	return 0;
}

SYSCTL_PROC(_hw, OID_AUTO, hciud_fire, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hciud_fire, "I", "fire DF-5754 detach lifetime race");

/* ------------------------------------------------------------------ */

static int
harness_mode(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		l2cap_pdu_pool = zinit("l2cap_pdu",
		    sizeof(struct l2cap_pdu), 1, ZONE_DESTROYABLE);
		l2cap_req_pool = zinit("l2cap_req",
		    sizeof(struct l2cap_req), 1, ZONE_DESTROYABLE);
		KKASSERT(l2cap_pdu_pool != NULL && l2cap_req_pool != NULL);

		netisr_register(NETISR_BLUETOOTH, btintr, NULL);

		kprintf("[harness] hci_unit_harness loaded: hciu_probe, "
		    "hciuu_setup, hciu_fire, hciu_dump, hcius_fire, "
		    "hciud_fire\n");
		break;
	case MOD_UNLOAD:
		return EBUSY;
	default:
		break;
	}
	return 0;
}

static moduledata_t harness_moddata = {
	"hci_unit_harness",
	harness_mode,
	NULL
};

DECLARE_MODULE(hci_unit_harness, harness_moddata, SI_SUB_DRIVERS,
    SI_ORDER_MIDDLE);