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

ng_fec_addport arpcom type confusion: writes a node pointer at +sizeof(ifnet)+16 and 6 known MAC bytes at +sizeof(ifnet) past ANY non-arpcom ifnet allocation (lo0), reads 6 bytes OOB disclosed as the fec MAC (kernel pointer leak via gif), and corrupts live gif_softc fields -> kernel GPF

Summary

addport casts any ifnet to struct arpcom with no if_type check and writes ac->ac_netgraph (8-byte node pointer) and ac->ac_enaddr (6 bundle-MAC bytes), and on the first-member path READS ac_enaddr into the fec interface MAC. Measured: sizeof(ifnet)=928, ac_enaddr at +928, ac_netgraph at +944. lo0's ifnet is a standalone kmalloc(928) - both writes land outside the allocation (proven by dumpif before/after; absorbed by slab slack on this build - latent cross-object corruption). gif_softc has gif_psrc/gif_pdst pointers right after gif_if: enslaving a configured gif1 leaks 6 bytes of the gif_psrc kernel pointer to unprivileged readers via 'ifconfig fec1' (ether f0:2d:0a:4f:00:f8 = 0xfffff8004f0a2df0); enslaving an unconfigured gif0 overwrites gif_psrc with garbage -> first 'ifconfig gif0' faults in gif_ioctl (Fatal trap 9 GPF, reproduced). REPRODUCED with documented lab enablers (defect code stock): heap OOB write + kernel-pointer disclosure readable WITHOUT privilege + deterministic panic via victim deref. Control-plane trigger root-only on stock DFly. Fix validated (reject non-IFT_ETHER members): lo0/gif rejected EPROTONOSUPPORT, lo0 tail unchanged, vtnet membership unaffected.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3501 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
run.log β€” 4.2 KB view raw
leak_sample.txt β€” 907 B view raw
panic.txt β€” 576 B view raw
fecdrive.c β€” 3.7 KB view raw
Makefile β€” 56 B ↓ download
dumpif.c β€” 1.9 KB view raw
setup_guest.sh β€” 1.1 KB view raw
fix.diff β€” 710 B view raw
verdict.json β€” 3.3 KB view raw
manifest.json β€” 1006 B view raw

DF-3501 β€” ng_fec_addport arpcom type confusion: OOB write past non-arpcom ifnet allocations + kernel-pointer leak + live-field corruption (panic)

  • File: sys/netgraph/fec/ng_fec.c (v1 netgraph, LIVE)
  • Lines: 378-405 (ng_fec_addport), specifically 379 (ac->ac_netgraph = priv->node), 386-392 (first-member MAC copy), 398-405 (MAC read-back)
  • Severity: Medium (kernel heap OOB write + info leak + kernel panic; control plane root-only on stock DFly) β€” memcorrupt bucket

Root cause

ng_fec_addport() looks up ANY interface by name (ifunit()) and unconditionally casts it to struct arpcom * with no if_type validation. struct arpcom extends struct ifnet by 24 bytes (ac_enaddr[6] at +0, ac_netgraph at +16 past the ifnet). For any non-arpcom member this yields:

  • an 8-byte write of the ng node pointer at member_ifp + sizeof(ifnet) + 16,
  • a 6-byte write of the bundle MAC at member_ifp + sizeof(ifnet) + 0,
  • a 6-byte READ of the same location (first-member path) disclosed as the fec interface's MAC address (world-readable).

Measured on the guest: sizeof(struct ifnet) = 928, off(ac_enaddr) = 928, off(ac_netgraph) = 944. lo0's ifnet is a standalone kmalloc(sizeof(struct ifnet)) (sys/net/if_loop.c:111) β€” the writes land outside the 928-byte allocation. For gif, struct gif_softc has gif_if first followed by gif_psrc/gif_pdst pointers (sys/net/gif/if_gif.h:50-53) β€” the writes land on live driver fields.

