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

oce_detach frees ifp (if_free) while interrupts are still enabled and EQ0 deliberately re-armed by oce_if_deactivate: async MCC event in the window reaches oce_mq_handler -> if_link_state_change on the freed ifnet (use-after-free)

Summary

oce_if_deactivate ends by re-enabling interrupts and arming eq[0] 'to get MCC async events'. oce_detach then runs ether_ifdetach + if_free BEFORE oce_hw_shutdown disables interrupts. A link-state async CQE in that window: fast_isr -> oce_mq_handler -> process_link_state -> if_link_state_change on the freed ifnet, plus wq/rq stat writes through sc->ifp. Timer callout terminated correctly; only the interrupt/taskqueue path outlives the free. Reorder-shutdown fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4080 Β· 16 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
df4080_oce_poc.c β€” 19.3 KB view raw
Makefile β€” 163 B ↓ download
build.sh β€” 198 B view raw
run.sh β€” 291 B view raw
build.log β€” 848 B view raw
run.log β€” 2.0 KB view raw
panic.txt β€” 1.2 KB view raw
panic_console_full.log β€” 4.0 KB view raw
fix.diff β€” 566 B view raw
fix_build.log β€” 5.8 MB ↓ download
fix_run.log β€” 2.0 KB view raw
env.txt β€” 423 B view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 6.4 KB view raw

DF-4080 β€” oce_detach: if_free() before interrupts are disabled while EQ0 is

deliberately re-armed by oce_if_deactivate β†’ async-event handlers run on a

freed ifnet (use-after-free window)

File: sys/dev/netif/oce/oce_if.c (pass 2) Class: object lifetime / free-while-in-flight (UAF on detach), bucket memcorrupt VERIFIED: reproduced (probe evidence + panic) on the DFly QEMU guest via the production-handler KLD dispatch rig; fix (reorder) build- and disassembly-validated.

How it was verified without OneConnect hardware

The guest kernel is built with device oce, so the driver's production code β€” including the static taskqueue body oce_intr() β€” lives in the RUNNING kernel image. The KLD rig (df4080_oce_poc.c):

  • references the kernel's own oce_hw_intr_enable(), oce_arm_eq(), ether_ifattach(), ether_ifdetach(), if_free(), oce_mq_handler(), oce_rq_handler(), oce_alloc_rx_bufs() and ring helpers as externs;
  • resolves the static oce_intr() (oce_if.c:553 β€” exactly what oce_fast_isr enqueues at oce_if.c:668 and the taskqueue thread runs) from /boot/kernel/kernel via nm(1) and injects it through a sysctl;
  • fabricates the post-attach state (BE3 softc, real EQ/CQ/RQ busdma rings, real posted clusters, REAL ether_ifattach()ed ifnet, MMIO BAR stand-ins);
  • then replays the production teardown sequence VERBATIM with real calls: 1. the tail of oce_if_deactivate (oce_if.c:2017-2018): oce_hw_intr_enable(sc); oce_arm_eq(sc, eq0, 0, TRUE, FALSE); 2. the oce_detach ordering (oce_if.c:338-342): ether_ifdetach(sc->ifp); if_free(sc->ifp); (where oce_hw_shutdown β€” the only place interrupts get disabled, oce_hw.c:345 β€” would run only AFTER the free) 3. the late async event: REAL oce_intr(&ii, 1) β€” the same function, same argument the taskqueue thread would receive in the window.

