β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4614

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4614 Β· 9 files
FileTypeDescriptionSize
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.md
↓ download raw

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)

  1. 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).

  1. 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).

  1. 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->commandarray after kfree and skip the walk when it is NULL;
  • guard the kfree with the used_commands == 0 condition 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_testable
baseline no→ patch + rebuild →patched clean

No 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).
↓ fix.diffper-fix-DF-4614

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.