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)
PoC verification
Evidence pack
findings/poc/DF-4558 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
Recommended fix
Only kfree the softc when the SIM was actually freed (registration dropped),
and honor device_delete_child() failure.
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
-
atapi_cam_identify()allocates the softc withkmalloc(M_ATACAM)and installs it withdevice_set_softc(child, scp)(atapi-cam.c:150-151, :169). This setsDF_EXTERNALSOFTC(sys/kern/subr_bus.c:1788-1797), so newbus NEVER frees the softc β the module event handler'skfree(scp, M_ATACAM)at atapi-cam.c:927 is the ONLY free of this object. -
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 */
-
device_delete_child()βatapi_cam_detach()βfree_softc(scp)(atapi-cam.c:249-261, :878-908). Insidefree_softc(), with a live peripheral on the bus (the normal case:cd0/pass0probed, 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).
-
Control returns to the event handler, which unconditionally executes
kfree(scp, M_ATACAM)(:927). The still-registered, still-reachable SIM now holds a danglingsoftc. -
Any later CCB routed to that bus (a read on the still-open
cd0, a rescan, anything) reachesatapi_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.
- 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.
Recommended fix
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_testablefix.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
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.
No comments yet.