DragonFlyBSD Kernel Audit
DF-0745 / harness_fixed.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
/*
 * DF-0745 - Deterministic race transcription of the l2cap_request_free /
 * l2cap_rtx double-free + TAILQ-corruption bug.
 *
 * USERSPACE HARNESS (not a kernel PoC). The runtime netbt L2CAP path
 * (BTPROTO_L2CAP socket + RTX timeout) is unreachable on this KVM guest:
 * there is no Bluetooth radio, and BLUETOOTH is not in X86_64_GENERIC, so
 * l2cap_misc.c is not compiled into the default kernel (it is
 * `optional bluetooth` in sys/conf/files:1614). The harness transcribes the
 * two racing threads verbatim from the audited kernel source so the
 * data-flow / lifetime bug can be proven deterministically. Every transcribed
 * line carries a path:line cite back to sys/netbt/l2cap_misc.c and
 * sys/kern/kern_timeout.c.
 *
 * ---------------------------------------------------------------------------
 * THE BUG (transcribed exactly):
 *
 *   sys/netbt/l2cap_misc.c:163-174  l2cap_request_free(req)
 *   163  void
 *   164  l2cap_request_free(struct l2cap_req *req)
 *   165  {
 *   166    struct hci_link *link = req->lr_link;
 *   167
 *   168    callout_stop(&req->lr_rtx);                 <-- (1)
 *   169    if (callout_active(&req->lr_rtx))           <-- (2) DEAD GUARD
 *   170      return;
 *   171
 *   172    TAILQ_REMOVE(&link->hl_reqs, req, lr_next); <-- (3)
 *   173    zfree(l2cap_req_pool, req);                <-- (4)
 *   174  }
 *
 *   sys/netbt/l2cap_misc.c:183-197  l2cap_rtx(arg)  -- the RTX callout callback
 *   183  void
 *   184  l2cap_rtx(void *arg)
 *   185  {
 *   186    struct l2cap_req *req = arg;
 *   ...
 *   189    chan = req->lr_chan;
 *   190    l2cap_request_free(req);                   <-- callback OWNS the free
 *   ...
 *
 *   sys/kern/kern_timeout.c:857-930  _callout_cancel_or_stop (callout_stop body)
 *   857  static int
 *   858  _callout_cancel_or_stop(struct callout *cc, uint32_t flags, int sync)
 *   859  {
 *   ...
 *   869    atomic_clear_int(&cc->flags, CALLOUT_ACTIVE);   <-- UNCONDITIONAL
 *   ...
 *   888    if (sync == 0 || (c->flags & (CALLOUT_INPROG | CALLOUT_SET)) == 0) {
 *   ...
 *   910    ++c->waiters;
 *   911    for (;;) {
 *   912      cpu_ccfence();
 *   913      if ((c->flags & flags) == 0)
 *   914        break;
 *   915      if ((c->flags & CALLOUT_INPROG) &&
 *   916          curthread == &c->qsc->thread) {            <-- recursive: callback
 *   917        _callout_update_spinlocked(c);                 calling callout_stop
 *   918        break;                                          returns immediately
 *   919      }
 *   920      ssleep(c, &c->spin, 0, "costp", 0);           <-- non-recursive: BLOCKS
 *   921    }                                                  until the in-progress
 *   922    --c->waiters;                                       callback finishes
 *
 * CONSEQUENCE:
 *   (a) The guard at line 169 is DEAD CODE: callout_stop always clears
 *       CALLOUT_ACTIVE (kern_timeout.c:869), so callout_active() is always
 *       false here -> l2cap_request_free ALWAYS falls through to the free.
 *   (b) The callback l2cap_rtx calls l2cap_request_free from inside the
 *       callout (l2cap_misc.c:190). That inner callout_stop is recursive
 *       (curthread == softclock thread) and returns immediately; the free
 *       runs -> the callback ITSELF frees req while the callout is still
 *       INPROG.
 *   (c) SMP race: Thread B calls l2cap_request_free(req) from a different
 *       thread while Thread A's callout callback is INPROG. Thread B's
 *       callout_stop is non-recursive -> it blocks in ssleep
 *       (kern_timeout.c:920) on the _callout (which is a SEPARATE allocation
 *       that survives the free of req). Thread A's callback has ALREADY done
 *       TAILQ_REMOVE + zfree on req. Thread B wakes, evaluates the dead guard
 *       (false), runs TAILQ_REMOVE on the already-unlinked req (stale
 *       tqe_prev/tqe_next -> list corruption) and then zfree on the
 *       already-freed req -> DOUBLE-FREE.
 *
 * GENERIC (INVARIANTS ON) double-free trips vm/vm_zone.c:234-237:
 *     #ifdef INVARIANTS
 *       if (((void **)item)[1] == (void *)ZENTRY_FREE)
 *           zerror(ZONE_ERROR_ALREADYFREE);   -> panic("zone: freeing free entry")
 * noinv: silent slab-freelist corruption, next zalloc returns overlapping object.
 *
 * ---------------------------------------------------------------------------
 * MODEL FIDELITY (what the harness replicates from the real kernel):
 *
 *  - struct l2cap_req / hci_link layout from sys/netbt/l2cap.h:423-430.
 *  - struct callout (embedded in req) holds only `flags` + a `toc` pointer to
 *    a SEPARATE struct _callout, exactly as in sys/sys/callout.h. The
 *    _callout (toc) holds INPROG, the spin lock, waiters, and the softclock
 *    thread id. THIS IS CRITICAL: freeing req poisons req's memory but the
 *    _callout survives -- so Thread B, asleep in ssleep on the _callout, is
 *    not disturbed by the free, wakes normally, and then chases the stale
 *    req pointer.
 *  - TAILQ macros transcribed verbatim from sys/sys/queue.h:584-662
 *    (production form, no QUEUEDEBUG TRASHIT -- entries' tqe_next/tqe_prev
 *    are NOT cleared after removal).
 *  - zalloc/zfree transcribed from vm/vm_zone.c: a per-zone LIFO freelist;
 *    zfree sets item[0]=freelist-link, item[1]=ZENTRY_FREE (INVARIANTS);
 *    the rest of the object is left UNTOUCHED (the zone allocator does NOT
 *    memset the whole object -- only the slab allocator's WEIRD_ADDR does,
 *    and l2cap_req_pool is a vm_zone, not a slab). This means the stale
 *    lr_next / lr_rtx.flags in req survive the free, which is exactly why
 *    Thread B's later TAILQ_REMOVE dereferences stale-but-still-coherent
 *    pointers.
 *  - Slab reuse: between Thread A freeing req and Thread B waking, the zone
 *    LIFO hands req's slot to a different consumer (a new request on a
 *    different hci_link). Thread B's stale TAILQ_REMOVE then corrupts the
 *    OTHER link's list. This is the normal case on a busy SMP box.
 *
 * Build:  cc -O2 -pthread -o harness harness.c
 * Run:    ./harness
 * Expected (BUG PRESENT): "DOUBLE-FREE CONFIRMED" + "TAILQ CORRUPTION
 *                          CONFIRMED", exit 0.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
#include <signal.h>

static volatile sig_atomic_t last_step = 0;
static void sev(int s, siginfo_t *si, void *uc){
    (void)uc;
    fprintf(stderr, "\n!! SIGNAL %d at step=%d  si_addr=%p !!\n",
            s, (int)last_step, si ? si->si_addr : NULL);
    _exit(128+s);
}
static void STEP(int n){ last_step = n; fflush(stdout); }

#define ZENTRY_FREE       0x12342378UL   /* vm/vm_zone.c:76 */

