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

login_callout never cancelled when sbp_dev is freed (stale-LUN invalidation, !VALID_LUN loop, post_explore GC) β€” softclock fires sbp_login_callout on the kfree'd sdev and runs sbp_mgm_orb writes into freed slab + fwdma_free'd ORB pages

Summary

sbp_login arms sdev->login_callout (up to 1000ms, auto_login default) but none of the three sdev free paths stop it (stale-LUN, trailing !VALID_LUN, post_explore GC -> sbp_free_target -> sbp_free_sdev); the only stop is shutdown-only sbp_logout_all. The callback feeds the freed pointer into sbp_mgm_orb: freed-slab reads, writes through the recycled slab and into fwdma_free'd ORB pages, re-arms mgm_ocb_timeout on the dangling ocb. A crafted device controls its Config ROM LUN set and bus-reset timing, so the free-while-armed window is deterministic. Mechanism PROVEN live on the guest via replica module (armed 2s callout on kmalloc'd container, kfree no stop, callback fired on freed object, two boots); end-to-end hardware-gated (no OHCI-1394).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3981 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
sbp_replica.c β€” 8.5 KB view raw
Makefile β€” 111 B ↓ download
build.sh β€” 76 B view raw
run.sh β€” 170 B view raw
build.log β€” 8.0 KB view raw
run.log β€” 1.6 KB view raw
env.txt β€” 677 B view raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.1 KB view raw

DF-3981 β€” sbp(4): login_callout never cancelled when sbp_dev is freed β†’ use-after-free (hostile FireWire device)

Where

