โฌข DragonFlyBSD Kernel Audit
DF-0754 / 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
/*
 * DF-0754 โ€” Deterministic proof of the TWO manifestations of the
 * mpls_output() by-value bug, as seen by the mpls_output_process() and
 * ip_output() callers (manifestations 1 & 2 of the finding).
 *
 * (DF-0753 already covered manifestation 3 โ€” the mpls_forward() caller in
 *  mpls_input.c.  This harness covers the OTHER two callers, which are the
 *  ones cited in the DF-0754 finding.)
 *
 * This harness transcribes the EXACT control flow of:
 *   - ip_output() MPLS dispatch    sys/netinet/ip_output.c:694-700, 738-744
 *   - mpls_output_process()        sys/netproto/mpls/mpls_output.c:134-150
 *   - mpls_output()                sys/netproto/mpls/mpls_output.c:49-129
 *   - mpls_push()                  sys/netproto/mpls/mpls_output.c:152-169
 *   - mpls_swap()                  sys/netproto/mpls/mpls_output.c:171-194
 *   - mpls_pop()                   sys/netproto/mpls/mpls_output.c:196-212
 *   - m_prepend()                  sys/kern/uipc_mbuf.c:1500-1520
 *   - m_pullup()                   sys/kern/uipc_mbuf.c:2103-2158
 *   - M_PREPEND / M_MOVE_PKTHDR    sys/sys/mbuf.h
 *
 * with faithful userspace stand-ins for struct mbuf / m_freem / m_gethdr.
 *
 * The bug: mpls_output() takes `struct mbuf *m` BY VALUE (mpls_output.c:50).
 * When mpls_push/mpls_swap/mpls_pop reallocate the head mbuf (via M_PREPEND
 * or m_pullup), only the LOCAL copy of m inside mpls_output is rebound.
 * mpls_output_process() (which called mpls_output(m,...) by value at :143)
 * and ip_output() (which called mpls_output_process(m,...) by value at :695)
 * NEVER see the new head.  Two consequences:
 *
 *   MANIFESTATION 1 (double-free / leak, mpls_output.c:143-146):
 *     If mpls_output() returns an error after a realloc, mpls_output_process()
 *     does m_freem(m) on the stale pointer.  Two sub-cases:
 *       M1a: m_prepend OOM  -> m already freed inside mpls_push   -> DOUBLE-FREE
 *       M1b: m_pullup OOM    -> m already freed inside mpls_swap   -> DOUBLE-FREE
 *       M1c: PUSH ok then later op fails -> stale head freed, NEW head LEAKED
 *
 *   MANIFESTATION 2 (stale-mbuf-to-driver, ip_output.c:698 / :742):
 *     On the SUCCESS path, mpls_output_process() returns TRUE and ip_output()
 *     hands the stale `m` to ifp->if_output().  Two sub-cases:
 *       M2a: PUSH realloc succeeded -> m is the OLD (demoted) head, missing
 *            the freshly-pushed MPLS label; the NEW head is LEAKED.  Driver
 *            transmits garbage / wrong packet.
 *       M2b: SWAP/POP m_pullup realloc succeeded -> m points to FREED memory
 *            -> UAF when if_output dereferences it.
 *
 * The harness uses a POISONED ALLOCATOR (freed memory marked 0xdeadc0de,
 * matching DragonFly's INVARIANTS WEIRD_ADDR) and detects double-free / UAF.
 * An injection knob (fail_after) forces m_prepend/m_pullup OOM deterministically
 * to drive M1a/M1b.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

/* Userspace stand-ins for kernel-only defines used by the transcribed code. */
#define ENOBUFS    105   /* mpls_output.c return values */
#define ETIMEDOUT  110
#define ENOTSUP    91
typedef unsigned char boolean_t;
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

/* ------------------------------------------------------------------ */
/*  Minimal mbuf model (faithful to the fields the bug touches)        */
/*  Identical to the proven DF-0753 harness.                           */
/* ------------------------------------------------------------------ */