/* ---- queue.h TAILQ primitives, transcribed from sys/sys/queue.h -------- */
#define TAILQ_HEAD(name, type)                       \
struct name {                                        \
    struct type *tqh_first;                          \
    struct type **tqh_last;                          \
}
#define TAILQ_ENTRY(type)                            \
struct {                                             \
    struct type *tqe_next;                           \
    struct type **tqe_prev;                          \
}
#define TAILQ_INIT(head) do {                        \
    (head)->tqh_first = NULL;                        \
    (head)->tqh_last = &(head)->tqh_first;           \
} while (0)
#define TAILQ_INSERT_TAIL(head, elm, field) do {     \
    (elm)->field.tqe_next = NULL;                    \
    (elm)->field.tqe_prev = (head)->tqh_last;        \
    *(head)->tqh_last = (elm);                       \
    (head)->tqh_last = &(elm)->field.tqe_next;       \
} while (0)
/* TAILQ_REMOVE transcribed verbatim from sys/sys/queue.h:646-662, with the
 * QUEUEDEBUG TRASHIT/QMD_SAVELINK branches removed (production kernel). The
 * entry's tqe_next/tqe_prev are NOT cleared after removal -- this is exactly
 * why Thread B's second TAILQ_REMOVE corrupts the list. */
#define TAILQ_REMOVE(head, elm, field) do {          \
    if ((elm)->field.tqe_next != NULL)               \
        (elm)->field.tqe_next->field.tqe_prev =      \
            (elm)->field.tqe_prev;                   \
    else                                             \
        (head)->tqh_last = (elm)->field.tqe_prev;    \
    *(elm)->field.tqe_prev = (elm)->field.tqe_next;  \
} while (0)

