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

MOD_UNLOAD ignores device_delete_child() failure and unconditionally kfrees the external softc while free_softc() can leave the cam_sim registered β€” post-kldunload use-after-free (lockmgr on freed+uninited state_lock) on the still-reachable SIM

Summary

The MOD_UNLOAD case ignores device_delete_child's return and kfrees the softc unconditionally, but free_softc deliberately keeps the cam_sim alive when xpt_bus_deregister fails (live peripherals - the normal case) with state_lock already lockuninit'ed: the kfree dangles sim->softc (its only free - DF_EXTERNALSOFTC), and the next CCB runs lockmgr over freed memory. Distinct from DF-2500 (hcbs vs softc). Not runnable on the guest (natapicam compiled-in); trace-verified. Free-only-when-SIM-gone + EBUSY fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4558 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
run.sh β€” 177 B view raw
build.sh β€” 117 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 1015 B view raw
verdict.json β€” 3.2 KB view raw

DF-4558 β€” atapi-cam MOD_UNLOAD kfrees the softc while the cam_sim can still be registered

File

sys/dev/disk/nata/atapi-cam.c β€” atapi_cam_event_handler() MOD_UNLOAD case (:910-936), interacting with free_softc() (:878-908).

Root cause (traced, not executed β€” see VERDICT.md)

atapi_cam_event_handler MOD_UNLOAD does:

device_t child = devlist[--devcount];
struct atapi_xpt_softc *scp = device_get_softc(child);
device_delete_child(device_get_parent(child), child);   /* return value ignored */
if (scp != NULL)
    kfree(scp, M_ATACAM);                               /* unconditional */

device_delete_child() β†’ atapi_cam_detach() β†’ free_softc(scp). Inside free_softc(), when the CAM bus is still busy (a cd0/pass0 peripheral still holds a path on the bus β€” normal with the medium open/probed), xpt_bus_deregister() returns non-zero (:895-896), BUS_REGISTERED is kept, and the SIM is deliberately NOT freed (:898-903, "Can't free %s SIM"). The softc, however, is lockuninited (:906) and then unconditionally kfreed by the event handler (:927) β€” the still-registered cam_sim keeps sim->softc pointing at freed memory. The next CCB routed to that bus (atapi_action) does lockmgr(&softc->state_lock, LK_EXCLUSIVE) (:482) on the freed, uninitialized lock β†’ use-after-free write; every field access in atapi_action is on freed memory.

Additionally, device_delete_child()'s own failure (detach refused / device busy paths in newbus) is also ignored before the same unconditional kfree.

The softc is an DF_EXTERNALSOFTC (allocated in atapi_cam_identify(), device_set_softc() at :169, sys/kern/subr_bus.c:1788-1797), so newbus never frees it β€” this kfree is the only free, but it happens while the SIM is live.

Trigger

kldunload atapicam (module variant of the driver) while any CAM peripheral on the atapicam bus is still alive β€” e.g. cd0 probed/open β€” then issue any I/O / rescan on that bus.

Guest status

The audit guest (X86_64_GENERIC) compiles natapicam into the kernel (sys/config/X86_64_GENERIC:82) β€” no loadable atapicam.ko exists there, so kldunload cannot execute this path on that guest. Logic verified by trace; not executed. Severity Low: trigger is privileged (kldunload = root).

Only kfree the softc when the SIM was actually freed (registration dropped), and honor device_delete_child() failure.

VERDICT.md
↓ download raw

DF-4558 VERDICT β€” MOD_UNLOAD frees the softc while the cam_sim can still be registered

Verdict: logic CONFIRMED by trace; NOT EXECUTED on the audit guest (path unreachable there)

status: not_reproduced in the machine verdict means exactly and only: the guest's X86_64_GENERIC compiles natapicam into the kernel (sys/config/X86_64_GENERIC:82; guest kldstat shows no atapicam.ko), so the module event handler's MOD_UNLOAD case never runs on that machine and kldunload atapicam cannot be issued. The defect itself is structural and is established by source trace below β€” it is fully exercisable on any system running the loadable atapicam.ko module variant.