Reproduction summary (full detail in run.log)

  1. OOB write proof (lo0, second member) β€” dumpif.ko shows lo0's tail (bytes past the ifnet allocation) all zeros before, and after add lo0: vtnet0's MAC 52:54:00:12:34:56 at +928 and the fec node pointer 0xfffff8004f1ae2e0 at +944. On this build the slab chunk class for 928 bytes is 960, so the writes fall in the 32-byte inter-chunk slack; any layout/size change (or a member ifnet without slack) makes this adjacent-object corruption.
  2. Kernel-pointer leak (gif1, first member) β€” ifconfig fec1 shows ether f0:2d:0a:4f:00:f8 = low 6 bytes of gif1->gif_psrc (0xfffff8004f0a2df0): a kernel heap pointer read past the ifnet and disclosed to unprivileged userspace (reading interface MACs needs no privilege).
  3. Live-field corruption β†’ panic (gif0, second member) β€” adding an unconfigured gif0 writes fec1's MAC bytes over gif0->gif_psrc (NULL β†’ garbage non-NULL) and the node pointer over gif_ro[0]; the first ifconfig gif0 dereferences it: Fatal trap 9: general protection fault … Stopped at gif_ioctl+0x9f (panic.txt).

Harness

The stock control plane cannot reach addport on this INVARIANTS build (DF-3499 then DF-3500 facet 1 panic first), so verification uses the lab module (HEAD source + documented enablers E1/E2/E3 β€” none touch the defect) driven from thread context by fecdrive.ko:

sh setup_guest.sh                       # apply enablers, build, load
sysctl -n debug.fecdrive=create
sysctl -n debug.fecdrive="add vtnet0"   # first member (ether, legal)
sysctl -n debug.fecdrive="add lo0"      # OOB write past lo0's ifnet
sysctl -n debug.fecdrive="addg gif1"    # pointer leak via fec1 MAC
sysctl -n debug.fecdrive="addg gif0"    # gif_psrc corruption
ifconfig gif0                          # -> kernel GPF panic

Fix (validated)

Reject non-ethernet members in addport (bifp->if_type != IFT_ETHER β†’ EPROTONOSUPPORT). Validated in-guest: lo0/gif1/gif0 rejected with "fec0: iface … is not an ethernet interface", dumpif shows lo0's tail unchanged (all zeros), gifs remain fully functional, vtnet0 membership still works.

VERDICT.md
↓ download raw

DF-3501 VERDICT β€” reproduced (OOB write + kernel-pointer leak + panic)

What was run

Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS). ng_fec.ko rebuilt from HEAD /usr/src with three documented lab enablers (E1: DF-3499 bcopy guard; E2: rmnode skips ng_fec_stop; E3: delport serializes member if_ioctl) β€” none touch ng_fec_addport's arpcom accesses, which are stock. Node operations driven from thread context with fecdrive.ko because the stock csock path panics earlier (DF-3499/DF-3500).

Measurements (dumpif.ko)

sizeof(struct ifnet)=928, sizeof(struct arpcom)=952, off(ac_enaddr)=928, off(ac_netgraph)=944. lo0 ifp=0xfffff8008e0ead40, a standalone kmalloc(928) (if_loop.c:111); slab chunk class 960 (zoneindex rounds 928 up to the next 64 multiple).

1) OOB write past lo0's ifnet allocation before: tail+0=0, tail+16=0 (all zero) after create + add vtnet0 + add lo0: tail+0 = 0000563412005452 (= 52 54 00 12 34 56 = vtnet0's MAC, written at lo0_ifp+928..933 via ng_fec.c:404 through the arpcom cast) tail+16 = fffff8004f1ae2e0 (= fec0's ng_node pointer, written at lo0_ifp+944..951 via ng_fec.c:379) Both writes are outside the 928-byte allocation. On this build they land in the 32-byte inter-chunk slack of the 960-byte chunk class; the primitive is layout-dependent (any change in sizeof(struct ifnet), the member's allocation geometry, or slab classing turns it into adjacent-object corruption). The write itself is fully proven by the dump delta.

2) Kernel heap pointer leak (unprivileged readback) gif1 configured (ifconfig gif1 tunnel 10.0.2.15 10.0.2.2 allocates gif_psrc), added as FIRST member of a second fec node: ifconfig fec1 -> ether f0:2d:0a:4f:00:f8 = low 6 bytes of gif1->gif_psrc = 0xfffff8004f0a2df0 (kmem pointer pattern). Interface MACs are readable without privilege (getifaddrs/SIOCGIFADDR), so this is a kernel-heap-pointer disclosure to unprivileged users (6 bytes per enslaved non-arpcom interface; on KASLR-less guests it directly reveals the kmem base region).

