DF-0994 / sastart_harness.c
/* * DF-0994 — userspace harness replicating the ERR_PENDING drain loop of * sastart() in sys/bus/cam/scsi/scsi_sa.c. * * The bug: `bio` is fetched once (bioq_first) before the ERR_PENDING branch. * The EOF_PENDING sub-branch does biodone(done_bio) then `goto again` WITHOUT * refreshing `bio`. The `again:` label re-enters and operates on the SAME * (already-biodone'd) bio: * - queue_count-- on an already-decremented count (underflow) * - bioq_remove on an already-removed bio (TAILQ corruption) * - biodone(done_bio) a SECOND time (double biodone / UAF) * * This harness models a 3-bio queue with SA_FLAG_EOF_PENDING set and walks the * buggy loop, showing queue_count underflow, double-dispatch of the same bio, * and TAILQ corruption. Compile with -DFIXED for the patched loop (refresh * bio before goto again). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define SA_FLAG_ERR_PENDING 0x10 #define SA_FLAG_EOF_PENDING 0x04 struct bio { int id; int done_count; int removed; struct bio *next; }; struct bioq { struct bio *head; int count; }; static struct bio *bioq_first(struct bioq *q){ return q->head; } static void bioq_remove(struct bioq *q, struct bio *b){ struct bio **pp = &q->head; while (*pp && *pp != b) pp = &(*pp)->next; if (*pp == b){ *pp = b->next; b->removed = 1; q->count--; } else { printf(" !! bioq_remove: bio %d NOT in queue (already removed: %s) " "-> TAILQ corruption / no-op\n", b->id, b->removed?"yes":"no"); } } static void biodone(struct bio *b){ b->done_count++; printf(" biodone(bio %d) -> total biodone calls on this bio: %d%s\n", b->id, b->done_count, b->done_count>1?" <-- DOUBLE biodone (UAF)":""); } int main(void){ struct bio b3={3,0,0,NULL}, b2={2,0,0,&b3}, b1={1,0,0,&b2}; struct bioq q={&b1,3}; int queue_count = 3; int flags = SA_FLAG_ERR_PENDING | SA_FLAG_EOF_PENDING; printf("queue: bio1->bio2->bio3 queue_count=%d flags=0x%x (EOF_PENDING)\n", queue_count, flags); struct bio *bio = bioq_first(&q); /* set ONCE, outside loop */ int iters = 0; if ((flags & SA_FLAG_ERR_PENDING) != 0){ struct bio *done_bio; again: if (++iters > 16){ printf(" (safety: >16 iters, stopping loop)\n"); goto end_loop; } printf("--- pass %d: bio=0x%x(id=%d) queue_count=%d ---\n", iters, (unsigned)(uintptr_t)bio, bio?bio->id:-1, queue_count); queue_count--; /* :1603 always */ bioq_remove(&q, bio); /* :1604 */ done_bio = bio; if ((flags & SA_FLAG_EOF_PENDING) != 0){ if (bioq_first(&q) != NULL){ biodone(done_bio); /* :1624 */ #ifdef FIXED bio = bioq_first(&q); /* FIX: refresh before again */ #endif goto again; /* :1625 stale bio */ } } bio = bioq_first(&q); /* :1628 (only if no goto) */ biodone(done_bio); } end_loop: ; printf("\nFINAL: queue_count=%d (started 3; correct=0; <0 = underflow)\n", queue_count); printf(" bio1.biodone=%d bio2.biodone=%d bio3.biodone=%d " "(any >1 = DOUBLE biodone/UAF)\n", b1.done_count, b2.done_count, b3.done_count); int dbl = (b1.done_count>1)||(b2.done_count>1)||(b3.done_count>1); if (queue_count < 0 || dbl) printf("RESULT: BUG CONFIRMED — queue_count underflow and/or double " "biodone on stale bio (kernel: bio UAF + TAILQ corruption)\n"); else printf("RESULT: NO BUG (fix: bio refreshed before goto again)\n"); return (queue_count < 0 || dbl) ? 1 : 0; } |