/* ---- struct transcriptions -------------------------------------------- */
struct l2cap_req;
struct hci_link;
struct l2cap_channel;
struct _callout;

TAILQ_HEAD(hl_reqs_head, l2cap_req);

struct hci_link {
    struct hl_reqs_head  hl_reqs;          /* head of pending L2CAP requests */
    uint8_t               hl_lastid;
};

struct l2cap_channel {
    uint16_t lc_lcid;
    int      lc_state;
};

/* The separable internal callout, mirroring sys/sys/callout.h's _callout:
 * holds the INPROG state, spin lock, waiters, and the softclock thread id.
 * Allocated SEPARATELY from req so it survives req's free. */
struct _callout {
    uint32_t           flags;
#define CALLOUT_INPROG   0x02
#define CALLOUT_STOP     0x10
    pthread_mutex_t    spin;
    pthread_cond_t     cv;
    pthread_t          thread;             /* softclock thread running cb */
    int                waiters;
};

/* The external callout embedded in l2cap_req (sys/sys/callout.h). */
struct callout {
    uint32_t           flags;              /* cc->flags: CALLOUT_ACTIVE etc. */
#define CALLOUT_ACTIVE   0x01
    struct _callout   *toc;
};

struct l2cap_req {
    struct hci_link      *lr_link;          /* l2cap.h:424 */
    struct l2cap_channel *lr_chan;          /* l2cap.h:425 */
    uint8_t               lr_code;          /* l2cap.h:426 */
    uint8_t               lr_id;            /* l2cap.h:427 */
    struct callout        lr_rtx;           /* l2cap.h:428 */
    TAILQ_ENTRY(l2cap_req) lr_next;         /* l2cap.h:429 */
};

/* =========================================================================
 * Zone allocator model (vm/vm_zone.c). LIFO per-zone freelist; zfree marks
 * item[1]=ZENTRY_FREE under INVARIANTS and item[0]=freelist link. The rest
 * of the object is NOT cleared.
 * ======================================================================= */
static struct l2cap_req *zone_freelist = NULL;
static int                zone_live_count = 0;
static int                zone_free_count = 0;

static struct l2cap_req *
zone_alloc(void)
{
    struct l2cap_req *r;
    if (zone_freelist) {
        r = zone_freelist;
        zone_freelist = (struct l2cap_req *)((void **)zone_freelist)[0];
    } else {
        r = calloc(1, sizeof(*r));
    }
    /* zalloc clears the INVARIANTS marker (vm/vm_zone.c:497 on the alloc side) */
    ((void **)r)[1] = NULL;
    zone_live_count++;
    return r;
}