#define MHLEN        84
#define MLEN         100
#define M_PKTHDR     0x02
#define M_EXT        0x04
#define M_MPLSLABELED 0x4000
#define ETHER_HDR_LEN 14
#define MPLS_SHIM_LEN 4

struct mbuf {
    uint32_t magic;
    uint32_t state;           /* 0=free(poisoned), 1=live */
    struct mbuf *m_next;
    int  m_flags;
    int  m_len;
    char *m_data;
    int  pkthdr_len;
    char m_pktdat[MLEN];
    int  refcnt;
    int  label;               /* tagged by tests to identify which mbuf */
};

#define LIVE_MAGIC  0x4d42464d   /* "MBFM" */
#define DEAD_MAGIC  0xdeadc0de

static int alloc_count = 0;
static int free_count  = 0;
static int double_free_detected = 0;
static int uaf_detected = 0;
static int live_allocs = 0;      /* outstanding (not freed) mbufs = leak meter */

/* OOM injection: when > 0, the next fail_after successful mbuf_alloc()s
 * in m_prepend/m_pullup return NULL (simulating M_NOWAIT exhaustion). */
static int fail_after = -1;      /* -1 = never fail */
static int alloc_budget = 0;     /* counts down inside prepend/pullup paths */

static struct mbuf *mbuf_alloc(int flags)
{
    struct mbuf *m = calloc(1, sizeof(struct mbuf));
    assert(m);
    m->magic = LIVE_MAGIC;
    m->state = 1;
    m->m_flags = flags;
    m->m_data = m->m_pktdat;
    m->m_next = NULL;
    m->refcnt = 1;
    alloc_count++;
    live_allocs++;
    return m;
}

static struct mbuf *m_gethdr(int how, int type)
{
    (void)how; (void)type;
    return mbuf_alloc(M_PKTHDR);
}

static struct mbuf *check_live(struct mbuf *m, const char *where)
{
    if (m == NULL) return NULL;
    if (m->state == 0 || m->magic == DEAD_MAGIC) {
        printf("  [UAF] deref of freed mbuf %p (label=%d) at %s (magic=0x%x)\n",
               (void*)m, m->label, where, m->magic);
        uaf_detected++;
        return NULL;
    }
    return m;
}

static void m_free_one(struct mbuf *m)
{
    if (m == NULL) return;
    if (m->state == 0 || m->magic == DEAD_MAGIC) {
        printf("  [DOUBLE-FREE] m_freem on already-freed mbuf %p (label=%d) "
               "(magic=0x%x)\n", (void*)m, m->label, m->magic);
        double_free_detected++;
        return;
    }
    m->state = 0;
    m->magic = DEAD_MAGIC;
    free_count++;
    live_allocs--;
}

static void m_freem(struct mbuf *m)
{
    while (m) {
        struct mbuf *next = m->m_next;
        m_free_one(m);
        m = next;
    }
}

/* ------------------------------------------------------------------ */
/*  Faithful transcription of M_MOVE_PKTHDR (uipc_mbuf.c:2551)         */
/*  NOTE: does NOT clear M_PKTHDR from `from`.                         */
/* ------------------------------------------------------------------ */
static void M_MOVE_PKTHDR(struct mbuf *to, struct mbuf *from)
{
    assert(to->m_flags & M_PKTHDR);
    assert(from->m_flags & M_PKTHDR);
    to->m_flags |= (from->m_flags & (M_PKTHDR|M_MPLSLABELED));
    to->pkthdr_len = from->pkthdr_len;
}

static int M_LEADINGSPACE(struct mbuf *m)
{
    return (int)(m->m_data - m->m_pktdat);
}

