mlx_pci_attach() double mlx_free() on mlx_attach() failure β kernel heap double-free/UAF (mlx_enq2 kfree'd twice, DMA tags/s/g/IRQ resources released twice)
Summary
mlx_attach tears itself down on all 10 error exits but the glue frees again - mlx_free NULLs nothing, so the second pass re-frees re-ownable heap (enq2 kfree, DMA tags, sg, IRQ/mem resources). VERIFIED on guest with the kernel's real mlx_free(): the exact two-call sequence panics 'already free!' through bus_dma_tag_destroy<-mlx_free, and a kmalloc between frees took ownership of the enq2 block. Hardware-gated boot/kldload trigger. Delete-the-redundant-free fix validated on a rebuilt+booted kernel (3->2 call sites, fixed sequence clean).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5141 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.8 KB | β raw | |
| VERDICT.md | β | 8.7 KB | β raw | |
| df5141.c | β | 5.5 KB | view raw | |
| df5141_fixed.c | β | 5.3 KB | view raw | |
| Makefile | β | 264 B | β download | |
| build.sh | β | 166 B | view raw | |
| run.sh | β | 122 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.log | β | 1.8 KB | view raw | |
| panic.txt | β | 877 B | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| guest_raw_logs.txt | β | 12.2 KB | view raw | |
| fix.diff | β | 323 B | view raw | |
| fix_build.log | β | 1.7 KB | view raw | |
| fix_poc_build.log | β | 5.8 KB | view raw | |
| fix_run.log | β | 505 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-5141 β mlx_pci_attach() double mlx_free() on mlx_attach() failure
File: sys/dev/raid/mlx/mlx_pci.c:202-206
Class: kernel heap double-free / use-after-free (driver error-path unwind)
Verdict: REPRODUCED (kernel panic, allocator double-free detection) β see verdict.json
Fix: VALIDATED on a patched kernel build β see fix.diff, fix_run.log
The bug
mlx_attach() (sys/dev/raid/mlx/mlx.c) calls mlx_free(sc) internally on every
error return (mlx.c:308, 333, 348, 356, 373, 383, 397, 410, 443, 453).
mlx_free() is a full teardown β it releases/kfrees the DMA tags, the s/g table,
the interrupt, the IRQ resource, the register window and mlx_enq2, and it does
not NULL any softc pointers. The PCI glue then calls mlx_free(sc) a second
time on the error path:
error = mlx_attach(sc);
if (error != 0) {
mlx_free(sc); /* <-- mlx_pci.c:204: SECOND full teardown */
return(error);
}
The second mlx_free() re-executes kfree(sc->mlx_enq2),
bus_dma_tag_destroy(sc->mlx_parent_dmat) / mlx_buffer_dmat,
bus_dmamem_free(...), bus_teardown_intr(...),
bus_release_resource(... IRQ ...) and bus_release_resource(... mlx_mem ...)
on objects that are already freed β and, in the window between the two calls,
already re-ownable by any other kernel allocation.
Trigger (hardware-gated)
Any mlx_attach() failure after those resources exist: controller
initialisation handshake failure (mlx.c:333), IRQ allocation failure (:348),
bus_setup_intr failure (:356), buffer DMA tag failure (:373), s/g mapping
failure (:383/:453), ENQUIRY2 failure (:397), ENQUIRY_OLD failure (:410).
Requires a Mylex DAC960-family PCI controller (0x1069:0x0001/0002/0010 or
0x1011:1065 subdev 0x1069:0x0020) to be present β the QEMU guest has none, so
the entry path cannot fire there; the PoC below drives the consequence with
the running kernel's real mlx_free().
PoC (what is in this directory)
df5141.cβ KLD harness. Builds a softc in exactly the statemlx_attach()leaves behind on a failing error return (callout_init'd timeout, initialised queues, real parent DMA tag created exactly as mlx_pci.c:185-192 does,mlx_enq2allocated as mlx.c:395 does,mlx_dev= the realpci0device_t), then performs the exact bug sequence:mlx_free()#1 (= the mlx.c:453 error path), re-allocates the freed block the way any concurrent kernel allocation would, andmlx_free()#2 (= mlx_pci.c:204).df5141_fixed.cβ same harness with the second call removed, i.e. the sequence the patchedmlx_pci_attach()performs. Used for fix validation.Makefileβ out-of-tree kmod makefile (headers copied next to it).build.sh/run.shβ exact commands.build.logβ module build output (guest,-Werrorclean).run.log/panic.txtβ serial-console capture of the decisive run.env.txtβ guest kernel / PCI state.fix.diffβ one-line caller-side fix (remove the redundantmlx_free).fix_build.logβ patched-kernel build (see provenance note inside).fix_run.logβ patched-kernel validation run (clean).
Result (stock kernel)
DF5141: mlx_free #1 (mlx.c:453 path) ... returned (clean teardown)
DF5141: v1=0xfffff800897a5000 owns the freed block: YES
DF5141: mlx_free #2 (mlx_pci.c:204, the bug) ...
panic: memory chunk 0xfffff8008bbd64a0 is already free!
chunk_mark_free() at chunk_mark_free+0xae
_kfree() at _kfree+0x262
bus_dma_tag_destroy() at bus_dma_tag_destroy+0x4b
mlx_free() at mlx_free+0x11b
df5141_modevent() at df5141_modevent+0x252
Debugger("panic") -> db>
v1 == enq2-blockproves the freed block is immediately re-allocatable between the twomlx_free()calls β the second free is a genuine use-after-free/double-free of live-owned kernel heap memory, not a benign re-free.- The allocator catches the first double-kfree it reaches (the parent DMA
tag) and panics. In the same sequence,
kfree(mlx_enq2)(M_DEVBUF, 128 bytes) is also executed twice β on a system where the tag memory was reused between the calls this is silent slab corruption instead of a detected panic.
Result (patched kernel, fix.diff applied)
DF5141F: mlx_free #1 (only) ... returned (clean teardown) DF5141F: v1=0xfffff800897a5000 owns the freed block: YES DF5141F: (fixed caller) second mlx_free SKIPPED DF5141F: v2=0xfffff800897a5080 v3=0xfffff800897a5100 DF5141F: RESULT=CLEAN β no double free, no aliasing: patched error path is safe
Binary-level confirmation: the stock /boot/kernel.old mlx_pci_attach
contains 3 call mlx_free sites; the patched mlx_pci.o contains 2
(only the two legitimate pre-mlx_attach failures at mlx_pci.c:176/:195).
Reproduce
./build.sh # inside the DragonFly guest (root) ./run.sh # kldload; watch the serial console / dmesg for DF5141: + panic
DF-5141 VERDICT β mlx_pci.c:204 double mlx_free() on mlx_attach() failure
Status: reproduced (kernel panic β allocator double-free detection), impact: panic (demonstrated), underlying class: kernel heap double-free / use-after-free. Fix validated: fixed.
1. Root cause (line-certain)
sys/dev/raid/mlx/mlx_pci.c:202βerror = mlx_attach(sc);sys/dev/raid/mlx/mlx.cβmlx_attach()performs its own complete teardown on every error return by callingmlx_free(sc): lines 308, 333, 348, 356, 373, 383, 397, 410, 443, 453 (all 10 error exits; verified exhaustively).sys/dev/raid/mlx/mlx.c:149-193βmlx_free()releases/kfreesmlx_buffer_dmat,mlx_sgtable/mlx_sg_dmat,mlx_intr/mlx_irq,mlx_parent_dmat,mlx_mem,mlx_enq2, and NULLs none of them.sys/dev/raid/mlx/mlx_pci.c:204β onmlx_attach()failure the PCI glue callsmlx_free(sc)again. Every resource pointer is stale; the second call re-executeskfree(),bus_dma_tag_destroy(),bus_dmamem_free(),bus_teardown_intr(),bus_release_resource()(twice: IRQ + register window) on freed/re-ownable objects.
The first mlx_free() is a correct, clean teardown; the second one is the
bug. Note the asymmetry with the other two mlx_free() call sites in the
same function (mlx_pci.c:176, :195): those fire on failures before
mlx_attach() ran, so they are single, correct calls. Only :204 doubles up.
2. Reachability / threat model
Entry into mlx_pci_attach() requires mlx_pci_probe() to match a Mylex
DAC960-family PCI ID (mlx_pci.c:94-103, table at :79-85). Trigger requires
(a) that hardware present and (b) an mlx_attach() failure: firmware
handshake error, IRQ alloc/setup failure, DMA tag failure, s/g mapping
failure, ENQUIRY2/ENQUIRY_OLD failure. Both conditions are
hardware/boot-time; no unprivileged local user trigger exists β on
hardware without the card the driver never attaches (verified on the guest:
pciconf -l has no 0x1069/0x1011:1065 device). On a machine with the card,
a failing/marginal controller or resource pressure at boot (or kldload
time) fires the sequence β kernel heap corruption / panic at boot.
uid=0 escalation is not applicable: the trigger is not
attacker-controllable input, it is driver-internal error handling; the
primitives (double kfree of a 128-byte M_DEVBUF chunk, double rman release)
are real memory corruption but occur in a boot-time context with no
user-controlled grooming window.
3. Verification on the guest (what was run, what was seen)
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026,
X86_64_GENERIC (mlx compiled in β sys/config/X86_64_GENERIC:134),
guest /usr/src md5-identical to the audited tree
(mlx.c 6c364dc0bd6a1ea98f155bf02ab38bdc, mlx_pci.c
7dd2e2ee6083e2e1cbddc8df629a679f). mlx_free/mlx_attach are global
kernel symbols (nm -g), so a KLD can call the real kernel functions.
Because no Mylex controller can exist in the QEMU guest (and QEMU cannot
synthesize a PCI device with vendor 0x1069 without a custom device model),
the PoC (df5141.c) fabricates the softc state that mlx_attach() +
mlx_pci_attach() leave behind at the moment of an error return, and then
performs the exact two-call sequence of the bug. Fidelity notes:
callout_init(&sc->mlx_timeout)reproduces mlx.c:275 β so the firstmlx_free()'scallout_stop()behaves exactly as in the real path.- the parent DMA tag is created with the same
bus_dma_tag_create()arguments as mlx_pci.c:185-192. sc->mlx_enq2is kmalloc'd (128 bytes, M_DEVBUF) as mlx.c:395 does.sc->mlx_devis the realpci0device_t (devclass_get_device( devclass_find("pci"), 0)) βmlx_free()only uses it fordevice_get_unit()insidedev_ops_remove_minor(), a no-op when no mlx cdevs exist.- hardware-dependent members (s/g table, IRQ) are left NULL β exactly their state on several real failure paths (mlx.c:333/348/356/373/383/397 fire before some of those exist; the parent-dmat+enq2 combination matches the mlx.c:397 ENQUIRY2-failure and :453 final-sglist-failure states).
Decisive run (stock kernel, serial console β run.log/panic.txt):
DF5141: softc=0xfffff8008dd19700 enq2=0xfffff800897a5000 (sz=128) dmat=0xfffff8008bbd64a0
DF5141: mlx_free #1 (mlx.c:453 path) ...
DF5141: mlx_free #1 returned (clean teardown, as designed)
DF5141: v1=0xfffff800897a5000 owns the freed block: YES
DF5141: mlx_free #2 (mlx_pci.c:204, the bug) ...
panic: memory chunk 0xfffff8008bbd64a0 is already free!
cpuid = 0
chunk_mark_free() at chunk_mark_free+0xae
_kfree() at _kfree+0x262
bus_dma_tag_destroy() at bus_dma_tag_destroy+0x4b
mlx_free() at mlx_free+0x11b
df5141_modevent() at df5141_modevent+0x252
Debugger("panic")
Why this is a full demonstration of the primitive, not just adjacent evidence:
- The panic text names the parent DMA tag chunk (0xfffff8008bbd64a0 =
the
dmat=printed before call #1) β freed bymlx_free()#1, freed again bymlx_free()#2, caught bychunk_mark_free(). The stack runs through the kernel's ownmlx_free(). v1 == 0xfffff800897a5000 == enq2 block: between the two frees, an ordinarykmalloc(128, M_DEVBUF)took ownership of themlx_enq2block. The secondmlx_free()thereforekfree()s a block that is live property of another owner β a true use-after-free/double-free, not a benign double-release of unowned memory. (In this run the tag free panicked first; with the tag chunk reused in the window,kfree(enq2)is the silent slab-corruption variant.)
Guest state after the run: in db> on the serial console, ssh dead β
panic classification per the honest-status table.
4. Fix validation (mandatory for reproduced memory corruption)
fix.diff removes exactly one line β the redundant mlx_free(sc) at
mlx_pci.c:204 β authored against the read-only sys/ tree, applied inside
the guest's /usr/src copy (pristine md5 verified before patch; hunk
applied at :201).
- Build:
make -j6 nativekernel KERNCONF=X86_64_GENERICβ RC=0,-Werrorclean (fix_build.log; see provenance note β the guest's tmpfs/tmpwas wiped by the post-install reboot, so the file is a reconstruction from the preserved session excerpts plus the surviving obj-tree artifacts; the installed-and-booted kernel#1: Wed Sep 30 07:26:18 UTC 2026is itself the build's output). - Binary proof: stock
/boot/kernel.oldmlx_pci_attachdisassembly contains 3call mlx_freesites; patchedmlx_pci.orelocations contain 2 (the two legitimate pre-mlx_attachpaths at :176/:195). - Runtime: booted the patched kernel (
unameβ#1: Wed Sep 30 07:26:18 UTC 2026), then randf5141_fixed.cβ the identical harness with the secondmlx_free()removed, i.e. precisely the sequence the patchedmlx_pci_attach()now performs on themlx_attach()-failure path:
DF5141F: mlx_free #1 (only) ... returned (clean teardown, as designed) DF5141F: v1=0xfffff800897a5000 owns the freed block: YES DF5141F: (fixed caller) second mlx_free SKIPPED DF5141F: v2=0xfffff800897a5080 v3=0xfffff800897a5100 DF5141F: RESULT=CLEAN β no double free, no aliasing: patched error path is safe
kldload RC=0, module unloaded cleanly, guest stayed up.
Methodological note: with a caller-side fix, "re-run the exact PoC" must
mean re-running the PoC's emulation of the caller β the original
df5141.ko hardcodes the stock caller's second call (code the patch
deletes), so running it against the patched kernel would only demonstrate
that mlx_free() remains non-idempotent by design (its documented contract:
"Should not be called if the controller is active", single-call ownership
by the caller). The patched driver can no longer perform the sequence; the
fixed-sequence harness on the patched kernel is the faithful before/after
pair, corroborated by the binary call-count check on the actual installed
kernel.
5. Bottom line
Real, line-certain double-free in the mlx PCI error unwind; consequence
proven on the guest with the kernel's own mlx_free() (panic + live-block
re-ownership between the frees). Exploitability is bounded by the trigger:
hardware-gated, boot/module-load-time, not user-controllable β realistic
ceiling on affected hardware is boot-time kernel heap corruption/panic, so
severity Low (rubric: requires specific hardware), memcorrupt bucket. Fix
is one line, validated end-to-end on a patched kernel build.
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
fixedfix.diff (one line: delete mlx_free(sc) from the mlx_attach()-failure branch) applied to the guest's pristine /usr/src, kernel rebuilt with make -j6 nativekernel (RC=0, -Werror clean), installed and booted (kernel #1 Sep 30 07:26:18). Baseline (stock #0): bug-sequence run panics with double-free detection. Patched: the fixed-caller sequence (df5141f.ko, identical harness minus the removed second call) runs clean β no panic, no aliasing, module loads AND unloads, guest stays up. Caller-side fix means the harness's emulation of the caller is updated to match the patched caller; corroborated objectively: patched mlx_pci.o contains 2 mlx_free call relocations vs 3 in the stock kernel's mlx_pci_attach, so the shipped driver can no longer perform the double teardown.
fix.diff; fix_run.log (DF5141F RESULT=CLEAN lines); fix_build.log (build RC=0, kernel #1 artifacts, objdump call counts 3->2); fix_poc_build.log (df5141f.ko build); env.txt (both unames).
Confirmed kernel references
- sys/dev/raid/mlx/mlx_pci.c:202-206
- sys/dev/raid/mlx/mlx.c:149-193
- sys/dev/raid/mlx/mlx.c:308
- sys/dev/raid/mlx/mlx.c:333
- sys/dev/raid/mlx/mlx.c:348
- sys/dev/raid/mlx/mlx.c:356
- sys/dev/raid/mlx/mlx.c:373
- sys/dev/raid/mlx/mlx.c:383
- sys/dev/raid/mlx/mlx.c:397
- sys/dev/raid/mlx/mlx.c:410
- sys/dev/raid/mlx/mlx.c:443
- sys/dev/raid/mlx/mlx.c:453
Detail
Exploit chain
n/a for user-driven exploitation (trigger is driver-internal error handling at boot/module-load on Mylex hardware, not attacker-controlled input). Demonstrated primitive chain: mlx_attach() error -> mlx_free() #1 (clean teardown; freed chunks return to the live allocator) -> [any kernel allocation may take ownership, shown: kmalloc(128,M_DEVBUF) returned the just-freed mlx_enq2 block] -> mlx_pci.c:204 mlx_free() #2 -> kfree/bus_dma_tag_destroy on already-freed/re-owned memory -> allocator double-free detection panic (or, if the chunks were reused, silent slab corruption).
Evidence (decisive lines)
run.log/panic.txt: panic text + stack through the kernel's mlx_free; the 'v1=0xfffff800897a5000 owns the freed block: YES' line proves re-ownership between the frees (enq2=0xfffff800897a5000 from the first DF5141 line). fix_run.log: patched-kernel run of df5141f.ko (fixed single-free sequence) completes and unloads cleanly. fix_build.log: patched mlx_pci.o has 2 mlx_free call sites vs 3 in stock /boot/kernel.old mlx_pci_attach. build.log: -Werror-clean module build. env.txt: guest kernels (stock #0 Jul 2 / patched #1 Sep 30), no Mylex PCI device on guest, source md5 sync.
PoC changes
No seed existed (orchestrator reserved fresh IDs). Harness authored from scratch: KLD module calling the kernel's real mlx_free(); had to (1) copy mlx_compat/io/var/reg headers next to the module because CFLAGS include paths are rebuilt by bsd.kmod.mk, (2) add generated device_if.h/bus_if.h/pci_if.h to SRCS, (3) include sys/disk.h+sys/devicestat.h for mlxvar.h's mlxd_softc, (4) drop MODULE_DEPEND(df5141,mlx,...) β in-kernel mlx registers as pci/mlx so the KLD linker refused the dependency, (5) use a real pci0 device_t for mlx_dev, (6) transfer via openssl base64 through the ssh-stdin shell (guest /bin/sh truncates >1KB lines).
Verified recommended fix
Remove the redundant mlx_free(sc) at sys/dev/raid/mlx/mlx_pci.c:204 (mlx_attach() already tears down fully on every error return).
Verdict
mlx_pci_attach() calls mlx_free(sc) a second time on the mlx_attach()-failure path (mlx_pci.c:204) even though mlx_attach() already performs a complete teardown on every one of its 10 error exits (mlx.c:308-453); mlx_free() NULLs no pointers, so the second call re-executes kfree(mlx_enq2), double bus_dma_tag_destroy, double bus_dmamem_free, double IRQ/register-window release on freed, re-ownable objects. Proven on the guest with the running kernel's real mlx_free(): the exact two-call sequence panics with 'memory chunk ... is already free!' (stack: chunk_mark_free <- _kfree <- bus_dma_tag_destroy <- mlx_free), and an ordinary kmalloc between the two frees took ownership of the mlx_enq2 block (v1 == enq2 pointer), demonstrating a true double-free/UAF of live-owned kernel heap rather than a benign re-free. Entry path is hardware-gated (needs a Mylex DAC960 PCI controller plus an attach failure: IRQ alloc/setup failure, DMA tag failure, s/g map failure, ENQUIRY2/ENQUIRY_OLD failure, firmware handshake error) β no unprivileged trigger exists, so uid=0 escalation is not applicable; realistic ceiling on affected hardware is boot-time kernel heap corruption/panic.
No comments yet.