biodone vs geom_up
Laier, Max
max.laier at isilon.com
Mon Oct 6 22:53:16 UTC 2014
It seems to me that there is a problem with biodone (particular vs. g_disk_start/g_disk_done_single):
void
biodone(struct bio *bp)
{
struct mtx *mtxp;
void (*done)(struct bio *);
// ...
done = bp->bio_done;
if (done == NULL) {
mtxp = mtx_pool_find(mtxpool_sleep, bp);
mtx_lock(mtxp);
bp->bio_flags |= BIO_DONE; // XXX [0]
wakeup(bp);
mtx_unlock(mtxp);
} else {
bp->bio_flags |= BIO_DONE; // XXX [1]
done(bp); // XXX [2]
}
}
static void
g_disk_start(struct bio *bp)
{
// ...
case BIO_READ:
case BIO_WRITE:
d_maxsize = (bp->bio_cmd == BIO_DELETE) ?
dp->d_delmaxsize : dp->d_maxsize;
if (bp->bio_length <= d_maxsize) {
bp->bio_disk = dp;
bp->bio_to = (void *)bp->bio_done;
bp->bio_done = g_disk_done_single; // XXX [3]
// ...
dp->d_strategy(bp);
break;
// ...
}
static void
g_disk_done_single(struct bio *bp)
{
struct bintime now;
struct g_disk_softc *sc;
bp->bio_completed = bp->bio_length - bp->bio_resid;
bp->bio_done = (void *)bp->bio_to;
bp->bio_to = LIST_FIRST(&bp->bio_disk->d_geom->provider);
// ...
g_io_deliver(bp, bp->bio_error); // XXX [4]
}
And a normal caller doing something like:
void *
g_read_data(struct g_consumer *cp, off_t offset, off_t length, int *error)
{
// ...
bp = g_alloc_bio();
bp->bio_cmd = BIO_READ;
bp->bio_done = NULL;
g_io_request(bp, cp);
errorc = biowait(bp, "gread");
if (error != NULL)
*error = errorc;
g_destroy_bio(bp);
// ...
}
With this code, it is possible that thread 1(the i/o initiator) only gets to biowait *after* the g_io_request() has already made it back, called (the first) biodone, set BIO_DONE at [1], and the bio is now either queued in the g_bio_run_up.bio_queue, or in direct dispatch (via g_io_deliver at [4] via done(bp) [aka bio_done set at [3] called at [2]].
biowait will just return (because BIO_DONE is set), and the initiator thread will g_destroy_bio while that bio is still being worked on, on the g_up thread ... use-after-free.
Does anyone see a reason why we need to set BIO_DONE at [1] at all? It seems to me that the default case, where there is no callback left [0], is enough?
Removing the BIO_DONE setting at [1] resolves the issue I'm seeing, but I wonder if there is a better way to resolve this? Should g_disk_start() clone a bio for its callback instead? Are there other places that set a bio_done callback on the way down?
Any comments, questions, or answers greatly appreciated.
Thanks,
Max
More information about the freebsd-geom
mailing list