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

Unchecked M_PREPEND(M_NOWAIT) failure in fwe_as_output() NULL-derefs the already-freed mbuf β€” guaranteed kernel panic under mbuf pressure

Summary

M_PREPEND frees the chain and returns NULL on m_gethdr(M_NOWAIT) failure (mbuf.h:465-482, uipc_mbuf.c:1500-1521); fwe_as_output then reads m->m_pkthdr.len unconditionally -> NULL-offset fault panic. Any egress frame with <2 bytes leading space (cluster-filled/forwarded/VLAN) takes the alloc path; unpriv local user + mbuf starvation spray reaches it on an up fwe0. Pure DoS read fault. NULL-check+oerror+return-xfer fix authored. FireWire-hw-gated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4763 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.3 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
fwe_trigger.c β€” 2.7 KB view raw
build.sh β€” 89 B view raw
run.sh β€” 149 B view raw
build.log β€” 139 B view raw
run.log β€” 847 B view raw
env.txt β€” 904 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 2.7 KB view raw
manifest.json β€” 748 B view raw

DF-4763 β€” unchecked M_PREPEND(M_NOWAIT) failure in fwe_as_output() NULL-derefs the freed mbuf β†’ guaranteed kernel panic

File: sys/dev/netif/fwe/if_fwe.c:506-511 (pass 2).

Build / Run / Expected

build.sh   # cc -O -o fwe_trigger fwe_trigger.c   (on the audit guest)
run.sh     # unprivileged UDP flood toward fwe0

On hardware with an OHCI-1394 controller (fwe0 up, configured by the admin), flood the interface while exhausting mbufs (e.g. sysctl -w kern.ipc.nmbclusters=... pressure or plain socket spray) β€” occasionally the TX mbuf has < 2 bytes leading space, so M_PREPEND(m, ETHER_ALIGN, M_NOWAIT) must allocate; on failure the macro frees the chain and sets m = NULL (sys/sys/mbuf.h:465-482), and fwe_as_output unconditionally dereferences m->m_pkthdr.len (if_fwe.c:509) β†’ NULL+offset read β†’ kernel panic.

Expected: immediate Fatal kernel mode fault … fault VA 0x… / panic: lp254 ... style page fault in fwe_as_output. DoS only β€” no write primitive (the panic is a read of ~NULL+offset; the packet path stops there).

On the audit QEMU guest there is no FireWire controller (QEMU has no OHCI-1394 device model), so fwe0 cannot exist β€” see run.log (exit 2) and env.txt. Runtime verification is therefore blocked by absent hardware; the code path itself is unambiguous (VERDICT.md).

VERDICT.md
↓ download raw

DF-4763 VERDICT β€” M_PREPEND NULL dereference in fwe_as_output()

Status: NOT RUNTIME-TESTABLE on the audit guest (hardware absent)

Same blocker as DF-4762: the QEMU guest has no OHCI-1394 controller (env.txt), QEMU has no 1394 device model, so fwe0 can never attach (DRIVER_MODULE(fwe, firewire, ...), if_fwe.c:619). The trigger builds and runs on the guest and exits 2 "fwe0 not present" (run.log) β€” the PoC is sound, only the device is missing. Contract status not_testable, persisted as inconclusive for the poc_results enum.

The bug (source-level, unambiguous)

fwe_as_output() (if_fwe.c:483):

    /* keep ip packet alignment for alpha */
    M_PREPEND(m, ETHER_ALIGN, M_NOWAIT);      /* if_fwe.c:506 */
    fp = &xfer->send.hdr;
    *(u_int32_t *)&xfer->send.hdr = *(int32_t *)&fwe->pkt_hdr;
    fp->mode.stream.len = m->m_pkthdr.len;    /* if_fwe.c:509  <-- NULL deref */
    xfer->mbuf = m;
    xfer->send.pay_len = m->m_pkthdr.len;     /* if_fwe.c:511  <-- NULL deref */

DragonFly's M_PREPEND contract (sys/sys/mbuf.h:464-483):

