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