/* ------------------------------------------------------------------ */
/*  M_PREPEND (mbuf.h:469)                                            */
/* ------------------------------------------------------------------ */
#define M_PREPEND(mp, plen) do {                          \
    if (M_LEADINGSPACE(*(mp)) >= (plen)) {                \
        (*(mp))->m_data -= (plen);                        \
        (*(mp))->m_len   += (plen);                        \
        (*(mp))->pkthdr_len += (plen);                     \
    } else {                                              \
        *(mp) = m_prepend_harness(*(mp), (plen));         \
        if (*(mp)) (*(mp))->pkthdr_len += (plen);         \
    }                                                     \
} while(0)

/* m_prepend (uipc_mbuf.c:1500) โ€” allocates new head, chains old.
 * BUG-CRITICAL OOM path: if alloc fails, m_freem(m) and return NULL. */
static struct mbuf *m_prepend_harness(struct mbuf *m, int len)
{
    struct mbuf *mn;
    if (fail_after >= 0 && alloc_budget >= fail_after) {
        printf("  [inject] m_prepend: alloc OOM -> m_freem(m=%p label=%d) + return NULL\n",
               (void*)m, m->label);
        m_freem(m);          /* *** OLD m is FREED here (uipc_mbuf.c:1510) *** */
        return NULL;
    }
    alloc_budget++;
    if (m->m_flags & M_PKTHDR)
        mn = m_gethdr(0, 0);
    else
        mn = mbuf_alloc(0);
    if (mn == NULL) { m_freem(m); return NULL; }
    if (m->m_flags & M_PKTHDR)
        M_MOVE_PKTHDR(mn, m);
    mn->m_next = m;               /* OLD m is chained, NOT freed */
    mn->m_len  = len;
    mn->m_data = mn->m_pktdat;
    return mn;                    /* returns NEW head */
}

/* ------------------------------------------------------------------ */
/*  m_pullup (uipc_mbuf.c:2103) โ€” slow path frees consumed source mbufs.
 *  BUG-CRITICAL OOM path: if alloc fails, m_freem(n) and return NULL. */
/* ------------------------------------------------------------------ */
static struct mbuf *m_pullup_harness(struct mbuf *n, int len)
{
    if (n->m_len >= len)
        return n;
    if (fail_after >= 0 && alloc_budget >= fail_after) {
        printf("  [inject] m_pullup: alloc OOM -> m_freem(n=%p label=%d) + return NULL\n",
               (void*)n, n->label);
        m_freem(n);          /* *** OLD n is FREED here *** */
        return NULL;
    }
    alloc_budget++;
    {
        struct mbuf *m;
        if (n->m_flags & M_PKTHDR)
            m = m_gethdr(0, 0);
        else
            m = mbuf_alloc(0);
        if (m == NULL) { m_freem(n); return NULL; }
        m->m_len = 0;
        if (n->m_flags & M_PKTHDR)
            M_MOVE_PKTHDR(m, n);
        {
            int want = len;
            struct mbuf *src = n;
            while (want > 0 && src) {
                struct mbuf *next = src->m_next;
                int cnt = src->m_len < want ? src->m_len : want;
                memcpy(m->m_pktdat + m->m_len, src->m_data, cnt);
                m->m_len += cnt;
                want     -= cnt;
                m_free_one(src);      /* consumed source mbuf FREED */
                src = next;
            }
        }
        m->m_data = m->m_pktdat;
        m->m_next = NULL;
        return m;                     /* NEW head, old n is FREED */
    }
}

/* ------------------------------------------------------------------ */
/*  MPLS label encode/decode (faithful to sys/netproto/mpls/mpls.h)    */
/* ------------------------------------------------------------------ */
struct mpls { uint32_t mpls_shim; };
#define MPLS_LABEL(s)   ((s >> 12) & 0xfffff)
#define MPLS_STACK(s)   ((s >> 8) & 1)
#define MPLS_TTL(s)     (s & 0xff)
#define MPLS_SET_LABEL(b,l) (b |= ((l & 0xfffff) << 12))
#define MPLS_SET_STACK(b,s) (b |= ((s & 1) << 8))
#define MPLS_SET_TTL(b,t)   (b |= (t & 0xff))

