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

[CROSS-FILE: if_lagg.c via ether_reinput_oncpu] lagg_input() reinputs a consumed NULL mbuf under IFF_MONITOR: deterministic first-packet NULL-deref panic (write to 0xa0) on any IFF_UP|IFF_MONITOR lagg

Summary

lagg_input() frees the received mbuf and sets m=NULL when the lagg master is in IFF_MONITOR mode, then falls through inside 'if (m != NULL)' to ether_reinput_oncpu(scifp, NULL, REINPUT_RUNBPF). ether_reinput_oncpu accepts no NULL: with the master IFF_UP it executes m->m_pkthdr.rcvif = ifp on a NULL mbuf. Enslaving sets the slave's if_type = IFT_IEEE8023ADLAG, which diverts ALL slave RX to lagg_input before any L3 processing - so ANY received frame (ambient broadcast suffices) panics the kernel on the first packet once a legitimate admin runs 'ifconfig laggX up monitor'. VERIFIED 2/2 on stock guest: 'Fatal trap 12, fault vaddr 0xa0, Stopped at ether_reinput_oncpu+0x1a: movq %rdi,0xa0(%rsi)' - exact predicted instruction. Privileged one-time setup (create lagg + enslave + monitor up - a normal traffic-observation mode), then unauthenticated remote trigger (single frame on any slave; also locally via /dev/tapN write). Fixed-address write of a kernel pointer to unmapped page 0 - clean DoS, no controllable primitive. Distinct from DF-0367 (unlocked ifp->if_lagg deref). Fix validated (return in the monitor-discard branch + defensive NULL guard in ether_reinput_oncpu): baseline panics, patched clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3387 Β· 15 files
FileTypeDescriptionSize
trigger.c β€” 3.5 KB view raw
build.sh β€” 103 B view raw
run.sh β€” 122 B view raw
build.log β€” 643 B view raw
run.log β€” 200 B view raw
run.2.log β€” 236 B view raw
run.patched.log β€” 451 B view raw
panic.txt β€” 2.1 KB view raw
panic.2.txt β€” 1.2 KB view raw
env.txt β€” 418 B view raw
fix.diff β€” 818 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 4.1 KB view raw
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw

DF-3387 β€” lagg_input() NULL-mbuf reinput panic (cross-file: sys/net/lagg/if_lagg.c via ether_reinput_oncpu contract)

Summary

lagg_input() (sys/net/lagg/if_lagg.c:1462-1471) frees the received mbuf and sets m = NULL when the lagg master interface is in IFF_MONITOR mode β€” then falls through and calls ether_reinput_oncpu(scifp, NULL, REINPUT_RUNBPF) inside the if (m != NULL) block. ether_reinput_oncpu() (sys/net/if_ethersubr.c:1303-1333) has no NULL check; with the lagg master IFF_UP it dereferences m->m_pkthdr.rcvif (write of ifp to fixed offset 0xa0 of a NULL mbuf) β†’ Fatal trap 12, page fault at vaddr 0xa0.

Deterministic on the FIRST packet received on any slave of an IFF_UP|IFF_MONITOR lagg. Setup is privileged (ifconfig lagg0 create, laggproto failover, laggport <slave>, ifconfig lagg0 up monitor); the trigger is ordinary received traffic β€” broadcast noise on any slave is enough, so once configured the panic is remotely triggerable by any host on the network.

Reproduce (on the DragonFly guest)

# build
cc -O -g -Wall -o /root/df3387_trigger trigger.c

# run (root)
/root/df3387_trigger

Expected (baseline, stock kernel)

Serial console / vm.sh log:

[df3387] + ifconfig lagg0 up monitor -> rc=0
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address   = 0xa0
fault code      = supervisor write data, page not present
current process     = Idle
kernel: type 12 trap, code=2
Stopped at      ether_reinput_oncpu+0x1a:       movq    %rdi,0xa0(%rsi)
db>

0xa0(%rsi) with rsi == NULL is exactly m->m_pkthdr.rcvif = ifp; (if_ethersubr.c:1320) β€” write-only to a fixed near-NULL address, i.e. a clean panic, not controllable corruption.

Fixed (fix.diff applied, rebuilt kernel)

Trigger runs to completion: all eight steps succeed, the injected frame is discarded by the monitor path, the program prints NO PANIC β€” bug did not reproduce, destroys lagg0/tap0 and exits 2. Guest stays up.

