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