3) Live-field corruption -> kernel panic gif0 created unconfigured (gif_psrc == NULL), added as second member: fec1's MAC bytes (the leaked pointer bytes) are written over gif0's gif_psrc (gif_softc+928..933) and the ng node pointer over gif_softc+944 (gif_ro[0]). First ifconfig gif0: Fatal trap 9: general protection fault while in kernel mode Stopped at gif_ioctl+0x9f: movzbl (%rsi),%ecx (gif_ioctl dereferences the non-NULL garbage gif_psrc β€” sys/net/gif/if_gif.c gif_getconfig path.)

Fix validation

Candidate fix (reject if_type != IFT_ETHER in addport) applied on top of the enablers, module rebuilt, same scenario re-run: add vtnet0 = 0 (ether member still works, fec0 MAC = vtnet0's MAC); add lo0 = 43 (EPROTONOSUPPORT, "fec0: iface lo0 is not an ethernet interface"); add gif1 / add gif0 = 43 likewise; dumpif lo0 tail unchanged (all zeros β€” no OOB write); ifconfig gif0/gif1 fully functional (no corruption); no leak (no fec MAC derived from foreign memory). fix_status: fixed.

Honest scope notes

  • Control-plane trigger: netgraph control socket (root on stock DFly). The leak READBACK side needs no privilege once the state exists.
  • The lo0-write lands in allocator slack on this exact build (proven write, latent cross-object impact); the gif path corrupts live objects in-allocation (type confusion) and panics deterministically.
  • Exploitability toward uid0: the write value/offset are fixed (node pointer at +16; 6 known MAC bytes at +0) and the trigger is root-only on stock config, so no privesc chain is claimed.

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
↓ fix.diffper-fix-DF-3501

Confirmed kernel references

Detail

Exploit chain

create fec node -> add_iface (first member, sets bundle MAC) -> add_iface lo0|gifN (arpcom cast writes node ptr + known MAC past the member's ifnet allocation / into gif_softc fields) -> leak readback via ifconfig (unprivileged) or victim deref (ifconfig gifN -> kernel panic)

Evidence (decisive lines)

run.log (full dumpif before/after + leak + panic sequence); leak_sample.txt (f0:2d:0a:4f:00:f8 = low bytes of 0xfffff8004f0a2df0); panic.txt (Fatal trap 9 at gif_ioctl+0x9f); fecdrive.c/dumpif.c sources; VERDICT.md fix-validation section

PoC changes

none to the defect code; lab enablers E1/E2/E3 (documented in setup_guest.sh / ../DF-3500) required to reach addport on the INVARIANTS guest; fecdrive.ko drives the node from thread context

Verified recommended fix

Reject non-ethernet members: if (bifp->if_type != IFT_ETHER) return EPROTONOSUPPORT in ng_fec_addport

Verdict

ng_fec_addport blindly casts any ifnet to struct arpcom with no if_type check: for non-arpcom members it writes an 8-byte ng-node pointer at member_ifp+sizeof(struct ifnet)+16 and 6 known MAC bytes at +0, and (first-member path) reads 6 bytes from the same location, disclosed as the fec interface MAC. Proven on the guest three ways: (1) dumpif delta on lo0 (standalone kmalloc(928) ifnet): vtnet0's MAC and the fec node pointer appear at +928/+944 past the allocation (absorbed by 32B slab slack on this build β€” write proven, cross-object impact latent); (2) unprivileged leak: ifconfig fec1 shows the low 6 bytes of gif1->gif_psrc (0xfffff8004f0a2df0 kmem pointer); (3) deterministic panic: adding gif0 overwrites gif_softc.gif_psrc with garbage -> ifconfig gif0 -> GPF in gif_ioctl. Fixed and validated: if_type != IFT_ETHER rejected (EPROTONOSUPPORT), lo0 tail unchanged, gifs functional.