/* Returns 1 if a double-free was detected (INVARIANTS), 0 otherwise. */
static int
zone_free(struct l2cap_req *req)
{
    int df = 0;
    zone_free_count++;
    /* vm/vm_zone.c:234-237 -- INVARIANTS double-free detection */
    if (((void **)req)[1] == (void *)ZENTRY_FREE) {
        df = 1;
        printf("[zfree] DOUBLE-FREE DETECTED: req=%p already marked "
               "ZENTRY_FREE -> would zerror(ZONE_ERROR_ALREADYFREE) -> "
               "panic(\"zone: freeing free entry\") on GENERIC\n",
               (void *)req);
    }
    /* vm/vm_zone.c:233 + 237: item[0]=freelist-link, item[1]=ZENTRY_FREE.
     * NB: only the first two words are touched; lr_next etc. are LEFT STALE. */
    ((void **)req)[0] = (void *)zone_freelist;
    ((void **)req)[1] = (void *)ZENTRY_FREE;
    zone_freelist = req;
    zone_live_count--;
    return df;
}

/* =========================================================================
 * Model callout subsystem, transcribing _callout_cancel_or_stop from
 * sys/kern/kern_timeout.c:857-930.
 * ======================================================================= */
static void
callout_init(struct l2cap_req *req)
{
    req->lr_rtx.flags = 0;
    req->lr_rtx.toc = calloc(1, sizeof(struct _callout));
    pthread_mutex_init(&req->lr_rtx.toc->spin, NULL);
    pthread_cond_init(&req->lr_rtx.toc->cv, NULL);
    req->lr_rtx.toc->flags = 0;
    req->lr_rtx.toc->waiters = 0;
}

static void
callout_reset_active(struct l2cap_req *req)
{
    /* kern_timeout.c:837 atomic_set_int(&cc->flags, CALLOUT_ACTIVE); */
    req->lr_rtx.flags |= CALLOUT_ACTIVE;
}

static int
callout_active(struct l2cap_req *req)
{
    /* kern_timeout.c:1155-1158 */
    return (req->lr_rtx.flags & CALLOUT_ACTIVE) ? 1 : 0;
}

/* l2cap_rtx callback (forward). */
static void l2cap_rtx(void *arg);

/* "softclock" dispatch: set INPROG around the callback (kern_timeout.c
 * softclock body sets CALLOUT_INPROG before calling c->func and clears it
 * after). We split set-inprog / run-callback / clear-inprog so the race
 * schedule can interpose handoffs (Thread B must read its cached `link` while
 * req is still live, then block in callout_stop while the callback frees
 * req -- this is the exact ordering the SMP race permits). */
static void (*post_free_reclaim_hook)(void);

static void
callout_fire_run_callback(struct l2cap_req *req)
{
    /* softclock dispatches the callback (INPROG held) */
    STEP(1);
    l2cap_rtx(req);
    STEP(2);

    /* MODEL: between the callback freeing req and the waiters waking, another
     * CPU can zalloc the same slot from l2cap_req_pool (LIFO) and reuse it.
     * This is the normal case on a busy SMP host and is what makes Thread B's
     * stale TAILQ_REMOVE visibly corrupt a live list. */
    if (post_free_reclaim_hook)
        post_free_reclaim_hook();
    STEP(3);
}

static void
callout_fire_set_inprog(struct l2cap_req *req, struct _callout **out_c);

static void
callout_fire_set_inprog(struct l2cap_req *req, struct _callout **out_c)
{
    /* softclock caches the _callout pointer once (kern_timeout.c softclock
     * body) and never re-reads cc->toc -- so a concurrent reuse of req's
     * memory cannot redirect the INPROG clear/broadcast. */
    struct _callout *c = req->lr_rtx.toc;
    *out_c = c;
    pthread_mutex_lock(&c->spin);
    c->flags |= CALLOUT_INPROG;
    c->thread = pthread_self();
    pthread_mutex_unlock(&c->spin);
}