"If how is M_NOWAIT and allocation fails, the original mbuf chain is freed and m is set to NULL."

m_prepend() (sys/kern/uipc_mbuf.c:1500-1521) confirms: m_gethdr(how) == NULL β†’ m_freem(m); return (NULL). The macro's fast path (M_LEADINGSPACE(m) >= 2) needs no allocation, but egress mbufs whose first mbuf fills the cluster to its start (e.g. cluster-backed frames with zero leading room β€” large UDP datagrams, forwarded paths, VLAN popped headers) take the allocation path. Under mbuf starvation (attacker-controllable with unprivileged socket spray against a system with default nmbufs/nmbclusters) m_gethdr(M_NOWAIT) fails and m becomes NULL β€” and is already freed. The unconditional m->m_pkthdr.len at 509 reads at ~NULL+offset (offsetof(struct mbuf, m_pkthdr.len)) β†’ kernel page fault β†’ panic.

Reachability: any local user can emit packets on fwe0 once the admin has configured/raised it (sending needs no privilege). The panic requires the allocation-failure branch, i.e. memory pressure + a cluster-head packet: probabilistic under flood, not attacker-forced deterministically. Impact = reliable eventual kernel panic (DoS); no write primitive, nothing beyond the faulting read.

Class comparison: same unchecked-M_PREPEND family as other drivers fixed over the years; lnc DF-4746-style TX-path care. Severity Medium (local kernel DoS, unusual config: FireWire NIC present and up).

Why not false-positive

The deref sequence is straight-line code with no intervening NULL check; the failure mode of M_PREPEND(M_NOWAIT) is documented in the header and implemented in m_prepend(). There is no other early return between 506 and 509.

Fix validation

Not possible on this guest (no hardware): fix_status = not_testable. fix.diff adds the NULL check, returns the xfer to the free list and counts an oerror (matches how sibling drivers handle it).

Validation procedure on real hardware (for the record)

  1. Peer traffic or local flood: ./fwe_trigger fwe0 while spraying mbufs (e.g. thousands of AF_UNIX/SO_RCVBUF sockets or a second flood) to starve m_gethdr.
  2. Baseline: expect panic in fwe_as_output within minutes.
  3. Patched (fix.diff applied, kernel rebuilt): no panic, oerrors counter increments instead.

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 against read-only sys/; cannot validate on the guest (fwe driver cannot attach without OHCI-1394 hardware).

fix.diff in this pack
↓ fix.diffper-fix-DF-4763

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (path:line proof incl. M_PREPEND contract); run.log (PoC builds BUILD_OK on guest, exits 2 'fwe0 not present' as root and unprivileged); fix.diff (NULL check returning xfer to the free list)

PoC changes

Authored from scratch (no seed); if_nametoindex probe + SO_BROADCAST UDP flood; built and ran first try on guest (BUILD_OK, exit 2 hardware-absent).

Verified recommended fix

Check m == NULL after M_PREPEND: count oerror, STAILQ_INSERT_TAIL the xfer back onto fwe->xferlist, and continue (see fix.diff).

Verdict

NOT runtime-testable on the audit guest: fwe(4) requires an OHCI-1394 FireWire controller and the QEMU guest has none (env.txt / run.log: if_nametoindex('fwe0') -> 'Device not configured'); QEMU has no 1394 device model - contract status not_testable, mapped to 'inconclusive' for the poc_results enum. The bug itself is unambiguous straight-line code: M_PREPEND(m, ETHER_ALIGN, M_NOWAIT) at if_fwe.c:506 frees the mbuf chain and returns NULL on allocation failure (sys/sys/mbuf.h:465-482, sys/kern/uipc_mbuf.c:1500-1521), and fwe_as_output() unconditionally dereferences m->m_pkthdr.len at if_fwe.c:509 and 511 -> NULL+offset read -> guaranteed kernel panic. Reachable by any unprivileged local user sending traffic on an admin-configured fwe0 under mbuf starvation; pure DoS, no write primitive.