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.
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
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.
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.
rmw_rbio() -> rbio_try_park(), the fullness recheck. This one.
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().
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.
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.
#217 (cand55) and validated by
after-boot-kasan-fix.sh: P1_online CORRUPT at the default park knobs,
stopping on a KASAN report, a refcount_t warning (a mis-balanced
get/put), or any raid56 WARNING (a leaked park reference shows up as
rbios that never free).#78 stays on the pre-fix KASAN kernel as the control. A report
there would confirm the free stack directly; its absence does not weaken the
analysis above, which stands on the struct offset and the lock/reference
argument.