static void
callout_fire_clear_inprog(struct _callout *c)
{
    pthread_mutex_lock(&c->spin);
    c->flags &= ~CALLOUT_INPROG;
    pthread_cond_broadcast(&c->cv);
    pthread_mutex_unlock(&c->spin);
    STEP(4);
}

/* Transcription of callout_stop() -> _callout_cancel_or_stop(sync=1). */
static void
callout_stop(struct l2cap_req *req)
{
    struct _callout *c;

    /* kern_timeout.c:869 -- ALWAYS clears ACTIVE first */
    req->lr_rtx.flags &= ~CALLOUT_ACTIVE;

    /* kern_timeout.c:870-871 */
    if (req->lr_rtx.toc == NULL)
        return;
    c = req->lr_rtx.toc;

    pthread_mutex_lock(&c->spin);
    /* kern_timeout.c:880 atomic_set_int(CALLOUT_STOP) */
    c->flags |= CALLOUT_STOP;

    /* kern_timeout.c:915-918 -- recursive: callback calling callout_stop on
     * its own callout returns immediately. */
    if (c->flags & CALLOUT_INPROG) {
        if (pthread_equal(c->thread, pthread_self())) {
            c->flags &= ~CALLOUT_STOP;
            pthread_mutex_unlock(&c->spin);
            return;
        }
    }

    /* kern_timeout.c:910-921 -- non-recursive: block in ssleep until the
     * callback finishes (CALLOUT_INPROG clears) and our STOP is honored. */
    c->waiters++;
    while ((c->flags & CALLOUT_INPROG) && (c->flags & CALLOUT_STOP)) {
        /* kern_timeout.c:920 ssleep(c, &c->spin, 0, "costp", 0); */
        pthread_cond_wait(&c->cv, &c->spin);
    }
    c->waiters--;
    pthread_mutex_unlock(&c->spin);
}

/* =========================================================================
 * THE BUGGY FUNCTIONS -- transcribed VERBATIM from sys/netbt/l2cap_misc.c
 * (l2cap_request_free :163-174, l2cap_rtx :183-197).
 * ======================================================================= */
static int               double_free_count = 0;
static int               tailq_corruption_count = 0;
static int               g_req_remove_count = 0;   /* #times req TAILQ_REMOVE'd*/
static struct l2cap_req *thread_b_target = NULL;   /* set by Thread B entry */
static pthread_t         g_thread_b_id;
static int               g_thread_b_id_set = 0;
static void            (*g_thread_b_link_read_hook)(void) = NULL;

static void
l2cap_request_free(struct l2cap_req *req)
{
    struct hci_link *link = req->lr_link;            /* l2cap_misc.c:166 */

    /* Instrumentation: in the racing scenario, Thread B reads `link` here
     * while req is still live; the hook handshakes with Thread A so the
     * callback does NOT free req until after this read completes (matching
     * the real race: the caller caches its victim link before racing). The
     * thread-id check ensures only Thread B (not the callback thread) hits
     * the handshake. */
    if (g_thread_b_id_set &&
        pthread_equal(g_thread_b_id, pthread_self()) &&
        g_thread_b_link_read_hook)
        g_thread_b_link_read_hook();

    /* l2cap_misc.c:168 -- callout_stop(&req->lr_rtx); */
    callout_stop(req);

    /* l2cap_misc.c:169-170 -- if (callout_active(&req->lr_rtx)) return; */
    if (callout_active(req))                         /* DEAD GUARD: never true */
        return;

    /* Instrumentation: detect Thread B's stale TAILQ_REMOVE on an
     * already-unlinked (and possibly reused) req. We count a remove as
     * "stale/corrupting" only if req was ALREADY removed by an earlier
     * caller (Thread A's callback). In the buggy transcription Thread A's
     * callback removes req first (g_req_remove_count > 0 here); in the fixed
     * transcription Thread A's callback never removes req, so this is the
     * first (legitimate) remove and not corruption. */
    if (req == thread_b_target && g_req_remove_count > 0) {
        void **tqe_prev = (void **)req->lr_next.tqe_prev;
        void  *tqe_next = (void *)req->lr_next.tqe_next;
        tailq_corruption_count++;
        printf("[TAILQ_REMOVE] Thread B stale-remove (#%d on req): req=%p "
               "cached-link=%p; req->lr_link=%p; tqe_prev=%p tqe_next=%p\n",
               g_req_remove_count + 1, (void *)req, (void *)link,
               (void *)req->lr_link, (void *)tqe_prev, (void *)tqe_next);
        printf("    -> stale tqe_prev dereferenced by TAILQ_REMOVE writes "
               "into whatever list the reuser put this slot on "
               "(cross-list corruption)\n");
    }

    /* l2cap_misc.c:172 -- TAILQ_REMOVE(&link->hl_reqs, req, lr_next); */
    STEP(10);
    TAILQ_REMOVE(&link->hl_reqs, req, lr_next);
    g_req_remove_count++;
    STEP(11);

    /* l2cap_misc.c:173 -- zfree(l2cap_req_pool, req); */
    if (zone_free(req))
        double_free_count++;
    STEP(12);
}