struct sockaddr_mpls { int smpls_op; uint32_t smpls_label; };
#define MPLSLOP_PUSH 1
#define MPLSLOP_SWAP 2
#define MPLSLOP_POP  3

struct rtentry {
    struct sockaddr_mpls shim[3];
    int nshim;
    int from_mpls;
    int rt_flags;
};
#define RTF_MPLSOPS 0x100
#define AF_MPLS 35
#define AF_INET  2

static int label_counter = 100;

/* ------------------------------------------------------------------ */
/*  THE BUGGY FUNCTIONS โ€” transcribed verbatim from the kernel.        */
/*  *** mpls_output / mpls_swap / mpls_pop take m BY VALUE ***         */
/* ------------------------------------------------------------------ */

/* mpls_push (mpls_output.c:152) โ€” takes struct mbuf** so it DOES update
 * mpls_output's local m.  The bug is one level up. */
static int mpls_push(struct mbuf **m, uint32_t label, int s, int ttl)
{
    uint32_t buf = 0;
    M_PREPEND(m, MPLS_SHIM_LEN);
    if (*m == NULL) return (ENOBUFS);   /* mpls_output.c:158-159 */
    MPLS_SET_LABEL(buf, label);
    MPLS_SET_STACK(buf, s);
    MPLS_SET_TTL(buf, ttl);
    {
        struct mpls *p = (struct mpls*)(*m)->m_data;
        p->mpls_shim = buf;
    }
    (*m)->m_flags |= M_MPLSLABELED;
    (*m)->label = ++label_counter;
    printf("  mpls_push: new head m=%p label=%d\n", (void*)(*m), (*m)->label);
    return 0;
}

/* mpls_swap (mpls_output.c:171) โ€” takes m BY VALUE. */
static int mpls_swap(struct mbuf *m, uint32_t label)
{
    if (m->m_len < MPLS_SHIM_LEN) {
        m = m_pullup_harness(m, MPLS_SHIM_LEN);   /* local rebind ONLY */
        if (m == NULL) return (ENOBUFS);
    }
    {
        struct mpls *p = (struct mpls*)m->m_data;
        uint32_t buf = p->mpls_shim;
        int ttl = MPLS_TTL(buf);
        if (--ttl <= 0) return (ETIMEDOUT);
        buf = 0;
        MPLS_SET_LABEL(buf, label);
        MPLS_SET_TTL(buf, ttl);
        p->mpls_shim = buf;
    }
    return 0;
}

/* mpls_pop (mpls_output.c:196) โ€” takes m BY VALUE. */
static int mpls_pop(struct mbuf *m, int *sbit)
{
    if (m->m_len < MPLS_SHIM_LEN) {
        m = m_pullup_harness(m, MPLS_SHIM_LEN);   /* local rebind ONLY */
        if (m == NULL) return (ENOBUFS);
    }
    {
        struct mpls *p = (struct mpls*)m->m_data;
        uint32_t buf = p->mpls_shim;
        *sbit = MPLS_STACK(buf);
    }
    m->m_data += MPLS_SHIM_LEN;
    m->m_len  -= MPLS_SHIM_LEN;
    return 0;
}