Results (stock kernel #0 Thu Jul 2 06:02:54 UTC 2026)

probe mode (survives, full evidence): - CFG PCICFG_INTR_CTRL=0x20000000 β†’ HOSTINTR_MASK SET: interrupts ENABLED; DB PD_EQ_DB=0x20000400 β†’ EQ0 doorbell ARMED (both from the REAL deactivate tail); - ifnet FREED by REAL if_free() in that state, sc->ifp left dangling exactly as oce_detach leaves it; - REAL late oce_intr dispatch on the freed ifnet: - oce_mq_handler consumed the link-state async CQE β†’ process_link_state ran post-free β†’ sc->link_status=1 (LINK_UP) β†’ if_link_state_change(freed ifp); - sc->ifp->if_ierrors++ and sc->ifp->if_ipackets++ β€” UAF WRITES 0β†’1 through the freed ifnet (oce_if.c:1606 / :1354); - a chain DELIVERED via the freed ifnet's if_input (oce_if.c:1373); - slot-reuse: kmalloc(M_IFNET, sizeof(arpcom)) immediately returned the freed ifnet's exact address β€” every UAF write above landed in memory that belongs to the next allocator.

panic mode (kernel panic): - the freed ifnet slot is REALLY re-allocated before the late dispatch (kmalloc(M_IFNET, sizeof(arpcom), M_ZERO) β€” the same allocation if_alloc() itself performs; M_ZERO wipes if_input); - the REAL late oce_intr β†’ oce_rq_handler β†’ oce_rx calls sc->ifp->if_input(sc->ifp, m, NULL, -1) through the re-owned memory: Fatal user address access from kernel mode from sysctl at 0x0, Fatal trap 12: page fault ... instruction pointer = 0x8:0x0 β€” a UAF crash through a legitimate allocator state (no synthetic poison).

Fix validation (fix.diff, fix_build.log, fix_run.log)

fix.diff reorders oce_detach: ether_ifdetach β†’ oce_hw_shutdown β†’ if_free (interrupts disabled and queues destroyed BEFORE the ifnet is freed). Applied to guest /usr/src, kernel rebuilt (make -j6 nativekernel KERNCONF=X86_64_GENERIC, full log) and installed, booted #1: Thu Oct 15 13:06:11 UTC 2026. objdump of the RUNNING patched kernel's oce_detach proves the shipped call order: ether_ifdetach β†’ oce_hw_shutdown β†’ if_free. The runtime race itself (real async event inside the microsecond window) cannot be exercised without OneConnect hardware β€” the rig dispatches the interrupt body directly, which the reorder cannot and must not prevent; what the reorder guarantees is that no such dispatch can exist after if_free in production.

Reproduce

vm.sh run_root 'sh /root/df4080/build.sh'
vm.sh run_root 'sh /root/df4080/run.sh probe'    # survives, full evidence
vm.sh reset with-src                              # fresh boot
vm.sh run_root 'sh /root/df4080/run.sh panic'    # Fatal trap 12 at ip=0

Trigger in production: privileged devctl detach oce0 / kldunload if_oce / shutdown racing a firmware async (link) event in the if_free β†’ intr-disable window.

VERDICT.md
↓ download raw

DF-4080 VERDICT β€” oce_detach if_free-before-intr-disable UAF window

Status: REPRODUCED (probe: post-free dispatch + UAF writes + UAF delivery + slot reuse; panic: Fatal trap 12 through the freed-then-reallocated ifnet). Fix build- and disassembly-validated.

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (stock) and #1 (patched), x86_64. Method: production-handler KLD dispatch rig (DF-4106/DF-4266 evidence class) β€” device oce is compiled into the guest's X86_64_GENERIC kernel.

What ran (production code, nothing recompiled)

Extern-bound kernel functions: oce_hw_intr_enable, oce_arm_eq, oce_arm_cq, ether_ifattach, ether_ifdetach, if_free, oce_mq_handler, oce_rq_handler, oce_alloc_rx_bufs, oce_free_posted_rxbuf, oce_create_ring_buffer, oce_destroy_ring_buffer. nm-injected static: oce_intr (0xffffffff805c7590; the taskqueue body oce_if.c:553 β€” what oce_fast_isr enqueues at oce_if.c:668).

Fabricated post-attach state: BE3 softc; real EQ (256Γ—4B) / MQ-CQ (256Γ—16B) / RX-CQ / RQ busdma rings from the kernel's own ring allocator; 8 real posted clusters; a REAL ether_ifattach()ed ifnet ("dfoce0") with an if_input shim; MMIO BAR stand-in pages; EQ wired to MQ-CQ (handler = real oce_mq_handler) and RX-CQ (handler = real oce_rq_handler), exactly as oce_queue_init_all wires them.

The replayed production sequence (all REAL calls)

  1. oce_if_deactivate tail (oce_if.c:2014-2018, under dev_lock): oce_hw_intr_enable(sc) + oce_arm_eq(sc, eq0, 0, TRUE, FALSE). Observable: CFG PCICFG_INTR_CTRL = 0x20000000 (HOSTINTR_MASK, bit 29, SET) and DB PD_EQ_DB = 0x20000400 (doorbell armed) β€” interrupts are LIVE with the queues quiesced, precisely the state the finding describes at oce_detach entry.
  2. oce_detach ordering (oce_if.c:338-342): ether_ifdetach(ifp) + if_free(ifp). sc->ifp left dangling (oce_detach never NULLs it). The only intr-disable (oce_hw_shutdown β†’ oce_hw_intr_disable, oce_hw.c:345) would run AFTER the free β€” that is the window.
  3. The late async event: firmware wrote a link-state CQE (valid|async| event_type=ASYNC_EVENT_CODE_LINK_STATE, link_status=LINK_UP) into the MQ CQ ring and one EQE into the EQ ring; the taskqueue body runs: REAL oce_intr(&ii, 1).

Observed on the stock kernel

probe mode (run.log, survives): - link event processed on the FREED ifnet: MQ CQE dw3 cleared by the real handler (oce_if.c:2125), sc->link_status=1 (process_link_state ran post-free, oce_if.c:2058-2075), if_link_state_change(freed ifp) executed (survives on freed-but-intact memory); - UAF WRITES through sc->ifp: ifi_ierrors 0β†’1 (oce_if.c:1606), ifi_ipackets 0β†’1 (oce_if.c:1354) β€” read back from the freed region; - UAF DELIVERY: a chain handed to if_input THROUGH the freed ifnet (oce_if.c:1373); - slot reuse: kmalloc(M_IFNET, sizeof(arpcom)) returned the freed ifnet's exact address (0xfffff801186bd1c0 / 0xfffff8008fa5e480 across runs) β€” the UAF writes corrupt whichever object takes the slot.

panic mode (panic.txt, panic_console_full.log): - slot REALLY re-allocated before the dispatch (kmalloc M_ZERO β€” the same allocation if_alloc performs; wipes if_input to NULL); - REAL late oce_intr β†’ oce_rq_handler β†’ oce_rx β†’ sc->ifp->if_input(sc->ifp, m, NULL, -1): Fatal user address access from kernel mode from sysctl at 0x0 / Fatal trap 12: page fault ... ip=0x8:0x0 β€” indirect CALL through the freed ifnet's reallocated content. Guest down in DDB. This is a UAF crash through a legitimate allocator state, not a synthetic poison.

Honest scope notes

  • The rig invokes the taskqueue body directly because no OneConnect device can raise the real MSI/INTx interrupt; everything the handler touches is production code and real (busdma rings, posted clusters, attached ifnet, the free). The window itself is source-certain (ordering at oce_if.c:338-342 vs oce_hw.c:345) and the re-arm tail executed for real.
  • The production trigger is privileged (detach/kldunload/shutdown) racing a firmware async event β€” realistic outcome per the finding: panic or ifnet-slot corruption, not an unprivileged remote primitive.

Fix validation (fix.diff / fix_build.log / fix_run.log)

fix.diff reorders oce_detach: ether_ifdetach β†’ oce_hw_shutdown β†’ if_free. Applied to guest /usr/src, kernel rebuilt (make -j6 nativekernel, full 41592-line fix_build.log, BUILD_DONE_OK), rebooted #1 Thu Oct 15 13:06:11. objdump of the RUNNING patched kernel (fix evidence) proves the shipped order:

ffffffff805c8845: callq ether_ifdetach
ffffffff805c884d: callq oce_hw_shutdown   <- intr disable + queue destroy
ffffffff805c8859: callq if_free           <- ifnet freed LAST

After oce_hw_shutdown, interrupts are disabled and the queues/EQs are destroyed, so no fast-isr/taskqueue dispatch can exist past if_free in production. The rig on the patched kernel still demonstrates the handler mechanics (expected β€” it dispatches oce_intr manually, which no ordering can prevent); that is why fix validation is at build+binary level, with the runtime race explicitly requiring 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (reorder oce_detach: ether_ifdetach -> oce_hw_shutdown -> if_free) applied to guest /usr/src, kernel rebuilt via make -j6 nativekernel KERNCONF=X86_64_GENERIC (fix_build.log, BUILD_DONE_OK) and rebooted (#1 Thu Oct 15 13:06:11 UTC 2026); objdump of the RUNNING patched kernel proves the shipped call order (ether_ifdetach @+0xa5, oce_hw_shutdown @+0xad, if_free @+0xb9), i.e. interrupts are disabled and queues destroyed before the ifnet is freed, closing the window. The runtime race itself cannot be exercised without OneConnect hardware (the rig dispatches the interrupt body directly, which no ordering can or should prevent), so validation is build- plus shipped-binary-level.

objdump excerpt in VERDICT.md: ffffffff805c8845 callq ether_ifdetach; ffffffff805c884d callq oce_hw_shutdown; ffffffff805c8859 callq if_free; fix_build.log (full 41592-line rebuild + install completed 13:11:22 UTC 2026, BUILD_DONE_OK).
↓ fix.diffper-fix-DF-4080

Confirmed kernel references

Detail

Exploit chain

privileged detach/kldunload/shutdown + firmware async (link) event inside the if_free -> oce_hw_intr_disable window (EQ0 deliberately re-armed by oce_if_deactivate's tail): fast_isr -> taskqueue oce_intr -> oce_mq_handler -> process_link_state -> if_link_state_change(freed ifnet) and oce_rq/oce_wq handlers write ifnet stats through sc->ifp; freed ifnet slot is immediately re-allocatable (proven alias) so the late writes corrupt the new owner; oce_rx's if_input call through the freed ifnet is an indirect call through re-owned memory β€” demonstrated Fatal trap 12.

Evidence (decisive lines)

run.log (probe: HOSTINTR_MASK SET + EQ doorbell ARMED from the REAL deactivate tail; ifnet FREED; LATE oce_intr dispatch; link_status=UP post-free; MQ CQE consumed; ifi_ierrors/ifi_ipackets 0->1 UAF writes; chain delivered through freed ifnet; slot-reuse alias); panic.txt + panic_console_full.log (Fatal trap 12, ip=0x8:0x0 after 'slot reuse ... if_input is now 0'); fix_build.log (full rebuild, BUILD_DONE_OK); fix_run.log; VERDICT.md (objdump of patched kernel: ether_ifdetach -> oce_hw_shutdown -> if_free)

PoC changes

Seed pack had no PoC (marked hardware-gated/untested). Replaced with the production-handler KLD dispatch rig: extern-binds the kernel's own teardown/attach/handlers, nm-injects the static taskqueue body oce_intr, fabricates the post-attach state (real busdma rings, real posted clusters, REAL ether_ifattach), replays the oce_if_deactivate tail + oce_detach ordering with real calls, and dispatches the late interrupt exactly as the taskqueue thread would.

Verified recommended fix

In oce_detach, call oce_hw_shutdown() (intr disable + queue teardown) BEFORE if_free(); a belt-and-braces variant additionally drains sc->intrs[].tq without re-arming.

Verdict

REPRODUCED via the production-handler KLD dispatch rig: the guest kernel ships device oce, so the rig binds the kernel's OWN teardown/interrupt code (extern: oce_hw_intr_enable, oce_arm_eq, ether_ifattach/detach, if_free, oce_mq_handler, oce_rq_handler, posting/ring helpers; nm-injected static: oce_intr, the taskqueue body) over a fabricated post-attach softc with real busdma EQ/CQ/RQ rings, real posted clusters and a REAL ether_ifattach()ed ifnet. Replay of the production sequence with real calls: (1) the REAL oce_if_deactivate tail (oce_if.c:2017-2018) left interrupts ENABLED (CFG PCICFG_INTR_CTRL=0x20000000, HOSTINTR_MASK set) and EQ0 ARMED (doorbell written); (2) the REAL oce_detach ordering (ether_ifdetach + if_free) freed the ifnet in that state, sc->ifp dangling; (3) the late async event dispatched as the taskqueue would: REAL oce_intr -> REAL oce_mq_handler consumed the link-state CQE, process_link_state flipped sc->link_status to UP and called if_link_state_change on the FREED ifnet; REAL oce_rq_handler/oce_rx wrote ifi_ierrors/ifi_ipackets THROUGH the freed ifp (UAF writes 0->1) and delivered a chain via the freed ifnet's if_input. Slot reuse proven: kmalloc(M_IFNET, sizeof(arpcom)) immediately returned the freed ifnet's exact address. Panic mode: with the freed slot really re-allocated (M_ZERO, the same allocation if_alloc performs), the REAL oce_rx called sc->ifp->if_input(...) through the re-owned memory -> 'Fatal user address access from kernel mode' / Fatal trap 12 at ip=0x0 β€” a UAF crash through a legitimate allocator state (no synthetic poison). Fix (reorder oce_detach: hw_shutdown before if_free) build-validated and proven in the shipped binary's disassembly; the runtime race itself needs OneConnect hardware.