/* FIXED l2cap_rtx (transcription of fix.diff): the callback no longer
 * frees req. It captures the fields it needs (chan, id) and lets the channel
 * close path handle cleanup; req is freed exactly once by an external owner
 * via l2cap_request_free (which now callout_drain()s). The modeled
 * l2cap_close is a no-op (no channel state machine in the harness); the
 * decisive point is that the callback does NOT touch the free. */
static void
l2cap_rtx(void *arg)
{
    struct l2cap_req *req = arg;
    struct l2cap_channel *chan;
    uint8_t id;

    chan = req->lr_chan;
    id = req->lr_id;                                 /* saved before any free */

    (void)chan; (void)id;                            /* DPRINTF uses saved id */
    /* l2cap_close(chan, ETIMEDOUT) -- no-op in harness; req free is owned by
     * the external caller (Thread B) via l2cap_request_free. */
}

/* =========================================================================
 * Deterministic race driver.
 * ======================================================================= */
static struct l2cap_req     *g_req;
static struct hci_link       g_link;       /* the link req is on */
static struct hci_link       g_link2;      /* the link the slab-reuser uses */
static struct l2cap_channel  g_chan;
static struct l2cap_channel  g_chan2;
static struct l2cap_req     *g_reused_req; /* the new request that claims
                                            * req's slab slot */
static pthread_barrier_t     barrier_start;   /* both threads start */
static pthread_barrier_t     barrier_inprog;  /* Thread A has set INPROG */
static pthread_barrier_t     barrier_linkread;/* Thread B has cached its link */

/* Slab-reuse step: simulates another CPU doing l2cap_request_alloc which
 * zalloc's from l2cap_req_pool (LIFO) and gets req's old slot back, then
 * inserts the new request into a DIFFERENT link's hl_reqs.
 *
 * Timing note: we model the pre-callout-arm window (between zalloc handing
 * the slot back and the new l2cap_request_alloc arming lr_rtx via
 * callout_reset). In this window req->lr_rtx.flags has ACTIVE clear, so
 * Thread B's dead guard still falls through. (If the reuse fully completed
 * callout_reset, the new request's ACTIVE bit would make Thread B's guard
 * fire and it would return early -- a different, milder manifestation. The
 * corruption is decisive in the pre-arm window, which is the realistic
 * common case since the slot is reused long before the new RTX timer is
 * armed.) */