/* mpls_output (mpls_output.c:49) โ€” takes m BY VALUE.  HEART OF THE BUG. */
static int mpls_output(struct mbuf *m, struct rtentry *rt)
{
    int i, error = 0;
    int stackempty;
    int ttl = 255;
    stackempty = rt->from_mpls ? 0 : 1;

    for (i = 0; i < rt->nshim; i++) {
        struct sockaddr_mpls *s = &rt->shim[i];
        switch (s->smpls_op) {
        case MPLSLOP_PUSH:
            error = mpls_push(&m, s->smpls_label,
                              (stackempty && i == 0) ? 1 : 0, ttl);
            if (error) return (error);            /* :88 โ€” local m lost */
            stackempty = 0;
            m->m_flags |= M_MPLSLABELED;
            break;
        case MPLSLOP_SWAP:
            if (stackempty) return (ENOTSUP);
            error = mpls_swap(m, s->smpls_label);  /* m by value! */
            if (error) return (error);             /* :102 */
            break;
        case MPLSLOP_POP:
            if (stackempty) return (ENOTSUP);
            { int sb; error = mpls_pop(m, &sb); }  /* m by value! */
            if (error) return (error);             /* :114 */
            break;
        default:
            return (ENOTSUP);
        }
    }
    return (error);     /* *** caller's m is UNCHANGED โ€” stale *** */
}

/* ------------------------------------------------------------------ */
/*  mpls_output_process (mpls_output.c:134) โ€” BY VALUE caller.         */
/*  MANIFESTATION 1 lives here: error-path m_freem(m) at :145.         */
/* ------------------------------------------------------------------ */
static boolean_t mpls_output_process(struct mbuf *m, struct rtentry *rt)
{
    int error;
    if (!(rt->rt_flags & RTF_MPLSOPS))   /* :140 */
        return TRUE;
    error = mpls_output(m, rt);          /* :143 โ€” m by VALUE */
    if (error) {
        printf("  mpls_output_process: error=%d -> m_freem(stale m=%p label=%d)\n",
               error, (void*)m, m ? m->label : -1);
        m_freem(m);                       /* :145 โ€” DOUBLE-FREE / UAF */
        return FALSE;
    }
    return TRUE;                          /* success: caller uses stale m */
}

/* ------------------------------------------------------------------ */
/*  ip_output MPLS dispatch (ip_output.c:694-700) โ€” the upstream caller.
 *  MANIFESTATION 2 lives here: success-path ifp->if_output(stale m) at :698. */
/* ------------------------------------------------------------------ */
static int if_output_deref_count = 0;
static int if_output_received_label = -1;

static int fake_if_output(struct mbuf *m)
{
    if_output_deref_count++;
    printf("  if_output: received m=%p label=%d\n", (void*)m, m ? m->label : -1);
    /* The driver dereferences m (reads m_len, m_data, m_flags, pkthdr.len).
     * If m is freed memory -> UAF.  If m is a demoted head -> wrong packet. */
    if (check_live(m, "if_output(m) deref")) {
        if_output_received_label = m->label;
        printf("  if_output: m LIVE, m_len=%d pkthdr.len=%d M_PKTHDR=%d "
               "M_MPLSLABELED=%d\n",
               m->m_len, m->pkthdr_len,
               (m->m_flags & M_PKTHDR)?1:0,
               (m->m_flags & M_MPLSLABELED)?1:0);
        /* faithfully: if_output/ifq_dispatch consumes (frees) the mbuf */
        m_freem(m);
    }
    return 0;
}

/* Simulate ip_output()'s MPLS dispatch + if_output call (ip_output.c:694-700). */
static void ip_output_mpls_dispatch(struct mbuf *m, struct rtentry *rt)
{
    boolean_t cont;
    printf("  ip_output: m=%p label=%d (head)\n", (void*)m, m->label);
    cont = mpls_output_process(m, rt);            /* ip_output.c:695 โ€” by value */
    if (!cont) {
        printf("  ip_output: mpls_output_process=FALSE -> goto done (m already freed at :145)\n");
        return;                                    /* goto done */
    }
    printf("  ip_output: mpls_output_process=TRUE -> ifp->if_output(stale m=%p)\n",
           (void*)m);
    (void)fake_if_output(m);                       /* ip_output.c:698 โ€” STALE m */
}

