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

ipsd open/detach TOCTOU: detach can free the ipsdisk softc under an open descriptor β€” ipsd_close/strategy then write freed memory (DF-5013 pattern in ips)

Summary

Unlocked IPS_DEV_OPEN set/read between open and the detach EBUSY gate: a lost race frees the softc under the fd (devfs retains the cdev) - close executes a UAF write of the state bits, strategy writes freed devstat memory. Only root can start the race (kldunload/PCI detach) - Low; hardware-gated. DF-5013-style disabled-flag+drain fix compile-validated. NOTE routed: ips.c:796-801 copperhead sem-bit timeout silently drops a command (slot leak + never-completed bio) - file-scope prevented filing here.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5025 Β· 6 files
FileTypeDescriptionSize
env.txt β€” 400 B view raw
fix.diff β€” 2.5 KB view raw
fix_validation.log β€” 1.8 KB view raw
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
verdict.json β€” 3.6 KB view raw

DF-5025 β€” ipsd open/detach TOCTOU β†’ ipsdisk_softc UAF (ips_disk.c)

File: sys/dev/raid/ips/ips_disk.c (pass 2) Lines: 92 (unlocked open flag), 103 (unlocked close, missing NULL check), 134-137 (strategy touches dsc unlocked), 211-212 (unlocked EBUSY gate) Class: lifetime/TOCTOU β†’ use-after-free write (CWE-367 + CWE-824-family); the ips instance of the DF-5013 (mfi_disk) pattern.

Why not reproduced at runtime

Requires (a) an IBM ServeRAID IPS adapter β€” absent from the guest (env.txt: no 0x1014/0x9005 PCI devices, no /dev/ips*, no ipsd disks) β€” and (b) a root-initiated detach (kldunload/devctl) racing an unprivileged open(). Root initiation caps the security impact (a privileged trigger), which is why this is filed Low rather than High like DF-5013 (whose mfi AEN path needed no privilege).

Source-level chain (certain, no locks anywhere on the path)

  1. ipsd_open (:84-95): dsc->state |= IPS_DEV_OPEN β€” no lock.
  2. ipsd_detach (:204-216): if (dsc->state & IPS_DEV_OPEN) return (EBUSY) β€” no lock. Window: open fetched a non-NULL si_drv1 but has not set the flag yet β†’ detach proceeds β†’ devstat_remove_entry + disk_destroy (:213-214) β†’ destroy_dev β†’ cdev unlinked; newbus then frees the ipsdisk_softc_t when the child device is deleted (ips.c:304-319 ips_diskdev_free via ips_adapter_free).
  3. The racer's fd is still open: devfs holds a cdev reference until last close (release_dev, kern_conf.c:1455 / devfs_core.c:1440-1459), so ipsd_close IS still called on the destroyed device with si_drv1 pointing at the freed softc β†’ dsc->state &= ~IPS_DEV_OPEN (:103) = UAF write; likewise any strategy in flight (devstat_start_transaction(&dsc->stats), :137).
  4. Independent defect on the same line: ipsd_close (:101-103) dereferences si_drv1 with no NULL check, while ipsd_open (:90-91) has one β€” the close path panics outright on NULL.

The strategy-vs-detach variant (bio in flight while parent detaches) is partially masked by ips_adapter_free's used_commands != 0 EBUSY (ips.c:533-537), but bios that are queued-but-not-started are invisible to that gate: they are abandoned forever (see DF-5026) and their bio_driver_info softc reference goes stale.

Fix

fix.diff (modeled on the verified DF-5013 mfi fix): serialize the IPS_DEV_OPEN state changes with sc->queue_lock, mark the disk disabled in detach and drain in-flight bios (active counter, lksleep until quiescent) before disk_destroy, fail-fast in ipsd_strategy once disabled, and add the missing NULL check in ipsd_close. Compile-validated as part of the patched ips.ko build (-Werror clean) β€” see DF-5026's pack for the build log; behavioral validation is hardware-gated (not_testable).

Artifacts

env.txt (hardware-absence probe), fix.diff, this README, VERDICT.md, manifest.json, verdict.json.

VERDICT.md
↓ download raw

DF-5025 Verification Verdict

Status: not_reproduced (hardware-gated entry + privileged trigger; source-chain certain, runtime exercise impossible on this guest)

