xa_strategy / xa_size dereference sc without lifetime lock racing xa_terminate_check free
Summary
Both xa_strategy and xa_size dereference sc=dev->si_drv1 without taking xdisk_lk. xa_terminate_check (run under xdisk_lk) frees sc at :680 and NULLs si_drv1 at :665. xa_size NULL-checks but then dereferences sc->info.d_media_blocks without lock classic check/use TOCTOU. xa_strategy reads si_drv1 at :801 immediately dereferences sc->stats at :805 with neither NULL check nor lock. disk_destroy synchronous so strategy path largely drained before si_drv1 NULLed making trigger narrow but xa_size has no such protection. [VERIFIED pass2: reproduced - 664 UAF reads captured in 157M real xa_size iterations vs 51.5M frees each observing a value planted only after the kfree, strategy NULL write-fault at 0x90 in devstat_start_transaction; production-reachable via diskpsize DSO_RAWPSIZE re-dispatch with no drain; groomable 8-byte heap disclosure as device size; lock-plus-recheck fix-validated 0 UAF reads over 82M iterations]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2534 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df253x_harness.c | β | 15.7 KB | view raw | |
| build.sh | β | 140 B | view raw | |
| run.log | β | 3.0 KB | view raw | |
| run.patched.log | β | 833 B | view raw | |
| panic.txt | β | 2.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 | |
| env.txt | β | 984 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| verdict.json | β | 4.7 KB | view raw |
VERDICT β DF-2534 (REPRODUCED: use-after-free READ window in real xa_size; NULL-write panic in xa_strategy)
Bottom line
Both cited defects are real and were mechanically demonstrated against the REAL functions (verbatim sha256-gated xdisk.c compiled into the guest kernel, fabricated cdev/softc, interposed disk/dmsg externals):
-
xa_sizeTOCTOU/UAF read (production-reachable window).xa_sizereadssc = a_dev->si_drv1at :833, NULL-checks it, then dereferencessc->info.d_media_blocksat :835 β with NO lock.xa_terminate_checkNULLssi_drv1at :665 andkfree(sc)s at :680 underxdisk_lk; a reader that loaded the pointer before the NULL-store dereferences freed memory after it. Hammer proof: cpu0 thread in the realxa_sizevs cpu1 thread driving the realxa_terminate_check:reader xa_size iterations: 157,024,256 (151M on the safe ENXIO path) terminator frees: 51,550,501 (100% same-address LIFO reuse) UAF READS CAPTURED: 664
Each capture is airtight: the reader observed
0xC0FFEE00DF2534 β a value that was only ever written into the slot
AFTER kfree(sc) and re-allocation, so the :835 load provably landed
after the free (a stale-pointer dereference of freed kernel heap through
shipping code). First capture arrived within milliseconds of start.
xa_strategymissing NULL check (write-fault panic). :801 loadssc = a_dev->si_drv1with no check and :805 passes&sc->statstodevstat_start_transactionβ withsc == NULL:Fatal trap 12 ... fault virtual address = 0x90, supervisor write dataatdevstat_start_transaction+0xa: lock xaddl %eax,0x58(%rdi)β0x90 == offsetof(xa_softc, stats) + 0x58, i.e. a write through the NULL softc exactly as coded. Demonstrated with fabricated state.
Honest narrowness quantification
xa_sizeis the live production window. psize is not drained by destruction:diskpsize(sys/kern/subr_disk.c:1259-1276) re-dispatchesdev_doperateto the raw device whendssize()returns -1 and the disk setsDSO_RAWPSIZEβ which xdisk sets (xdisk.c:491).dev_doperate(kern_device.c:544) has no closing/refs gate; it dispatches straight throughsi_ops. So a psize racing span-loss teardown reads freed heap. What the primitive yields: an 8-byte read at a fixed offset of a freed M_XDISK slab slot, returned to the caller as the device size (via dssize/DIOCGPART paths) β an attacker-groomable kernel heap info-leak channel (spray an object into the freed slot, read its content at that offset). It is a READ: no control-flow or write primitive; uid=0 is not reachable from this bug alone.xa_strategyis practically narrowed by destruction ordering:xa_terminate_checkcallsdisk_destroyat :663 BEFORE NULLingsi_drv1at :665, and disk_destroyβdestroy_dev drains device references;xa_terminate_checkalso refuses to free whilebioq/tag_pendqare non-empty (:646-647). So a NULLscat strategy entry requires a caller that slipped past the drain β the NULL-write panic demonstration is fabricated-state (post-drain) and labeled as such; its value is proving the code has no defense at all if the drain assumption ever breaks.
Threat model (per team directive)
xdisk devices are created by root via dmsg; afterwards the /dev/xa%d
nodes are ordinary disks. Whoever can open the node (permissions
permitting) can race lseek/size-query paths against span teardown by an
unprivileged-triggerable disconnect. Ceiling: 8-byte heap disclosure per
hit (Low), repeatable at will; not an escalation primitive by itself.
Fix validation (fixed)
fix.diff takes xdisk_lk and re-checks si_drv1 under it in both
functions, matching the file's own idiom (the same lock
xa_terminate_check uses to NULL+free). In xa_strategy the bio is
queued before the lock is dropped so xa_terminate_check's empty-queue
precondition protects the softc; lock order xdisk_lk -> sc->lk matches
xaio_rcvdmsg. Applied in guest /usr/src together with DF-2533's fix
(combined tree sha256
15d189799d499d96ee6bbb5796ef3b3c5e316fce06d15507a47776119ae43385),
kernel rebuilt (#3 Mon Oct 19 21:13:06), EXACT same triggers re-run:
- race: 82,313,216
xa_sizeiterations vs 30,146,560 frees (100% same-address reuse) β 0 UAF reads (baseline: 664) - strategy NULL: clean return 0, bio completed with
b_error=6 (ENXIO),b_flags=0x800 (B_ERROR)via biodone β no fault
PoC notes
- The magic-value method cannot false-positive:
si_drv1never points at the re-allocated block while set, and the planted value exists only after the free. - The hammer threads periodically
tsleep1 tick (a spinning kernel thread starves its CPU under the cooperative LWKT scheduler β harness artifact, not part of 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
Root creates an xa device via dmsg; any holder of the /dev/xa%d node can race size queries (dssize/DIOCGPART paths reach xa_size via diskpsize raw re-dispatch) against span-loss teardown. The reader captures sc from si_drv1 before xa_terminate_check NULLs it, then dereferences after the free: it reads whatever now occupies the freed M_XDISK slab slot (8 bytes at offsetof d_media_blocks) and that value is returned to userspace as the device size. Grooming the freed slot with a chosen kernel object converts this into a repeatable fixed-offset heap info leak. Read-only primitive; escalation ceiling is information disclosure, not code execution.
Evidence (decisive lines)
run.log (DF2534-RACE summary: 157024256 iterations, 664 UAF READS CAPTURED; DF2534-S Fatal trap 12 at 0x90 in devstat_start_transaction+0xa), panic.txt (full trap dump), run.patched.log (0 UAF reads; xa_strategy returned 0 with biodone b_error=6 b_flags=0x800), fix_build.log, env.txt
PoC changes
Authored the race harness fresh (no seed): magic-value method (plant 0xC0FFEE00DF2534 into the freed slot after same-address reallocation; si_drv1 never points at the reused block, so observation cannot false-positive). Two bring-up fixes: lwkt_yield() did not prevent CPU starvation under the cooperative LWKT scheduler (spinning kthread never gives the cpu back to user threads) - replaced with a 1-tick tsleep every 256K iterations; first frozen run still captured 3 UAF reads before the wedge (serial_console.baseline.log), final run completed with the full summary and the guest healthy.
Verified recommended fix
Take xdisk_lk and re-check si_drv1 under it in xa_size and xa_strategy (queue the bio before dropping the lock), matching the lifetime lock xa_terminate_check already uses.
Verdict
Both cited missing-lifetime-lock defects confirmed against the real functions (verbatim sha256-gated xdisk.c compiled into the guest kernel). (1) xa_size:833-835 NULL-checks si_drv1 then dereferences sc->info.d_media_blocks with no lock while xa_terminate_check NULLs si_drv1 (:665) and kfree(sc)s (:680) under xdisk_lk. Hammer race with the real xa_size on cpu0 and the real xa_terminate_check on cpu1: 157,024,256 reader iterations vs 51,550,501 frees (100 percent same-address slab reuse), 664 UAF READS captured, each proven by observing 0xC0FFEE00DF2534, a value planted only AFTER the kfree plus reallocation, so the deref provably landed on freed heap through the stale pointer. Production window quantified: diskpsize re-dispatches psize to the raw device (DSO_RAWPSIZE set by xdisk at :491; subr_disk.c:1273) and dev_doperate has no drain, so psize genuinely races teardown; primitive is an attacker-groomable 8-byte fixed-offset kernel heap disclosure returned as the device size (info leak, Low ceiling, no write/control-flow primitive, no uid=0 chain). (2) xa_strategy:801/805 has no NULL check at all: with si_drv1 NULL it write-faults at 0x90 in devstat_start_transaction (Fatal trap 12, supervisor write) - demonstrated with fabricated state and honestly labeled post-drain-only in production because disk_destroy at :663 drains strategy before si_drv1 is NULLed at :665. Fix takes xdisk_lk and re-checks si_drv1 under it in both functions; A/B validated: 0 UAF reads over 82M iterations on the patched kernel and clean ENXIO biodone from strategy.
No comments yet.