The trace

  1. atapi_cam_identify() allocates the softc with kmalloc(M_ATACAM) and installs it with device_set_softc(child, scp) (atapi-cam.c:150-151, :169). This sets DF_EXTERNALSOFTC (sys/kern/subr_bus.c:1788-1797), so newbus NEVER frees the softc β€” the module event handler's kfree(scp, M_ATACAM) at atapi-cam.c:927 is the ONLY free of this object.

  2. MOD_UNLOAD path (atapi-cam.c:916-930):

    device_t child = devlist[--devcount]; struct atapi_xpt_softc scp = device_get_softc(child); device_delete_child(device_get_parent(child), child); / result IGNORED / if (scp != NULL) kfree(scp, M_ATACAM); / unconditional */

  3. device_delete_child() β†’ atapi_cam_detach() β†’ free_softc(scp) (atapi-cam.c:249-261, :878-908). Inside free_softc(), with a live peripheral on the bus (the normal case: cd0/pass0 probed, or the medium open):

    if ((scp->flags & BUS_REGISTERED) != 0) { if (xpt_bus_deregister(cam_sim_path(scp->sim)) == 0) scp->flags &= ~BUS_REGISTERED; / only cleared on success / } if (scp->sim != NULL) { if ((scp->flags & BUS_REGISTERED) == 0) cam_sim_free(scp->sim); else kprintf("Can't free %s SIM (still registered)\n", ...); / sim LEAKED, still live / }

When deregistration fails (peripherals still hold paths on the bus), BUS_REGISTERED remains set, the SIM is deliberately kept alive in the XPT bus table β€” with sim->softc still pointing at scp β€” and then lockuninit(&scp->state_lock) runs (:906).

  1. Control returns to the event handler, which unconditionally executes kfree(scp, M_ATACAM) (:927). The still-registered, still-reachable SIM now holds a dangling softc.

  2. Any later CCB routed to that bus (a read on the still-open cd0, a rescan, anything) reaches atapi_action():

    struct atapi_xpt_softc softc = cam_sim_softc(sim); / freed memory / ... lockmgr(&softc->state_lock, LK_EXCLUSIVE); / UAF write on freed+lockuninit'd lock / if (softc->flags & DETACHING) ... / UAF read */

This is a use-after-free WRITE primitive (lockmgr mutates the freed lock word) plus reads of attacker-timing-controlled freed-heap contents.

  1. Independently, device_delete_child()'s own failure return is also ignored before the same unconditional kfree β€” detach refused (device busy in newbus) would leave the device fully alive while its softc is freed.

Why Low severity

Trigger is kldunload atapicam β€” root-only, and unloading storage drivers from under live I/O is already a privileged act. The bug is nonetheless a real lifetime defect distinct from DF-2500 (in-flight hcbs outliving detach): here the softc itself is freed while the SIM object remains reachable by the XPT.

Move softc ownership out of the unload handler: free the softc only where the SIM is actually freed (when BUS_REGISTERED was cleared and cam_sim_free() ran, or immediately when no sim was allocated), and abort the unload (return EBUSY from the event handler) when device_delete_child() fails or the SIM could not been deregistered/freed.

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

fix.diff authored (guard: only free the external softc when BUS_REGISTERED was cleared and device_delete_child succeeded; return EBUSY to abort the unload otherwise). Not kernel-built: the path cannot be exercised on this guest (driver compiled-in); logic-compiles by inspection (uses only existing symbols).

findings/poc/DF-4558/fix.diff
↓ fix.diffper-fix-DF-4558

Confirmed kernel references

Detail

Exploit chain

kldunload atapicam (root) with a live CAM peripheral on the bus -> xpt_bus_deregister fails -> SIM kept registered, BUS_REGISTERED stays set -> lockuninit(state_lock) -> handler kfree(scp) -> sim->softc dangles -> any cd0 I/O/rescan -> atapi_action reads/writes freed softc (lockmgr UAF write). Escalation not applicable: trigger requires root.

Evidence (decisive lines)

VERDICT.md (full trace with path:line); README.md; fix.diff (git apply --check OK, scoped to the unload handler)

PoC changes

n/a - no PoC executed; run.sh documents the hypothetical trigger for modular systems

Verified recommended fix

Free the softc only when the SIM was actually deregistered/freed; honor device_delete_child() failure and abort the unload with EBUSY instead of leaving a dangling cam_sim->softc

Verdict

Not executed: the audit guest compiles natapicam into X86_64_GENERIC (sys/config/X86_64_GENERIC:82; kldstat shows no atapicam.ko) so the MOD_UNLOAD module-event path (atapi-cam.c:916-930) cannot run there and kldunload atapicam is not possible. The defect itself is established by source trace (VERDICT.md): device_delete_child()'s return is ignored and kfree(scp, M_ATACAM) runs unconditionally at :927 even when free_softc() left BUS_REGISTERED set and the cam_sim deliberately alive (:894-903) - the still-registered SIM keeps a dangling softc, and the next CCB on that bus runs atapi_action on freed memory including lockmgr on a freed, lockuninit'd state_lock (:482) - a UAF write. Additionally device_delete_child() failure itself (detach refused) also precedes the unconditional kfree. Real but privileged-only (kldunload=root); any system running the loadable atapicam.ko module is exposed.