xa_close leaks xdisk_lk and releases wrong lock on raced-destruction defensive paths
Summary
Two defensive early-return paths in xa_close copied from xa_open but mistyped: both return while still holding xdisk_lk and terminating-path branch releases sc->lk (never acquired) instead of xdisk_lk. Result: leaked exclusive xdisk_lk deadlocking every subsequent xdisk operation OR lockmgr corruption/panic from releasing unowned lock. Correct pattern visible in xa_open lines 703-715. [VERIFIED pass2: reproduced both - si_drv1 NULL path returns holding exclusive xdisk_lk with probe thread wedged forever on the leaked lock, terminating path releases never-acquired sc->lk panicking lockmgr LK_RELEASE no lock held; D_TRACKCLOSE makes the raced path reachable by any node holder; unlock-pattern fix-validated]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2533 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df253x_harness.c | β | 15.7 KB | view raw | |
| build.sh | β | 207 B | view raw | |
| run.sh | β | 218 B | view raw | |
| run.log | β | 542 B | view raw | |
| run.patched.log | β | 864 B | view raw | |
| panic.txt | β | 3.2 KB | view raw | |
| serial_console.baseline.log | β | 14.0 KB | view raw | |
| fix_build.log | β | 284 B | view raw | |
| fix_run.log | β | 1.8 KB | view raw | |
| sha256_gate.log | β | 65 B | view raw | |
| env.txt | β | 984 B | view raw | |
| fix.diff | β | 694 B | view raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| verdict.json | β | 4.5 KB | view raw |
VERDICT β DF-2533 (REPRODUCED: kernel panic + permanent lock leak)
Bottom line
Both defensive early-return paths in xa_close are defective exactly as
claimed, and both defects were mechanically demonstrated by driving the REAL
xa_close (verbatim, sha256-gated, compiled into the guest kernel) into each
path with fabricated raced-destruction state:
-
Path 1 β
si_drv1 == NULL(xdisk.c:769-770): returns holding the exclusively-acquiredxdisk_lk(leak). Observed:xa_close returned 6(ENXIO) andxdisk_lk lockstatus=2 after return (LK_EXCLUSIVE=2 => EXCLUSIVE LOCK LEAKED). A probe thread running the REALxa_openon another CPU wedged forever on the leaked lock (started=1 finished=0 => STUCK FOREVER) β the leak deadlocks every later xdisk subsystem operation:xdisk_open,xdisk_close,xdisk_ioctl(XDISKIOCATTACH),xa_open,xa_close,xaio_rcvdmsg(every dmsg rx dispatch),xa_terminate_checkcallers. The harness released the lock afterwards only to keep the guest alive; a production caller has no owner to release it: the leak is permanent. -
Path 2 β
sc->terminating(xdisk.c:771-773): releasessc->lk, a lock that was NEVER acquired, while still holdingxdisk_lk. Observed: unconditional kernel panicpanic: lockmgr: LK_RELEASE: no lock heldbacktracelockmgr_release() <- xa_close() at xa_close+0x183β kern_lock.c:768 panics on any release of a lock with no holders; this is NOT INVARIANTS-gated, so production kernels panic too. Pre-call state printed:sc->lk.lk_count=0x0(lockinit leaves count 0). Had a kernel not panicked,xdisk_lkwould additionally be leaked on this path as well.
The correct pattern exists 60 lines above in xa_open (:703-715): both of
its defensive paths lockmgr(&xdisk_lk, LK_RELEASE) before returning.
Production reachability (honest quantification)
- The device ops carry
D_TRACKCLOSE(xdisk.c:188), so devfs dispatchesd_closefor EVERY close of a/dev/xa%dnode, not just the last (sys/vfs/devfs/devfs_vnops.c:1104/1170). Two processes holding the node open: peer drops the span AND the first closer drivesopencntto 0 βxa_terminate_checkfrees the softc and NULLssi_drv1(:665-680) underxdisk_lkβ the second closer entersxa_close, acquiresxdisk_lk, readssi_drv1 == NULLand takes the leak path. A variant:xa_open's BLK_OPEN error unwind (:749-753) runsxa_terminate_checkwhile devfs still owes the error-pathd_close. - The
terminatingbranch requires observingsi_drv1 != NULLtogether withsc->terminating == 1; in the current code both transitions happen underxdisk_lkinsidexa_terminate_check, so this branch is defensive (dead today) β but it is a booby trap: any future reordering (e.g. moving theterminating = 1store, or splitting the lock) turns it into the panic above. The panic was verified mechanically with fabricated state. - Threat model (per team directive): xdisk devices are created by root via
dmsg attach, but the resulting
/dev/xa%dnodes are ordinary block devices; ANY holder of an open node can triggerxa_closeat the moment the raced destruction state arises. Impact ceiling: permanent subsystem wedge of all xd* devices plus the dmsg rx machinery (local DoS), and β on the terminating path β an unconditional kernel panic. No memory-corruption primitive: no uid=0 chain exists for this bug class; the escalation ceiling is denial of service.
Exploit chain
Not memory corruption. Chain is: (unprivileged holder of an xa node)
close(2) concurrent with span-loss teardown β leaked exclusive xdisk_lk β
every subsequent xdisk/dmsg operation sleeps forever (xaopen/xawait
stuck threads pile up) β subsystem-wide local DoS; or the terminating
variant β instant node panic.
Fix validation (fixed)
fix.diff mirrors the xa_open pattern: both defensive paths release
xdisk_lk; the terminating branch no longer touches sc->lk. Applied in
guest /usr/src (both DF-2533+DF-2534 diffs, combined tree sha256
15d189799d499d96ee6bbb5796ef3b3c5e316fce06d15507a47776119ae43385),
kernel rebuilt (#3 Mon Oct 19 21:13:06) and the EXACT same triggers re-run:
- baseline: lockstatus=2 after
xa_closereturn, probe thread stuck; patched: lockstatus=0, probe completes (run.patched.log) - baseline:
panic: lockmgr: LK_RELEASE: no lock held; patched: clean ENXIO return, no panic,sc->lk.lk_count=0x0untouched
PoC notes
- The harness kernels were built from the verbatim guest source (sha256
gate in build.sh, hash recorded in env.txt/sha256_gate.log); only the
external registration calls were interposed via pre-include
#definerenames βxa_close/xa_openmachine code is the shipping code. - The probe thread calls the real
xa_open, not a stubbed lock take. - Harness iterations: first race-hammer version starved a CPU (LWKT
cooperative scheduler) and wedged the guest mid-run; fixed with periodic
1-tick
tsleepin the hammer loops (harness-only artifact, unrelated to the finding).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
Unprivileged holder of a root-created /dev/xa%d node close(2)s concurrently with span-loss teardown: a first closer (or the xa_open BLK_OPEN error unwind) drives opencnt to 0 so xa_terminate_check frees the softc and NULLs si_drv1 under xdisk_lk; D_TRACKCLOSE makes devfs dispatch d_close for every close, so the raced second close enters xa_close, reads si_drv1==NULL and returns holding xdisk_lk exclusively and permanently: every subsequent xdisk_open/xdisk_close/xdisk_ioctl/xa_open/xa_close/xaio_rcvdmsg blocks forever (xaopen/xawait threads pile up; root cannot even attach or detach anymore). The terminating variant instead panics the node instantly (lockmgr LK_RELEASE of a never-held lock). Impact ceiling: local DoS / kernel panic.
Evidence (decisive lines)
run.log (lockstatus=2 LEAKED + probe STUCK FOREVER lines), panic.txt (panic: lockmgr: LK_RELEASE: no lock held; xa_close+0x183 backtrace; sc->lk.lk_count=0x0 printed pre-call), run.patched.log (lockstatus=0, probe finished=1, no panic), fix_build.log, sha256_gate.log, env.txt
PoC changes
Authored the harness fresh (no seed): includes verbatim xdisk.c with pre-include #define interposition of make_dev/destroy_dev/dev_ops_remove_all/disk_destroy/devstat_remove_entry; fabricated kmalloc cdev + xa_softc; sysctl triggers call the real xa_close/xa_open; lockstatus probe + real-xa_open watchdog thread prove the leak; kernel-compiled via conf/files because runtime kldload GPFs on this guest. Two harness-only fixes during bring-up: dropped static on interposed stubs (-Werror prototype mismatch) and nothing else; the DF-2534 half of the harness needed periodic tsleep (see DF-2534 verdict).
Verified recommended fix
In xa_close, release xdisk_lk (not sc->lk) on both raced-destruction early-return paths, mirroring xa_open:703-715.
Verdict
Both defensive paths of the real xa_close (verbatim sha256-gated xdisk.c compiled into the guest kernel) are defective exactly as claimed. Path si_drv1==NULL (xdisk.c:769-770) returns ENXIO still holding the exclusive xdisk_lk: observed lockstatus=2 after return and a probe thread running the real xa_open stuck forever on the leaked lock, wedging every later xdisk/dmsg operation (permanent subsystem DoS). Path sc->terminating (xdisk.c:771-773) releases sc->lk, a lock that was never acquired, while still holding xdisk_lk: observed the unconditional kern_lock.c:768 panic (lockmgr: LK_RELEASE: no lock held) with backtrace lockmgr_release <- xa_close, on the stock INVARIANTS kernel and on any kernel since the check is not ifdef-gated. Production reachability of the NULL path is real (D_TRACKCLOSE per-close dispatch in devfs plus terminate_check freeing under xdisk_lk); the terminating branch is currently dead-defensive but is a booby trap. Escalation ceiling honestly quantified: panic / permanent subsystem wedge, no memory-corruption primitive, no uid=0 chain. Fix mirrors the xa_open pattern; A/B validated on a rebuilt kernel: lock released, no panic, probe completes.
No comments yet.