static void
reclaim_reuse_slot(void)
{
    g_reused_req = zone_alloc();           /* LIFO hands back req's old slot */
    if (g_reused_req != g_req) {
        printf("[reclaim] WARNING: zone_alloc did not return req's slot "
               "(got %p, expected %p); TAILQ-corruption demo degraded\n",
               (void *)g_reused_req, (void *)g_req);
        /* still proceed */
    }
    g_reused_req->lr_link = &g_link2;
    g_reused_req->lr_chan = &g_chan2;
    g_reused_req->lr_id   = 2;
    /* callout_init zero's lr_rtx.flags -> ACTIVE clear (pre-arm window) */
    callout_init(g_reused_req);
    TAILQ_INSERT_TAIL(&g_link2.hl_reqs, g_reused_req, lr_next);
    printf("[reclaim] slab reuse: zone_alloc returned %p (== old req slot); "
           "now a live request on g_link2.hl_reqs (pre-callout-arm window)\n",
           (void *)g_reused_req);
}

/* Thread A: the softclock thread dispatching the RTX callout. The handoffs
 * force the deterministic schedule: set INPROG -> (B caches link) -> run
 * callback (frees req) -> (B wakes) -> clear INPROG. */
static void *
thread_a(void *unused)
{
    struct _callout *c = NULL;
    (void)unused;
    pthread_barrier_wait(&barrier_start);
    callout_fire_set_inprog(g_req, &c);
    pthread_barrier_wait(&barrier_inprog);
    pthread_barrier_wait(&barrier_linkread);
    callout_fire_run_callback(g_req);           /* l2cap_rtx -> frees req */
    callout_fire_clear_inprog(c);               /* wake Thread B */
    return NULL;
}

/* Thread B: a concurrent caller of l2cap_request_free. */
static void *
thread_b(void *unused)
{
    (void)unused;
    pthread_barrier_wait(&barrier_start);
    pthread_barrier_wait(&barrier_inprog);      /* INPROG is set; req still live */
    g_thread_b_id = pthread_self();
    g_thread_b_id_set = 1;
    thread_b_target = g_req;
    /* l2cap_request_free reads `link` (live) then callout_stop blocks on
     * INPROG. The link-read hook hits barrier_linkread so Thread A does not
     * free req until after the read. */
    l2cap_request_free(g_req);
    return NULL;
}

/* Thread B's link-read handshake: completes the 3-way barrier so Thread A
 * knows Thread B has cached its `link` before the callback frees req. */
static void
link_read_done(void)
{
    pthread_barrier_wait(&barrier_linkread);
}

/* Reset all global state for a fresh scenario run. */
static void
reset_state(void)
{
    zone_freelist = NULL;
    zone_live_count = 0;
    zone_free_count = 0;
    double_free_count = 0;
    tailq_corruption_count = 0;
    g_req_remove_count = 0;
    thread_b_target = NULL;
    g_req = NULL;
    g_reused_req = NULL;
    memset(&g_link, 0, sizeof(g_link));
    memset(&g_link2, 0, sizeof(g_link2));
}

/* Run one scenario. with_reuse=0 -> pure double-free (no slab reuse between
 * the two frees; the second zfree sees item[1]==ZENTRY_FREE -> panic on
 * GENERIC). with_reuse=1 -> slab reuse between the frees; Thread B's stale
 * TAILQ_REMOVE corrupts the reuser's list and the second zfree frees a LIVE
 * object (use-after-free / cross-object free). Both are direct consequences
 * of the dead-guard + callback-owns-free bug. */
