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

4. 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`.

5. 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.

6. 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.