/* ------------------------------------------------------------------ */
/*  Test helpers                                                       */
/* ------------------------------------------------------------------ */
static struct mbuf *make_ip_pkt(int leading_space)
{
    struct mbuf *m = m_gethdr(0, 0);
    m->m_data = m->m_pktdat + leading_space;
    memset(m->m_pktdat, 0xAA, sizeof(m->m_pktdat));
    /* dummy IP header (first 4 bytes = version/IHL + ttl byte at offset 8) */
    m->m_data[0] = 0x45; m->m_data[8] = 64;       /* ip_ttl = 64 */
    m->m_len = 20;
    m->pkthdr_len = 20;
    m->label = ++label_counter;
    return m;
}

static struct mbuf *make_mpls_frag_chain(void)
{
    /* fragmented MPLS chain: first mbuf m_len=2 (< 4) -> triggers m_pullup.
     * Write a valid MPLS shim (TTL=64 in the low byte) across the split so
     * that after m_pullup reassembles it, mpls_swap sees a live TTL and
     * SUCCEEDS (returns 0) โ€” exercising the success-path stale-mbuf-to-driver
     * (manifestation 2b) rather than the error path. */
    struct mbuf *m = m_gethdr(0, 0);
    struct mbuf *m2 = mbuf_alloc(0);
    uint32_t shim = 0;
    MPLS_SET_LABEL(shim, 100);
    MPLS_SET_STACK(shim, 1);
    MPLS_SET_TTL(shim, 64);          /* TTL=64 so SWAP succeeds (--ttl=63) */
    m->m_flags |= M_MPLSLABELED;
    m->m_len = 2;
    /* scatter the 4-byte shim: first 2 bytes in m, next 2 in m2 */
    memcpy(m->m_data, &shim, 2);
    memcpy(m2->m_pktdat, ((char*)&shim) + 2, 2);
    m2->m_len = 32;
    memset(m2->m_pktdat + 2, 0xBB, 30);
    m2->m_data = m2->m_pktdat;
    m2->label = ++label_counter;
    m->m_next = m2;
    m->pkthdr_len = 34;
    m->label = ++label_counter;
    return m;
}

static void reset_accounting(void)
{
    alloc_count = free_count = 0;
    double_free_detected = uaf_detected = 0;
    live_allocs = 0;
    if_output_deref_count = 0;
    if_output_received_label = -1;
    fail_after = -1;
    alloc_budget = 0;
}

static void report(const char *label, const char *expect)
{
    printf("\n  --- %s ---\n", label);
    printf("  allocs=%d frees=%d  outstanding(live)=%d  double_free=%d  uaf=%d\n",
           alloc_count, free_count, live_allocs, double_free_detected, uaf_detected);
    printf("  if_output derefs=%d  last_received_label=%d\n",
           if_output_deref_count, if_output_received_label);
    printf("  EXPECT: %s\n", expect);
    if (double_free_detected)
        printf("  *** MANIFESTATION 1 (DOUBLE-FREE) CONFIRMED ***\n");
    if (uaf_detected)
        printf("  *** UAF CONFIRMED ***\n");
    if (live_allocs > 0 && !double_free_detected)
        printf("  *** MBUF LEAK: %d outstanding alloc(s) not freed ***\n", live_allocs);
}