sys/dev/disk/sbp/sbp.c

  • Arming: sbp_login() callout_reset(&sdev->login_callout, ticks<=login_delay(1000ms), sbp_login_callout, sdev) β€” sbp.c:649-650; called from sbp_probe_target (sbp.c:692-693, auto_login=1 default) for every LUN in SBP_DEV_RESET.
  • Free paths with no callout_stop(&sdev->login_callout):
  • sbp_alloc_lun() stale-LUN invalidation: sbp_cam_detach_sdev(sdev); sbp_free_sdev(sdev); β€” sbp.c:444-445 (LUN dropped from the device's Config ROM),
  • sbp_alloc_lun() trailing !VALID_LUN loop β€” sbp.c:534-541,
  • sbp_post_explore() GC: sbp_cam_detach_target(target); sbp_free_target(target); β€” sbp.c:776-786, and num_lun==0 β€” sbp.c:816-817, both reaching sbp_free_sdev β€” sbp.c:1978-1990 (kfree at :1989, no callout handling). Only sbp_logout_all (sbp.c:1959, shutdown/detach) stops it.
  • Fire: sbp_login_callout (sbp.c:625-629) β†’ sbp_mgm_orb(sdev, ORB_FUN_LGI, NULL) on the freed sdev: reads sdev->target, sbp_get_ocb(sdev) walks sdev->free_ocbs (freed slab), then writes ocb->flags, ocb->orb[0..7] into fwdma_free'd ORB pages and re-arms target->mgm_ocb_timeout with the freed ocb β€” UAF read+write of kernel heap.

Threat model

A crafted/hostile FireWire device fully controls its Config ROM (CROM_LUN set) and bus-reset timing (any node can force a bus reset). Sequence: present LUN N β†’ probe arms login_callout (up to 1 s) β†’ force bus reset and re-appear without LUN N β†’ sbp_post_explore β†’ sbp_alloc_lun frees LUN N's sdev while armed β†’ callback fires ≀1 s later on freed memory. Deterministic, attacker-timed.

Evidence in this pack

  • sbp_replica.c mechanism A (built & run on the audit guest, build.log/run.log): proves the DragonFly callout subsystem fires the callback on a kfree'd container β€” sbpprep[A]: login_callout FIRED on FREED object 0xfffff80117817880 β€” reproduced on two independent boots.
  • env.txt: guest has no FireWire OHCI hardware β†’ end-to-end trigger not runnable on this guest (status inconclusive, mechanism proven).
  • VERDICT.md: full path:line analysis.
  • fix.diff: one-line callout_stop restoration in sbp_free_sdev (+ scan_callout in sbp_free_target).

Reproduce

# on the DFly guest with /usr/src:
cd /root/kmod && make && kldload ./sbpprep.ko
sysctl kern.sbpprep_a=1      # arm + immediate kfree of container
sleep 4; dmesg | grep 'sbpprep\[A\]'
# expect: "...login_callout FIRED on FREED object 0x... (arg == freed pointer: YES)"

(For the DF-3982 sibling mechanism in the same module see ../DF-3982/.)

VERDICT.md
↓ download raw

DF-3981 VERDICT β€” sbp(4) login_callout use-after-free on sdev free paths

Status: inconclusive end-to-end (hardware-gated: guest has no FireWire OHCI controller β€” see env.txt), mechanism PROVEN on the guest kernel (callout fires on kfree'd container, two runs, two boots). Source-level certainty: certain.

Root cause (path:line, sys/dev/disk/sbp/sbp.c)

sdev->login_callout (sbp.c:183) is armed by sbp_login():

649:    callout_reset(&sdev->login_callout, ticks,
650:            sbp_login_callout, (void *)(sdev));

with ticks up to login_delay (default 1000 ms, sysctl-writable), for every newly seen/RESET LUN (sbp_probe_target, sbp.c:689-694, auto_login default 1; also sbp_reset_start_callback sbp.c:864-868, RCN failure sbp.c:1727).

The callback dereferences the argument immediately:

625: static void sbp_login_callout(void *arg)
626: {
627:    struct sbp_dev *sdev = (struct sbp_dev *)arg;
628:    sbp_mgm_orb(sdev, ORB_FUN_LGI, NULL);
629: }

Three sdev-free paths never stop the callout:

  1. sbp_alloc_lun stale-LUN invalidation (sbp.c:436-447): when the device's Config ROM (attacker-controlled content) reports a smaller LUN set after a bus reset, LUNs β‰₯ maxlun are freed: sbp_cam_detach_sdev(sdev); sbp_free_sdev(sdev); (sbp.c:444-445).
  2. sbp_alloc_lun trailing !VALID_LUN loop (sbp.c:534-541): any LUN whose CROM_LUN entry disappeared from the ROM is freed even when the array is not shrunk.
  3. sbp_post_explore garbage collection (sbp.c:776-786, 816-817): sbp_free_target() (sbp.c:1993-2014) β†’ sbp_free_sdev() (sbp.c:1978-1990) β†’ kfree(sdev, M_SBP) (sbp.c:1989) β€” the only callout it stops is target->mgm_ocb_timeout (sbp.c:2000); login_callout (per-sdev) is not touched anywhere in these functions (rg confirms the only callout_stop(&sdev->login_callout) in the driver is sbp_logout_all sbp.c:1959, reached only from sbp_shutdown/sbp_detach).

After the kfree, the armed callout fires sbp_login_callout(freed_sdev): sdev->target is read from freed slab memory; sbp_get_ocb(sdev) walks the freed free_ocbs STAILQ head (freed-slab read β†’ attacker-influenceable pointer if the slab is recycled); the resulting "ocb" is written through (ocb->flags = OCB_ACT_MGM, ocb->sdev = sdev, bzero(ocb->orb), ocb->orb[0..7] = ... β€” sbp.c:1306-1311,1319-1325) into the fwdma_free'd ORB page pool; callout_reset(&target->mgm_ocb_timeout, 5*hz, ..., ocb) re-arms with the dangling ocb (sbp.c:1351). This is a use-after-free read + write of kernel heap, plus later FireWire writes of ORB contents into freed pages (sbp_write_cmd β†’ fw_asyreq).

The window is deterministic for the attacker: login_delay default 1000 ms between arming and firing; the hostile device triggers the bus reset / ROM change inside that window.

Runtime verification performed (guest, DF 6.5-DEVELOPMENT)

Mechanism replica (sbp_replica.c, mechanism A): allocate an object containing an armed 2 s callout, kfree() the object with no stop, observe the callback execute with the freed pointer:

sbpprep[A]: armed login_callout in 0xfffff80117817880, tick=3940
sbpprep[A]: kfree(0xfffff80117817880) DONE - callout still armed, no callout_stop() (sbp.c:1978 has none)
sbpprep[A]: login_callout FIRED on FREED object 0xfffff80117817880 (arg == freed pointer: YES)

(run.log; identical result on the pre-reset boot with pointer 0xfffff8008bb64460.)

The end-to-end sbp path additionally requires a FireWire OHCI controller plus an SBP-2 device; pciconf -lv on the guest shows none (env.txt), so the full UAF cannot be triggered on this guest β€” hence inconclusive rather than reproduced. Nothing in the path is guest-specific: the callout subsystem behavior is the shipped kernel's (kern_timeout.c of this tree).

Not-a-duplicate check

Known sbp.c findings are DF-1317 (INQUIRY bcopy), DF-1318 (NULL cam_path), DF-2514 (CALC_GEOMETRY div0), DF-2515 (16-byte CDB orb overflow), DF-2516 (payload-length). None covers callout lifetime. The firewire.c-side findings (fwdev/tlabel/ioctl classes) do not cover per-sdev callouts in sbp.c.

Fix

fix.diff: stop login_callout in sbp_free_sdev (covers all three free paths) and also stop scan_callout in sbp_free_target (currently only stopped via sbp_cam_detach_target). Cannot be validated on this guest (no FireWire hardware; sbp is compiled per sys/conf/files and module build of the driver alone would not exercise the free path without hardware).

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 from line-accurate source analysis; applies cleanly (patch --dry-run verified). Cannot be kernel-build validated on this guest: the trigger path needs FireWire hardware, so a patched-kernel run has no observable delta here.

fix.diff (callout_stop additions; slot-release restructure of sbp_free_target)
↓ fix.diffper-fix-DF-3981

Confirmed kernel references

Detail

Exploit chain

crafted FireWire device: present LUN N (ROM CROM_LUN) -> probe arms login_callout (<=1s) -> device forces bus reset and re-presents ROM without LUN N -> sbp_post_explore/sbp_alloc_lun kfree(sdev) with callout armed -> softclock fires sbp_login_callout on freed slab -> sbp_mgm_orb UAF: slab-recycled reads drive pointer walks; orb writes land in freed (reusable) DMA pages; mgm timeout re-armed on dangling ocb -> subsequent status write by the attacker completes through freed state -> heap corruption / panic.

Evidence (decisive lines)

VERDICT.md (full path:line analysis); run.log (mechanism A output, two boots); build.log (module built on guest /usr/src); env.txt (no OHCI-1394 on guest); fix.diff (callout_stop in sbp_free_sdev + scan_callout/slot-release in sbp_free_target, applies cleanly)

PoC changes

No runnable end-to-end PoC possible (no FireWire hardware in guest). Wrote a kernel mechanism replica instead: kmalloc'd container + armed callout + kfree, callback reports firing on the freed pointer without dereferencing (guest-safety); requires callout_init (not CALLOUT_INIT, which is a firewire-internal alias) and DFly's 10-arg SYSCTL_ADD_PROC.

Verified recommended fix

callout_stop(&sdev->login_callout) at the top of sbp_free_sdev; also stop scan_callout and clear fwdev in sbp_free_target even when luns==NULL (fix.diff in pack).

Verdict

sbp(4) arms sdev->login_callout (sbp.c:649-650, up to login_delay=1000ms, auto_login default on) but never stops it on the three sbp_dev free paths: sbp_alloc_lun stale-LUN invalidation (sbp.c:444-445), the trailing !VALID_LUN loop (sbp.c:534-541), and sbp_post_explore GC -> sbp_free_target -> sbp_free_sdev (sbp.c:776-786, 816-817, 1978-1990, kfree at 1989; the only stop in the whole driver is sbp_logout_all sbp.c:1959, shutdown-only). A hostile FireWire device controls its Config ROM LUN set and bus-reset timing, so it can free an sdev while the callout is armed; sbp_login_callout (sbp.c:625-629) then calls sbp_mgm_orb() on the kfree'd sdev: freed-slab reads (sdev->target, free_ocbs head), writes into freed slab (ocb->flags/sdev) and into fwdma_free'd ORB pages (orb[0..7]) plus a re-armed mgm_ocb_timeout on the dangling ocb - kernel heap use-after-free read+write, deterministic attacker timing. End-to-end inconclusive only because the audit guest has no FireWire OHCI controller (pciconf, env.txt); the core mechanism - the DragonFly callout subsystem firing a callback on a kfree'd container - was PROVEN live on the guest kernel twice on two boots via mechanism replica A (run.log: 'login_callout FIRED on FREED object 0xfffff80117817880, arg == freed pointer: YES').