Parked-rbio use-after-free: root cause

2026-09-17. Companion to dhive-rmw-listdel-oops-20260916.txt (the raw Oops and first-pass analysis). That file characterised the crash; this one names the racing window, the free site, and the fix.

The crash, restated

dhive kernel #77 (rebased 6.18 lane, CORRUPT model) wedged with every CPU spinning on stripe_hash_table->parked_lock. The task holding the lock had died first, in rmw_rbio_work():

list_del on a list_head at rbio+0xe0 whose ->next was NULL
    -> NULL dereference at rmw_rbio_work+0x3d7, parked_lock still held
    -> all other CPUs in __pv_queued_spin_lock_slowpath():
       btrfs_flush_parked_rbios() from commit and ordered-extent waiters,
       parked_rbios_timeout_work(), other rmw_rbio_work()s
    -> soft lockup on every CPU, taint G D W L

Which list_head

pahole -C btrfs_raid_bio on the matching vmlinux (dhive /mnt/linux/linux/vmlinux, the #78 KASAN build of the same source):

member offset
hash_list 8
stripe_cache 24
work 40
plug_list 208
parked_node 224 = 0xe0
park_deadline 240
refs 284

So the faulting list_del was on parked_node, i.e. an inlined rbio_unpark_locked(). Combined with the rmw_rbio_work symbol that leaves exactly two candidate call sites, both inlined into that function.

Which call site

  1. lock_stripe_add() -> unpark_ready_rbio(cur) on a merge target. Not this one. cur was found on h->hash_list under h->lock, and the only path that frees an rbio removes it from that hash under the same lock (unlock_stripe() -> list_del_init(&rbio->hash_list), refcount_dec, then free_raid_bio() from rbio_orig_end_io()). While h->lock is held, cur still carries the hash list's reference and cannot be freed.

  2. rmw_rbio() -> rbio_try_park(), the fullness recheck. This one.

The window

rbio_try_park() publishes the rbio and then keeps using it:

spin_lock(&table->parked_lock);
set_bit(RBIO_PARKED_BIT, ...);
...
list_add_tail(&rbio->parked_node, &table->parked);
atomic_inc(&fs_info->stripe_parked_now);
spin_unlock(&table->parked_lock);          <-- rbio is now public
atomic64_inc(&fs_info->stripe_park_stats.parked);
queue_delayed_work(...);
if (rbio_is_full(rbio)) {                  <-- touches rbio
        spin_lock(&table->parked_lock);
        if (test_bit(RBIO_PARKED_BIT, &rbio->flags)) {
                rbio_unpark_locked(rbio);  <-- list_del at +0xe0
        ...

Nothing owns the rbio across that gap. A parked rbio holds exactly two references -- the initial one from alloc_rbio() and the one lock_stripe_add() took for the stripe hash list -- and its completion consumes both: rbio_orig_end_io() -> unlock_stripe() (hash reference) -> free_raid_bio() (initial reference) -> kfree(). The parking thread holds no reference of its own.

So once parked_lock is dropped, any flusher (btrfs_flush_parked_rbios() from run retirement or an ordered-extent waiter, unpark_ready_rbio() from a merge, or parked_rbios_timeout_work()) may unpark the rbio, start its RMW, let it run to completion, and free it -- while the parking thread is still inside rbio_try_park(), about to dereference it.

Free site: free_raid_bio() called from rbio_orig_end_io() at the end of the RMW that the flusher started. Use site: rbio_is_full() and the test_bit/rbio_unpark_locked() recheck in rbio_try_park().

Why it is rare

rbio_unpark_locked() clears RBIO_PARKED_BIT under parked_lock, and the recheck tests that bit under the same lock. So the ordinary interleaving is benign: the stale thread reads the cleared bit and returns without touching parked_node. The crash needs the freed object to have been reused by an unrelated allocation whose bytes happen to leave that bit set -- and then parked_node is that allocation's data, not a list. btrfs_raid_bio is kzalloced, so a reuse as another rbio would zero the flags and be caught by the bit test; the observed ->next == NULL with the bit apparently set points at a foreign slab user.

That also explains three clean KASAN passes (~10 h of P1 CORRUPT across dhive #78 and bhive #216) without a report: the window is a handful of instructions, and most of the time the bit guard swallows it. KASAN would fire on the first stale read (rbio_is_full() takes rbio->bio_list_lock), so the repro is sound -- it simply has not caught the interleaving yet.

Fix

8e2f9e14ca63 "btrfs: stripe_alloc: hold a reference on parked rbios" (candidate refs/cand/parkref/zb64 = 8f67cef3e02c, bhive branch zb64-cand55).

Make the parked list an owner, like the hash list and the stripe cache already are. rbio_try_park() does refcount_inc(&rbio->refs) before publishing; whoever takes the rbio off the list inherits that reference and drops it with free_raid_bio() once done -- always before start_async_work(), because after the hand-off the rbio may complete and be freed at any moment.

Drop sites: the rbio_try_park() recheck, unpark_ready_rbio(), btrfs_flush_parked_rbios()'s second loop, parked_rbios_timeout_work()'s second loop. The latter two also walk unparked rbios on a local list after dropping parked_lock, and the timeout path reads ->flags and ->park_stuck_deadline for its stats after unparking; the reference covers those too.

The park reference is never the last one at any drop site -- the stripe lock's reference outlives all of them -- so the actual free still happens where it always did, in the completion path.

What still needs proving