/* ================================================================== */
/*  MAIN โ€” run all manifestation scenarios                             */
/* ================================================================== */
int main(void)
{
    struct rtentry rt_push, rt_swap, rt_push_then_swap;

    printf("================================================================\n");
    printf("DF-0754 harness โ€” mpls_output() by-value bug\n");
    printf("Focus: mpls_output_process() [M1] + ip_output() [M2] callers\n");
    printf("(DF-0753 already covered the mpls_forward()/mpls_input.c caller)\n");
    printf("================================================================\n\n");

    /* ---- MANIFESTATION 1a: PUSH + m_prepend OOM -> DOUBLE-FREE ----
     * mpls_push -> M_PREPEND -> m_prepend OOM -> m_prepend does
     * m_freem(m) and returns NULL.  mpls_output returns ENOBUFS.
     * mpls_output_process:145 does m_freem(m) on the SAME (freed) m. */
    printf("----------------------------------------------------------------\n");
    printf("M1a: PUSH + m_prepend OOM -> mpls_output_process:145 DOUBLE-FREE\n");
    printf("     (mpls_output.c:143 by-value, :145 m_freem on already-freed m)\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    memset(&rt_push, 0, sizeof(rt_push));
    rt_push.from_mpls = 1;
    rt_push.rt_flags = RTF_MPLSOPS;
    rt_push.nshim = 1;
    rt_push.shim[0].smpls_op = MPLSLOP_PUSH;
    rt_push.shim[0].smpls_label = 999;
    {
        struct mbuf *m = make_ip_pkt(/*leading_space=*/0);  /* force m_prepend */
        fail_after = 0;  /* first m_prepend alloc -> OOM */
        ip_output_mpls_dispatch(m, &rt_push);
    }
    report("M1a result", "double_free >= 1 (mpls_output_process:145 frees freed m)");

    /* ---- MANIFESTATION 1b: SWAP + m_pullup OOM -> DOUBLE-FREE ----
     * mpls_swap -> m_pullup OOM -> m_pullup does m_freem(n) and returns NULL.
     * mpls_swap returns ENOBUFS, mpls_output returns ENOBUFS,
     * mpls_output_process:145 m_freem(m) on freed m. */
    printf("\n----------------------------------------------------------------\n");
    printf("M1b: SWAP + m_pullup OOM -> mpls_output_process:145 DOUBLE-FREE\n");
    printf("     (mpls_swap takes m by value; m_pullup frees old m on OOM)\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    memset(&rt_swap, 0, sizeof(rt_swap));
    rt_swap.from_mpls = 1;
    rt_swap.rt_flags = RTF_MPLSOPS;
    rt_swap.nshim = 1;
    rt_swap.shim[0].smpls_op = MPLSLOP_SWAP;
    rt_swap.shim[0].smpls_label = 200;
    {
        struct mbuf *m = make_mpls_frag_chain();  /* m_len=2 -> m_pullup */
        fail_after = 0;  /* m_pullup alloc -> OOM */
        ip_output_mpls_dispatch(m, &rt_swap);
    }
    report("M1b result", "double_free >= 1 (stale m freed by m_pullup, re-freed at :145)");

    /* ---- MANIFESTATION 1c: PUSH ok, then later op errors -> LEAK ----
     * PUSH reallocs (new head mn, old m chained under it).  Then a subsequent
     * op errors (here: unknown op -> ENOTSUP at mpls_output.c:124, which is
     * the cleanest "push-then-error" since it does no alloc/free itself).
     * mpls_output returns error.  mpls_output_process:145 m_freem(stale m)
     * frees only the OLD chain reachable from the stale pointer; the NEW head
     * mn is NOT reachable from stale m (mn->m_next=m, not m->m_next=mn) and
     * is therefore LEAKED.  This is the "new-head leak (push-then-error)"
     * variant named in the finding. */
    printf("\n----------------------------------------------------------------\n");
    printf("M1c: PUSH ok then later op ENOTSUP -> stale head freed, NEW head LEAKED\n");
    printf("     (mpls_output_process:145 frees stale m; new pushed head unreachable)\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    memset(&rt_push_then_swap, 0, sizeof(rt_push_then_swap));
    rt_push_then_swap.from_mpls = 1;
    rt_push_then_swap.rt_flags = RTF_MPLSOPS;
    rt_push_then_swap.nshim = 2;
    rt_push_then_swap.shim[0].smpls_op = MPLSLOP_PUSH;
    rt_push_then_swap.shim[0].smpls_label = 999;
    rt_push_then_swap.shim[1].smpls_op = 99;   /* unknown -> default: ENOTSUP */
    rt_push_then_swap.shim[1].smpls_label = 200;
    {
        struct mbuf *m = make_ip_pkt(/*leading_space=*/0);  /* force m_prepend */
        fail_after = -1;  /* PUSH succeeds, second op errors without alloc */
        ip_output_mpls_dispatch(m, &rt_push_then_swap);
    }
    report("M1c result", "double_free=0 (old chain freed once), "
                       "live_allocs >= 1 (new pushed head LEAKED)");

    /* ---- MANIFESTATION 2a: PUSH realloc, SUCCESS -> stale m to driver ----
     * PUSH succeeds (new head mn created, old m chained under it).
     * mpls_output returns 0, mpls_output_process returns TRUE.
     * ip_output:698 if_output(stale m) โ€” m is the OLD head, missing the
     * freshly-pushed MPLS label.  The driver transmits the WRONG packet.
     * New head mn is LEAKED. */
    printf("\n----------------------------------------------------------------\n");
    printf("M2a: PUSH realloc SUCCESS -> ip_output:698 if_output(STALE m)\n");
    printf("     (driver gets demoted old head; new pushed head leaked)\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    {
        struct mbuf *m = make_ip_pkt(/*leading_space=*/0);  /* force m_prepend */
        fail_after = -1;
        ip_output_mpls_dispatch(m, &rt_push);
    }
    report("M2a result", "if_output_received_label == original IP mbuf label (STALE), "
                       "live_allocs >= 1 (new head leaked)");

    /* ---- MANIFESTATION 2b: SWAP m_pullup realloc, SUCCESS -> UAF ----
     * mpls_swap -> m_pullup (m_len<4) -> realloc, frees old m, returns new.
     * mpls_swap's local m is rebound; mpls_output's & caller's m are NOT.
     * mpls_output returns 0, mpls_output_process returns TRUE.
     * ip_output:698 if_output(stale m) โ€” m is FREED memory -> UAF in driver. */
    printf("\n----------------------------------------------------------------\n");
    printf("M2b: SWAP m_pullup realloc SUCCESS -> ip_output:698 if_output(FREED m)\n");
    printf("     (stale m is freed memory; driver deref = UAF)\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    {
        struct mbuf *m = make_mpls_frag_chain();  /* m_len=2 -> m_pullup realloc */
        fail_after = -1;
        ip_output_mpls_dispatch(m, &rt_swap);
    }
    report("M2b result", "uaf >= 1 (driver deref of freed m at if_output)");

    /* ---- CONTROL: PUSH with adequate leading space -> NO bug ----
     * 14 bytes headroom >= 4 -> M_PREPEND fast path (m_data -= 4), no realloc.
     * Caller's m stays valid.  Proves the bug is realloc-dependent. */
    printf("\n----------------------------------------------------------------\n");
    printf("CONTROL: PUSH with leading_space=14 -> fast path, NO realloc, NO bug\n");
    printf("----------------------------------------------------------------\n");
    reset_accounting();
    {
        struct mbuf *m = make_ip_pkt(/*leading_space=*/ETHER_HDR_LEN);
        fail_after = -1;
        ip_output_mpls_dispatch(m, &rt_push);
    }
    report("CONTROL result", "double_free=0, uaf=0, live_allocs=0 (clean)");

    printf("\n================================================================\n");
    printf("DF-0754 harness complete.\n");
    printf("Root cause: mpls_output() takes `struct mbuf *m` BY VALUE\n");
    printf("  (mpls_output.c:50).  Callers mpls_output_process() (:143) and\n");
    printf("  ip_output() (:695) never see the new head after a realloc.\n");
    printf("  M1: error path m_freem(stale) at :145 = DOUBLE-FREE / leak.\n");
    printf("  M2: success path if_output(stale) at ip_output.c:698 = stale/UAF.\n");
    printf("Fix: mpls_output(struct mbuf **mp) propagates head through *mp\n");
    printf("  (same fix as DF-0753 โ€” closes BOTH findings).\n");
    return 0;
}