Files

  • trigger.c β€” setup + RX injection (write(2) to /dev/tapN is an RX inject: tapwrite() β†’ ifp->if_input(ifp, top, NULL, -1) β†’ ether_input)
  • build.sh, run.sh β€” exact commands
  • build.log β€” compile output (also kernel-build tail for the fix run)
  • run.log, run.2.log β€” baseline runs (panic)
  • run.patched.log β€” fix-validation run (no panic)
  • panic.txt, panic.2.txt β€” full serial-console panics (baseline + 2nd run)
  • env.txt β€” guest identity
  • fix.diff β€” git-apply-able two-hunk fix (if_lagg.c primary, if_ethersubr.c defensive)
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-3387 β€” VERDICT

Status: reproduced β€” deterministic kernel NULL-deref panic, exact predicted instruction. Impact: dos (panic) β€” write-only fault at fixed vaddr 0xa0, no controllable primitive. Fix: validated (fixed) β€” patched kernel boots, same PoC harmlessly discards the frame, guest stays up.

Root cause (line-accurate)

sys/net/lagg/if_lagg.c:1462-1471:

    if (m != NULL) {
        IFNET_STAT_INC(ifp, ipackets, 1);
        IFNET_STAT_INC(ifp, ibytes, m->m_pkthdr.len);

        if (scifp->if_flags & IFF_MONITOR) {
            m_freem(m);
            m = NULL;                       /* <-- m consumed   */
        }
        ether_reinput_oncpu(scifp, m, REINPUT_RUNBPF);   /* <-- NULL passed  */
    }

