mps FW-diag state machine: shared DMA trio + stale per-type size yields OOB kernel disclosure, cross-type free, UAF read, and double free (DF-4008 mpr twin, worse)
Summary
One shared DMA trio backs all three per-type fw_diag_buffer_list[] registrations. A second REGISTER silently replaces the shared pointers (leaking the first allocation) while the first type keeps its per-type size; READ_BUFFER bounds-checks against the stale size but copies from the shared allocation; both free paths free the CURRENT trio regardless of ownership and never NULL (unlike the mpr twin), arming the dangling read and a double free. Proven LIVE via verbatim state-machine replica driven by uid=2: 60 KiB OOB copyout past a 4 KiB allocation; cross-type free; UAF read leaking slab free-list kernel pointer 0xfffff80116c7d000; second unregister -> 'panic: memory chunk already free!'. Three-arm fix validated live (-DFIXED replica fails closed) and compiled+booted.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4013 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dfdiag_replica.c | trigger-source | KLD replica: verbatim mps diag state machine | 18.7 KB | view raw |
| trigger.c | trigger-source | operator-side DIAG_ACTION driver | 4.4 KB | view raw |
| build.log | build-log | kmod build rc=0 | 840 B | view raw |
| fix_build.log | build-log | -DFIXED kmod build rc=0 | 847 B | view raw |
| replica_run.log | run-log | 6-step baseline + fixed transcript | 4.9 KB | view raw |
| panic.txt | crash-proof | double-free panic with verbatim free block in traceback | 827 B | view raw |
| leak_sample.txt | leak-sample | slab free-list kernel pointer from freed memory | 446 B | view raw |
| fix.diff | suggested-fix | one-trio invariant + NULL-after-free + NULL-guard | 2.4 KB | view raw |
| env.txt | env | guest/kernel/node/uid context | 535 B | view raw |
Fix verification
fixedBaseline replica: stale-size 60 KiB OOB copyout + cross-type free + UAF read leaking a slab pointer + double-free panic (guest down). Patched (-DFIXED replica with the three fix arms): second REGISTER refused; after UNREGISTER the trio is NULLed and reads/unregisters through dangling state are refused; full attack sequence fails closed; guest healthy. fix.diff dry-run applies cleanly to sys/ and is included in the combined nativekernel build validation. Combined nativekernel validation: DF-4012+4013+4014 fix.diffs applied together to /usr/src, make nativekernel X86_64_GENERIC rc=0 with -Werror (mps_user.o recompiled clean), installed and booted (kern.version 'DragonFly 6.5-DEVELOPMENT #1: Thu Sep 17 14:24:13 UTC 2026', fix_kernel_build.log).
replica_run.log RUN 1 (steps 1-6 + panic) vs RUN 2 (all steps refused, uptime healthy), panic.txt, fix_build.log, fix_kernel_build.log
Confirmed kernel references
Detail
Exploit chain
operator-group member -> /dev/mpsN (0640 root:operator) -> ioctl(MPTIOCTL_DIAG_ACTION REGISTER x2 with two buffer types) -> stale per-type size -> ioctl(READ_BUFFER 64 KiB via uid of type A) = OOB kernel heap disclosure; ioctl(UNREGISTER uid A) frees the shared trio still owned by type B; ioctl(READ_BUFFER via uid B) = UAF read leaking slab free-list pointers; ioctl(UNREGISTER uid B) = double free -> kfree panic (or, with heap grooming, allocator metadata corruption -> arbitrary write). Replica demonstrated every step including the double-free panic.
Evidence (decisive lines)
replica_run.log (all 6 steps), panic.txt ('memory chunk ... already free', chunk_mark_free <- _kfree <- dfdiag_ioctl_devsw, i.e. the verbatim unregister free), leak_sample.txt (0xfffff80116c7d000 slab pointer from freed memory), dfdiag_replica.c, build.log/fix_build.log
PoC changes
no original PoC shipped; created dfdiag_replica.c (verbatim mps diag state machine; bus_dma trio stubbed as kmalloc, HW post/release stubbed to succeed with the exact flag side-effects of the real success paths; trigger knobs for forcing post/alloc failures) + trigger.c (register/read/unregister driver).
Verified recommended fix
Enforce a one-trio invariant at REGISTER, NULL the shared trio pointers after every free path, and refuse reads through a NULL trio (fix.diff).
Verdict
The mps FW-diagnostic state machine (mps_user.c:1314-1660) keeps ONE shared DMA trio (fw_diag_dmat/fw_diag_buffer/fw_diag_map/fw_diag_busaddr, mpsvar.h:377-380) for all three per-type fw_diag_buffer_list[] entries (size/unique_id per type, mpsvar.h:409-410). Registering a second buffer type silently overwrites the shared pointers (leaking the first registration) while the first type keeps its stale per-type size; the read path bounds-checks StartingOffset+BytesToRead against pBuffer->size (:1583) but copies from the shared sc->fw_diag_buffer (:1593-1595); both free paths (:1419-1427 register-failure, :1472-1480 unregister) free the CURRENT trio without NULLing -- even when it was allocated for a different type. Proven live on the guest with a kernel replica carrying the verbatim register/unregister/read/dispatch logic (bus_dma stubbed, HW post/release stubbed to succeed exactly like :1216-1219/:1249-1251), driven by uid=2 over a 0640 root:operator node: (1) REGISTER TRACE 1 MiB + REGISTER SNAPSHOT 4 KiB; (2) READ 64 KiB via the TRACE uid passed the stale-size check and copyout'ed ~60 KiB past the 4 KiB live allocation (OOB kernel heap disclosure); (3) UNREGISTER of the TRACE uid freed the trio belonging to SNAPSHOT (cross-type free); (4) READ via the SNAPSHOT uid returned the freed chunk with the slab free-list kernel pointer 0xfffff80116c7d000 in its first 8 bytes (UAF read + kernel pointer disclosure, leak_sample.txt); (5) the second UNREGISTER double-freed the trio: 'panic: memory chunk 0xfffff80116c67000 is already free!' with the verbatim mps_diag_unregister free block in the traceback (panic.txt). Status inconclusive only because the shipped driver needs an LSI HBA. This is the DF-4008 (mpr) twin, strictly worse: the mpr twin at least NULLs the trio after free (mpr_user.c:1627/1631), mps does not, arming the double-free/UAF. All three fix arms (one-trio invariant, NULL-after-free, NULL-guard on read) validated live via -DFIXED replica: the full attack sequence now fails closed at every step and the guest stays up.
No comments yet.