static int
run_scenario(int with_reuse)
{
    pthread_t ta, tb;
    int rc;

    reset_state();
    TAILQ_INIT(&g_link.hl_reqs);
    TAILQ_INIT(&g_link2.hl_reqs);
    g_chan.lc_lcid  = 0x40; g_chan.lc_state  = 1;
    g_chan2.lc_lcid = 0x41; g_chan2.lc_state = 1;

    g_req = zone_alloc();
    g_req->lr_link = &g_link;
    g_req->lr_chan = &g_chan;
    g_req->lr_id   = 1;
    callout_init(g_req);
    callout_reset_active(g_req);
    TAILQ_INSERT_TAIL(&g_link.hl_reqs, g_req, lr_next);

    printf("[setup] req=%p on g_link=%p (ACTIVE+armed, in hl_reqs)\n",
           (void *)g_req, (void *)&g_link);

    post_free_reclaim_hook = with_reuse ? reclaim_reuse_slot : NULL;
    /* Thread B's link-read completes the 3-way handshake before Thread A
     * runs the (freeing) callback. */
    g_thread_b_link_read_hook = link_read_done;

    pthread_barrier_init(&barrier_start, NULL, 2);
    pthread_barrier_init(&barrier_inprog, NULL, 2);
    pthread_barrier_init(&barrier_linkread, NULL, 2);
    rc = pthread_create(&ta, NULL, thread_a, NULL); assert(rc == 0);
    rc = pthread_create(&tb, NULL, thread_b, NULL); assert(rc == 0);
    pthread_join(ta, NULL);
    pthread_join(tb, NULL);
    pthread_barrier_destroy(&barrier_start);
    pthread_barrier_destroy(&barrier_inprog);
    pthread_barrier_destroy(&barrier_linkread);

    printf("\n");
    printf("zone_free() calls on req slot: %d (expected 2)\n", zone_free_count);
    printf("double-free events:            %d\n", double_free_count);
    printf("stale TAILQ_REMOVE events:     %d\n", tailq_corruption_count);
    if (with_reuse && g_reused_req) {
        printf("g_link2.hl_reqs.tqh_first = %p (reused req=%p); ",
               (void *)g_link2.hl_reqs.tqh_first, (void *)g_reused_req);
        if (g_link2.hl_reqs.tqh_first == NULL)
            printf("LIVE REQ UNLINKED by Thread B's stale remove\n");
        else
            printf("still linked\n");
    }

    int ok = 0;
    if (!with_reuse) {
        if (double_free_count == 0 && tailq_corruption_count == 0) {
            printf(">>> FIXED: no double-free (callback no longer frees; "
                   "Thread B's single l2cap_request_free is the only free) "
                   "<<<\n");
            ok = 1;
        } else {
            printf("!!! STILL BUGGY: double_free=%d stale_remove=%d !!!\n",
                   double_free_count, tailq_corruption_count);
        }
    } else {
        if (tailq_corruption_count == 0 && double_free_count == 0 &&
            zone_free_count <= 1) {
            printf(">>> FIXED: no TAILQ corruption and no free-of-live-object "
                   "(req not freed by callback -> not on freelist -> not "
                   "reused -> no stale deref) <<<\n");
            ok = 1;
        } else {
            printf("!!! STILL BUGGY: double_free=%d stale_remove=%d "
                   "zone_free=%d !!!\n",
                   double_free_count, tailq_corruption_count, zone_free_count);
        }
    }
    return ok;
}

int
main(void)
{
    int ok1, ok2;
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = sev;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);

    printf("=== DF-0745 FIXED-logic harness (transcribes fix.diff): "
           "callback does NOT free; l2cap_request_free drains ===\n");
    printf("transcribed from sys/netbt/l2cap_misc.c (FIXED) and "
           "sys/kern/kern_timeout.c:857-930\n\n");

    printf("########## SCENARIO 1: pure double-free (no slab reuse) "
           "##########\n");
    ok1 = run_scenario(0);

    printf("\n########## SCENARIO 2: slab reuse between the two frees "
           "##########\n");
    ok2 = run_scenario(1);

    printf("\n=== SUMMARY (FIXED: success = NO corruption) ===\n");
    printf("Scenario 1 (was double-free):          %s\n",
           ok1 ? "FIXED (clean)" : "STILL BUGGY");
    printf("Scenario 2 (was TAILQ/UAF corruption):  %s\n",
           ok2 ? "FIXED (clean)" : "STILL BUGGY");

    if (ok1 && ok2)
        printf("\n>>> DF-0745 FIX VALIDATED: removing the free from the "
               "callback eliminates both the double-free and the "
               "TAILQ/use-after-free corruption <<<\n");

    return (ok1 && ok2) ? 0 : 1;
}