ips command-queue lifecycle double-free/UAF: unchecked ips_cmdqueue_init at ips.c:414 plus unconditional, non-NULLing kfree in ips_cmdqueue_free (also frees under live commands on detach)
Summary
cmdqueue_free kfrees commandarray unconditionally without clearing it; the first cmdqueue_init's ENOMEM is ignored even though that init already freed the array internally - the subsequent error unwind frees it again (UAF read of command_phys_addr, possible bus_dmamem_free on garbage pointers, then double kfree). Third trigger: the kfree also runs with used_commands != 0 on detach under in-flight commands. The exact call sequence PROVEN on the guest with a stubbed-bus_dma harness ('DOUBLE FREE: kfree(#2) on already freed', exit=42). Corruption-by-fault at attach under DMA-map exhaustion. NULL-guard + check-the-init-return fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4614 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| doublefree_harness.c | β | 4.9 KB | view raw | |
| build.log | β | 10 B | view raw | |
| run.log | β | 77 B | view raw | |
| env.txt | β | 273 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 865 B | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4614 β ips command-queue lifecycle double-free / UAF (unchecked ips_cmdqueue_init + unconditional kfree in ips_cmdqueue_free)
What
sys/dev/raid/ips/ips.c:414 (init return ignored), ips.c:139-141
(kfree(sc->commandarray) unconditional, pointer not cleared),
ips.c:125 (UAF read of command_phys_addr on re-entry),
ips.c:447-451 β 462 β 533 (second trigger), ips.c:121-138 (array freed
even while commands outstanding).
Trigger A β first init fails (ips.c:414)
sc->max_cmds = 1; ips_cmdqueue_init(sc); β the return value is ignored.
On failure (bus_dmamem_alloc ips.c:164, bus_dmamap_create ips.c:181, or
command_phys_addr == 0 ips.c:171 β i.e. DMA map exhaustion / load failure at
attach) the internal error path already ran ips_cmdqueue_free(sc)
(ips.c:190), which kfrees sc->commandarray (ips.c:140) without setting
it to NULL and marks the adapter IPS_OFFLINE (ips.c:137).
ips_adapter_init then keeps going: ips_adapter_reinit (ips.c:416) still
succeeds (the card itself is healthy), and the very next command
(ips_ffdc_reset, ips.c:421) fails with EIO because of IPS_OFFLINE β
goto error β ips_adapter_free(sc) (ips.c:462) β ips_cmdqueue_free(sc)
again (ips.c:533):
* ips.c:124-125 walks sc->commandarray[i] β use-after-free read;
* if the freed chunk was reused and command_phys_addr != 0,
ips.c:127-134 run bus_dmamap_unload/bus_dmamem_free/bus_dmamap_destroy
on garbage pointers β arbitrary free;
* ips.c:140 kfree(sc->commandarray) β double free β kernel heap
corruption.
Trigger B β second init fails (ips.c:447-451)
The re-sized init after ips_get_adapter_info is checked, but its failure
path has the identical shape: internal free already happened β goto error
β ips_adapter_free β second ips_cmdqueue_free β same double free.
Trigger C β detach with commands in flight
ips_cmdqueue_free executes kfree(sc->commandarray) at ips.c:140 even
when used_commands != 0 (the if at ips.c:122 only guards the loop and
the error value). Detach/unload with an outstanding command frees the array
under a live interrupt handler (ips.c:570/573, ips.c:773-775 walk it), and
ips_adapter_free then returns EBUSY leaving the driver loaded on a
dangling array (ips.c:533-537) β subsequent I/O is UAF.
Harness (control-flow proof, run on the audit guest)
doublefree_harness.c mirrors the exact call sequence with stubbed bus_dma
and a double-free-detecting allocator. Built and executed on the DragonFly
guest itself:
$ cc -O0 -g -o harness harness.c && ./harness [!] DOUBLE FREE: kfree(#2) on 0x800490ec0 already freed at kfree(#1) exit=42
This proves the call sequence ips_adapter_init β (failed) ips_cmdqueue_init
β ips_cmdqueue_free β β¦ β ips_adapter_free β ips_cmdqueue_free reaches two
kfrees of the same pointer (and an intervening UAF read). It does not
execute the real in-kernel driver β the guest has no ServeRAID controller
(env.txt), so the kernel path itself is not_reproduced on this machine.
Contents
VERDICT.md, doublefree_harness.c, build.log, run.log, env.txt,
fix.diff, manifest.json, verdict.json.
VERDICT β DF-4614
Status: not_reproduced for the in-kernel bug (the audit guest has no IBM
ServeRAID controller β env.txt; the ips driver is linked into the kernel
but never attaches, /dev/ips* absent). The control flow itself was
demonstrated on the DragonFly guest with doublefree_harness.c: the exact
call sequence reaches a double kfree of sc->commandarray and an
intervening use-after-free read (run.log: [!] DOUBLE FREE: kfree(#2) β¦
exit=42). Confidence in the code defect: certain.
Root cause (line-accurate)
ips_cmdqueue_free()frees unconditionally and leaves the dangling pointer (ips.c:139-141):
c
sc->staticcmd = NULL;
kfree(sc->commandarray, M_IPSBUF); /* NOT sc->commandarray = NULL */
and the kfree runs outside the used_commands == 0 guard
(ips.c:122-138), so it also frees the array while commands are in flight
(detach/unload case; ips_adapter_free then returns EBUSY with the driver
still loaded on a freed array, ips.c:533-537).
ips_adapter_init()ignores the first init's failure (ips.c:414):
c
sc->max_cmds = 1;
ips_cmdqueue_init(sc); /* <-- return value discarded */
The failed init already ran ips_cmdqueue_free internally (ips.c:190)
and set IPS_OFFLINE (ips.c:137). Because the card itself is healthy,
ips_adapter_reinit(sc, 0) (ips.c:416) succeeds; the next card command
(ips_ffdc_reset β ips_get_free_cmd, ips.c:421) returns EIO due to
IPS_OFFLINE β goto error (ips.c:424) β ips_adapter_free (ips.c:462)
β ips_cmdqueue_free again (ips.c:533):
- ips.c:124-125: command->command_phys_addr read from freed memory
(UAF read);
- if nonzero, ips.c:127-134 bus_dmamap_unload + bus_dmamem_free +
bus_dmamap_destroy on stale/garbage pointers (arbitrary free);
- ips.c:140: second kfree(sc->commandarray) (double free).
- Second trigger with identical shape: the checked re-sized init at
ips.c:447-451 fails internally β
goto errorβ same double free via ips.c:462 β 533.
Harness fidelity
doublefree_harness.c copies the decision structure of ips.c:116-143 and
ips.c:374-464 verbatim (with the bus_dma layer stubbed to force the ips.c:167
failure). Compiled and run on the audit guest (DragonFly 6.5-DEVELOPMENT,
cc): build.log, run.log. Output exit=42 is the harness's allocator
aborting on the second free of the same pointer β the point at which the real
kernel allocator's free lists would be corrupted.
Impact
Kernel heap double-free / use-after-free in the boot-time attach path (Triggers A/B) and in the detach path (Trigger C). Consequences: memory corruption (crash β exploitable heap state in principle; attacker control of the trigger is limited to forcing DMA-map exhaustion during attach and concurrent I/O during detach). Precondition: IBM ServeRAID PCI hardware present. Severity Medium (certain code path, constrained trigger, no unprivileged reachability).
Why not reproduced in-kernel on the guest
env.txt: pciconf -l shows no vendor 0x1014/0x9005 device; /dev/ips*
does not exist; the driver never probes. QEMU has no ServeRAID device model,
so ips_attach cannot be entered. A fault-injected kernel build (forcing
bus_dmamem_alloc to fail) would change no conclusion beyond what the harness
already proves β the call graph after the injected failure is fully static.
Fix
- NULL out
sc->commandarrayafter kfree and skip the walk when it is NULL; - guard the kfree with the
used_commands == 0condition and return EBUSY before freeing anything when commands are outstanding; - check the return of the first
ips_cmdqueue_init(ips.c:414) and bail out immediately.
See fix.diff.
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_testableNo hardware to build an A/B kernel run on; fix.diff removes all three trigger paths by construction (NULL-out + guard + checked init), and the harness's mirrored control flow confirms the guard semantics (free-called-once) when re-run with the same fix applied to the mirror.
fix.diff (ips_cmdqueue_free NULL/guard clauses + sc->commandarray = NULL + checked ips_cmdqueue_init at ips.c:414).
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (guest-executed harness aborting on the second free of the same pointer: exit=42); build.log (cc on DragonFly 6.5-DEVELOPMENT); VERDICT.md (line-accurate trace of Triggers A/B/C); env.txt (no ServeRAID hardware on guest, driver never attaches)
PoC changes
Harness written fresh for this finding (no seed existed): mirrors ips_cmdqueue_init/ips_cmdqueue_free/ips_adapter_init decision structure with stubbed bus_dma forced to fail at ips.c:167; compiled and run on the DragonFly guest via vm.sh (source transferred with openssl base64 since the guest lacks base64(1)).
Verified recommended fix
NULL sc->commandarray after kfree and early-return when NULL; refuse to free (EBUSY) while used_commands != 0; honor the return value of the first ips_cmdqueue_init at ips.c:414.
Verdict
The ips command-queue lifecycle double-frees. ips_cmdqueue_free (ips.c:139-141) kfrees sc->commandarray unconditionally (outside the used_commands==0 guard, and without NULLing the pointer), and ips_adapter_init ignores the return of the first ips_cmdqueue_init (ips.c:414). When that init fails (bus_dmamem_alloc/map_create/load failure under DMA exhaustion), the internal ips_cmdqueue_free (ips.c:190) already freed the array; adapter init then fails through the IPS_OFFLINE-blocked ffdc path (ips.c:421-425) and its error unwind (ips.c:462 -> ips_adapter_free -> ips.c:533) calls ips_cmdqueue_free a second time: UAF read of command_phys_addr (ips.c:125), possible bus_dmamem_free/bus_dmamap_destroy on garbage pointers (ips.c:127-134), then double kfree (ips.c:140) - kernel heap corruption. Identical shape when the checked second init fails (ips.c:447-451). Trigger C: the unconditional kfree also fires with used_commands != 0, freeing the array under live interrupt handlers during detach, after which ips_adapter_free returns EBUSY leaving the driver on a dangling array. The exact call sequence was demonstrated on the audit guest with doublefree_harness.c (stubbed bus_dma, aborting allocator): '[!] DOUBLE FREE: kfree(#2) on 0x800490ec0 already freed at kfree(#1)', exit=42 - see run.log. The in-kernel path itself is not_reproduced because the guest has no ServeRAID PCI device (env.txt) and QEMU has no device model for one; the defect trace is fully static and certain.
No comments yet.