What is certain (code)

  • ipsd_open sets IPS_DEV_OPEN with no lock (ips_disk.c:92); ipsd_detach tests it with no lock (ips_disk.c:211). Nothing on the openβ†’detach path serializes them β€” a textbook TOCTOU.
  • On losing the race, detach completes disk_destroy (:214) while an fd is open; devfs retains the cdev until last close, so ipsd_close later runs dsc->state &= ~IPS_DEV_OPEN (:103) on the newbus-freed ipsdisk_softc_t β†’ use-after-free write. Reference chain: subr_disk.c:876β†’527-548 (disk_destroy), kern_conf.c:345-354 (destroy_dev), devfs_core.c:1440-1459 (release_dev), ips.c:304-319 (diskdev free).
  • ipsd_close itself lacks the si_drv1 NULL check its open twin has (compare :90-91 vs :101-103).
  • In-flight-bio variant: ips_adapter_free's used_commands gate (ips.c:533-537) does not cover queued-but-unstarted bios; those are abandoned (DF-5026) referencing a softc about to be freed.

Why it cannot be exercised here

  • Guest has no IBM ServeRAID adapter (env.txt probe: no vendor 0x1014/0x9005 PCI devices, no /dev/ips*, no ipsd disks) β€” the ips driver code is compiled into the guest kernel (kldstat -v shows ids ips/ipsd, pci/ips) but attach never runs without hardware.
  • The only detach initiators are privileged (kldunload / PCI bus detach), so even with hardware the trigger requires root β€” this caps severity at Low (contrast DF-5013, whose mfi AEN offline path was unprivileged).

Fix validation

fix.diff mirrors the verified DF-5013 mfi fix (DISABLED flag + in-flight drain + lock-serialized state + close NULL check). It compile-validates as part of the merged patched ips.ko build (-Werror clean, see DF-5026 pack / fix_validation.log). Behavioral validation hardware-gated β†’ fix_status not_testable.

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 compile-validated as part of the merged patched ips.ko module build on the guest (-Werror clean, fix_validation.log in DF-5026's pack; sources restored). Behavioral validation is hardware-gated (no IPS adapter on the guest), so the fix cannot be exercised end-to-end.

fix.diff; patched ips.ko build log (fix_validation.log, referenced from DF-5026 pack); VERDICT.md fix section
↓ fix.diffper-fix-DF-5025

Confirmed kernel references

Detail

Evidence (decisive lines)

env.txt (hardware-absence probe: no IPS PCI devices, no /dev/ips*, no ipsd disks, ips code compiled into kernel); VERDICT.md source trace with the lock-free open/detach/close/strategy citations and the disk_destroy/release_dev lifetime chain

PoC changes

no runtime PoC possible; hardware-absence probe captured as env.txt evidence

Verified recommended fix

Serialize the IPS_DEV_OPEN state changes with sc->queue_lock, add a disabled flag + in-flight drain (active counter, lksleep) in ipsd_detach before disk_destroy, fail-fast in ipsd_strategy once disabled, and add the missing si_drv1 NULL check in ipsd_close (fix.diff mirrors the verified DF-5013 mfi fix).

Verdict

Source-verified TOCTOU/UAF, runtime reproduction impossible on this guest: ipsd_open sets IPS_DEV_OPEN with no lock (ips_disk.c:92) while ipsd_detach tests it with no lock (ips_disk.c:211); losing the race lets detach complete disk_destroy (:214) under a still-open fd (devfs retains the cdev until last close, kern_conf.c:345-354/devfs_core.c:1440-1459), so the later ipsd_close executes dsc->state &= ~IPS_DEV_OPEN (:103) as a use-after-free WRITE on the newbus-freed ipsdisk_softc, and any in-flight strategy touches freed devstat memory (:137). ipsd_close additionally lacks the si_drv1 NULL check its open twin has (:101-103 vs :90-91). Guest hardware probe (env.txt): no IBM/Adaptec ServeRAID PCI devices (vendor 0x1014/0x9005), no /dev/ips*, no ipsd disks -- the ips driver is compiled in (kldstat -v ids ips/ipsd, pci/ips) but attach never runs. Even with hardware the only detach initiators are privileged (kldunload/devctl), capping severity at Low (the DF-5013 mfi pattern-instance here; DF-5013 was High only because its AEN path needed no privilege). fix.diff (DF-5013-style: lock-serialized state, disabled flag, in-flight drain before destroy, close NULL check) compile-validated in the patched ips.ko build (-Werror clean).