DragonFlyBSD Kernel Audit
DF-0994 / sastart_harness.c
← back to finding ↓ download raw
/*
 * 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;
}