sys/net/if_ethersubr.c:1303-1321 (ether_reinput_oncpu) accepts no NULL β€” first deref with the lagg master IFF_UP:

    if ((reinput_flags & REINPUT_KEEPRCVIF) == 0 ||
        m->m_pkthdr.rcvif == NULL)          /* m == NULL -> fault at 0xa0 */
    {
        m->m_pkthdr.rcvif = ifp;

Attack chain (all reproduced on the guest):

  1. ifconfig tap0 create && ifconfig tap0 up
  2. ifconfig lagg0 create && ifconfig lagg0 laggproto failover && ifconfig lagg0 laggport tap0 β€” enslaving sets the slave's if_type = IFT_IEEE8023ADLAG (if_lagg.c:674), which routes its RX to the lagg branch of ether_demux_oncpu (if_ethersubr.c:1076) β€” before any L3 processing, so frame content beyond the 14-byte header is irrelevant.
  3. ifconfig lagg0 up monitor β€” master gets IFF_UP|IFF_MONITOR.
  4. One frame received on tap0. write(2) to /dev/tap0 is an RX injection: tapwrite() (sys/net/tap/if_tap.c) ends in ifp->if_input(ifp, top, NULL, -1) β†’ ether_input β†’ ether_input_handler β†’ ether_input_oncpu β†’ ether_demux_oncpu β†’ lagg_input β†’ failover input returns the mbuf (tap0 is primary) β†’ IFF_MONITOR branch NULLs it β†’ ether_reinput_oncpu(scifp, NULL, ...).
  5. Fatal trap 12, fault vaddr 0xa0, stopped at ether_reinput_oncpu+0x1a: movq %rdi,0xa0(%rsi) β€” m == NULL (rsi), the write of ifp to m->m_pkthdr.rcvif.

Runs: 2/2 identical panics (run.log/run.2.log, panic.txt/panic.2.txt). The ssh-side write() return line is lost both times because the panic freezes the guest before sshd flushes; the serial-console mirror ([df3387] ... up monitor -> rc=0) pins the sequence β€” the only action the program takes after that line is open("/dev/tap0") + write(fd, frame, 34).

Threat model

  • Setup is privileged (ifconfig). After setup, the trigger is any received frame on any slave β€” ambient broadcast traffic suffices; a remote attacker on the L2 segment panics the box with a single packet.
  • The faulting instruction writes ifp to fixed address 0xa0 β€” not attacker-controlled, page 0 unmapped β†’ clean DoS panic, no escalation path.

Why pass-1 missed it / where it sits

This is a callsite violation of the ether_reinput_oncpu contract documented in the audited file (if_ethersubr.c:1303). The other four reinput callers (vlan if_vlan.c:630, carp ip_carp.c:1231/1253, bridge if_bridge.c:2880/2987) all pass non-NULL mbufs β€” lagg is the outlier. DF-0367 covers a different bug in lagg_input (unlocked ifp->if_lagg deref, lines 1442-1456); the NULL reinput at 1466-1470 is distinct and was unfiled.

Fix validation

fix.diff (two hunks: primary return in if_lagg.c after monitor-discard; defensive NULL guard at the top of ether_reinput_oncpu):

  • Baseline (stock #0 kernel): panic as above β€” reproduced before patching.
  • Patched: applied in-guest to /usr/src, make -j8 nativekernel KERNCONF=X86_64_GENERIC + make installkernel, rebooted into #1 kernel. Same PoC: all 8 steps rc=0, frame discarded by the monitor path, NO PANIC β€” bug did not reproduce, interfaces cleanly destroyed, guest stays up (vm.sh status = up, ssh alive). See run.patched.log + build.log.
  • Guest then reset to the clean snapshot (vm.sh reset with-src).

Notes

  • ether_reinput_oncpu's only other NULL hazard (m_freem(NULL) on a !IFF_UP interface) is harmless β€” m_freem() is while (m)-guarded (sys/kern/uipc_mbuf.c:1469). The deref at :1318-1320 is the crash.
  • Could an unprivileged user reach this without root setup? No β€” SIOCSLAGG and SIOCSIFFLAGS are privileged; the finding is privileged-config + remote/unprivileged-trigger, same profile as DF-0445 (Medium).

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 applied in-guest to /usr/src, make -j8 nativekernel KERNCONF=X86_64_GENERIC + make installkernel, rebooted into #1. Exact PoC re-run: frame harmlessly discarded by the monitor path, 'NO PANIC - bug did not reproduce' printed, lagg0/tap0 destroyed cleanly, guest remained up and ssh-able. Baseline panicked 2/2 on the same input.

run.patched.log (full PoC output on patched kernel), build.log (kernel build tail + uname)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 11 23:54:18 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

ifconfig tap0 create/up; ifconfig lagg0 create; ifconfig lagg0 laggproto failover; ifconfig lagg0 laggport tap0 (slave if_type becomes IFT_IEEE8023ADLAG -> RX diverted to lagg_input at if_ethersubr.c:1076 before L3); ifconfig lagg0 up monitor; write 34 bytes (bcast ether + minimal IPv4) to /dev/tap0 -> deterministic panic on the first received packet. Remote variant: once configured, any frame arriving on any slave (e.g. ambient broadcast) panics the kernel.

Evidence (decisive lines)

["panic.txt / panic.2.txt: 'Fatal trap 12 ... fault virtual address = 0xa0 ... Stopped at ether_reinput_oncpu+0x1a: movq %rdi,0xa0(%rsi)'", 'run.2.log: all 7 ifconfig steps rc=0, connection dropped immediately after (write() executed, output lost to the panic)', "panic.2.txt serial mirror: '[df3387] + ifconfig lagg0 up monitor -> rc=0' directly followed by the panic", 'fix.diff: two hunks (if_lagg.c return-after-monitor-discard; if_ethersubr.c defensive NULL guard)', "run.patched.log + build.log: patched kernel #1, same PoC -> 'NO PANIC - bug did not reproduce', guest stays up"]

PoC changes

Seed (none existed for this bug; written fresh). Frame content beyond the 14-byte header is irrelevant since the IFT_IEEE8023ADLAG branch in ether_demux_oncpu runs before any L3 processing; a 34-byte bcast/ETHERTYPE_IP frame is used for realism. Progress lines mirrored to /dev/console because the panic eats unflushed ssh output.

Verified recommended fix

if_lagg.c: return immediately after the IFF_MONITOR discard (do not reinput a consumed mbuf); if_ethersubr.c: defensive NULL check at top of ether_reinput_oncpu.

Verdict

Reproduced 2/2 on the stock INVARIANTS guest: lagg_input() (sys/net/lagg/if_lagg.c:1466-1470) frees the mbuf and NULLs it under IFF_MONITOR, then falls through to ether_reinput_oncpu(scifp, NULL, REINPUT_RUNBPF); with the lagg master IFF_UP, ether_reinput_oncpu (sys/net/if_ethersubr.c:1317-1320) writes ifp to m->m_pkthdr.rcvif of a NULL mbuf -> Fatal trap 12 at vaddr 0xa0, Stopped at ether_reinput_oncpu+0x1a: movq %rdi,0xa0(%rsi). Trigger is a single frame received on any slave (write(2) to /dev/tapN = RX injection via tapwrite -> if_input); setup is privileged ifconfig. Write-only fault at fixed near-NULL address: clean DoS panic, no controllable primitive, no escalation path.