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)
PoC verification
Evidence pack
findings/poc/DF-3981 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Arming:
sbp_login()callout_reset(&sdev->login_callout, ticks<=login_delay(1000ms), sbp_login_callout, sdev)β sbp.c:649-650; called fromsbp_probe_target(sbp.c:692-693,auto_login=1default) for every LUN inSBP_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_LUNloop β sbp.c:534-541,sbp_post_explore()GC:sbp_cam_detach_target(target); sbp_free_target(target);β sbp.c:776-786, andnum_lun==0β sbp.c:816-817, both reachingsbp_free_sdevβ sbp.c:1978-1990 (kfree at :1989, no callout handling). Onlysbp_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 freedsdev: readssdev->target,sbp_get_ocb(sdev)walkssdev->free_ocbs(freed slab), then writesocb->flags,ocb->orb[0..7]intofwdma_free'd ORB pages and re-armstarget->mgm_ocb_timeoutwith 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.cmechanism 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-linecallout_stoprestoration insbp_free_sdev(+ scan_callout insbp_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/.)
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:
- 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). - 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.
- 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 istarget->mgm_ocb_timeout(sbp.c:2000);login_callout(per-sdev) is not touched anywhere in these functions (rg confirms the onlycallout_stop(&sdev->login_callout)in the driver issbp_logout_allsbp.c:1959, reached only fromsbp_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_testablefix.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)
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').
No comments yet.