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

BPF write to ng_iface reads uninitialized sa_data, can KASSERT-panic the kernel

Field Value
ID DF-0607
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-908 Use of Uninitialized Resource; CWE-754 Improper Check for Unusual Exceptional Conditions
File sys/netgraph7/iface/ng_iface.c
Lines 428-431, 481
Area netgraph7 (ng_iface virtual interface node)
Confidence likely
Discovered 2026-07-02
Reported pending

Summary

ng_iface_output treats dst->sa_data as an out-of-band address-family source when dst->sa_family == AF_UNSPEC, but for DLT_NULL BPF writes (the link type ng_iface uses), bpf_movein never initializes sa_data, so the kernel reads 4 bytes of uninitialized stack into af. If the low byte of that garbage is 0, ng_iface_bpftap's KASSERT(family != AF_UNSPEC) fires and the kernel panics. INVARIANTS is enabled in the default X86_64_GENERIC kernel, so this is a local- privileged denial-of-service via a single legitimate write() to /dev/bpf on an ng_iface.

Root cause

In ng_iface_output (sys/netgraph7/iface/ng_iface.c:428-431):

428:    if (dst->sa_family == AF_UNSPEC) {
429:        bcopy(dst->sa_data, &af, sizeof(af));   /* af is uint32_t */
430:        dst->sa_family = af;                    /* sa_family_t is __uint8_t! */
431:    }
434:    ng_iface_bpftap(ifp, m, dst->sa_family);

For ng_iface the BPF link-layer type is DLT_NULL (constructor line 625: bpfattach(ifp, DLT_NULL, sizeof(u_int32_t))). For DLT_NULL, bpf_movein (sys/net/bpf.c:189-193) sets sockp->sa_family = AF_UNSPEC and hlen=0, so the if (hlen != 0) block at bpf.c:253-275 that would normally bcopy the link header into sa_data is skipped entirely. The struct sockaddr dst declared in bpfwrite (sys/net/bpf.c:619) is an uninitialized stack local; bpfwrite passes &dst straight to ifp->if_output (sys/net/bpf.c:598). So dst->sa_data is whatever garbage the stack happens to contain.

The bcopy reads 4 bytes of that garbage into af, then dst->sa_family = af truncates to one byte. If the low byte is 0, dst->sa_family becomes AF_UNSPEC, and ng_iface_bpftap (line 481) panics:

481:    KASSERT(family != AF_UNSPEC, ("%s: family=AF_UNSPEC", __func__));

KASSERT is enabled under INVARIANTS (sys/sys/systm.h:94-96), and INVARIANTS is in the default X86_64_GENERIC kernel config (sys/config/X86_64_GENERIC).

The correct semantics (already implemented in newer FreeBSD ng_iface.c) is to read the AF from the leading 4 bytes of the mbuf via m_copydata(m, 0, sizeof(af), &af) and then m_adj(m, sizeof(af)) to strip the DLT_NULL link-layer word β€” neither of which this file does, so even when af is non-zero the packet is forwarded into netgraph with the AF prefix still attached, corrupting the peer's view of the payload.

Threat model & preconditions

  • Attacker position: local user with write access to /dev/bpf* (root on a default DragonFly install; or any principal granted BPF via devfs rules / group network on hardened setups).
  • Privileges gained or impact: local denial of service (kernel panic). The KASSERT fires whenever the stack garbage at &dst.sa_data happens to have a zero low byte β€” roughly 1/256 probability per write on x86_64, so a tight write() loop panics the machine within seconds. No confidentiality or integrity impact.
  • Required config or capabilities: write access to /dev/bpf*; an ng_iface interface attached (common in ng_pppoe / ng_l2tp / ng_nat topologies).
  • Reachability: write() to /dev/bpfN (bound to an ng_iface) β†’ bpfwrite β†’ ifp->if_output β†’ ng_iface_output β†’ reads uninitialized dst->sa_data β†’ ng_iface_bpftap β†’ KASSERT.

Proof of concept

PoC source: findings/poc/DF-0607/df-bpf-panic.c

Build & run

cc -o df-bpf-panic df-bpf-panic.c
# one-time topology:
ngctl mkpeer iface dummy inet    # creates ng0
sudo ./df-bpf-panic

Expected output

Kernel panic on the console and in /var/crash:

panic: assertion "family != AF_UNSPEC" failed in ng_iface_bpftap at sys/netgraph7/iface/ng_iface.c:481

On a non-INVARIANTS kernel the same code still reads 4 bytes of uninitialized stack and (when af != 0) forwards a 4-byte-AF-corrupted mbuf into the inet/inet6 hook.

Impact

  • Blast radius: any DragonFly system exposing /dev/bpf* with an ng_iface interface (VPN concentrators, routers, test setups).
  • Severity rationale: Low. Requires BPF write access (root), panic is probabilistic (~1/256 per write). No info leak or code execution. CVSS 3.1 base β‰ˆ 5.0.
  • Reliability: probabilistic on INVARIANTS kernels (~1/256 per write); deterministic panic with a tight loop within seconds. On non-INVARIANTS kernels, silent payload corruption.

Read the AF from the leading 4 bytes of the mbuf (DLT_NULL convention) instead of from dst->sa_data, and strip those 4 bytes so the downstream hook receives the actual protocol payload. This matches the newer FreeBSD ng_iface.c semantics.

--- a/sys/netgraph7/iface/ng_iface.c
+++ b/sys/netgraph7/iface/ng_iface.c
@@ -425,12 +425,20 @@ ng_iface_output(struct ifnet *ifp, struct mbuf *m,
    uint32_t af;
    int error;

    /* Check interface flags */
    if (!((ifp->if_flags & IFF_UP) && (ifp->if_flags & IFF_RUNNING))) {
        m_freem(m);
        return (ENETDOWN);
    }

-   /* BPF writes need to be handled specially. */
+   /*
+    * BPF writes arrive with dst->sa_family == AF_UNSPEC.  For our
+    * DLT_NULL link type the actual address family is carried in the
+    * leading 4 bytes of the mbuf; bpf_movein() does not populate
+    * dst->sa_data, so we must not read from it.
+    */
    if (dst->sa_family == AF_UNSPEC) {
-       bcopy(dst->sa_data, &af, sizeof(af));
-       dst->sa_family = af;
+       if (m->m_pkthdr.len < sizeof(af)) {
+           m_freem(m);
+           return (EINVAL);
+       }
+       m_copydata(m, 0, sizeof(af), (caddr_t)&af);
+       m_adj(m, sizeof(af));
+       dst->sa_family = (sa_family_t)af;
    }

    /* Berkeley packet filter */

Independently, the KASSERT(family != AF_UNSPEC, ...) at line 481 documents an internal invariant that the public ng_iface_output path can violate; even with the fix above, replacing that KASSERT with a graceful if (family == AF_UNSPEC) return; would make the helper robust against future callers that fail to rewrite AF_UNSPEC.

References

  • sys/net/bpf.c:189-193, 253-275, 598, 619 β€” bpf_movein sets sa_family=AF_UNSPEC and hlen=0 for DLT_NULL; bpfwrite passes uninitialized &dst to if_output.
  • sys/netgraph7/iface/ng_iface.c:625 β€” bpfattach(ifp, DLT_NULL, ...).
  • sys/config/X86_64_GENERIC:56 β€” default-config INVARIANTS.

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0607 Β· 10 files
FileTypeDescriptionSize
df-bpf-panic.c trigger-source minimal BPF write PoC (authored β€” was missing from original PoC folder) 4.1 KB view raw
VERDICT.md verdict full analysis: false positive β€” wrong source tree cited 4.3 KB ↓ raw
fix.diff suggested-fix fix for netgraph7 dead code (netgraph version already fixed) 653 B view raw
build.sh build-script build script 475 B view raw
run.sh run-script run script (documents why it cannot reproduce) 991 B view raw
env.txt environment guest environment and module verification 1.2 KB view raw
README.md readme original PoC README 1.8 KB ↓ raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 954 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC README
↓ download raw

DF-0607 β€” PoC: BPF write to ng_iface reads uninitialized sa_data -> KASSERT panic

Privileged local DoS PoC. ng_iface_output reads from dst->sa_data when dst->sa_family == AF_UNSPEC, but for DLT_NULL BPF writes bpf_movein never initializes sa_data. The KASSERT(family != AF_UNSPEC) at ng_iface_bpftap:481 fires and panics on INVARIANTS kernels (default).

Files

  • df-bpf-panic.c β€” minimal reproducer (tight write() loop on /dev/bpfN bound to an ng_iface).
  • (added by per-PoC verifier) build.sh, run.sh, run.log, VERDICT.md, manifest.json, fix.diff, panic.txt.

Build & run

cc -o df-bpf-panic df-bpf-panic.c
# one-time topology:
sudo ngctl mkpeer iface dummy inet    # creates ng0
sudo ./df-bpf-panic

Expected first outcome

Kernel panic on the console and in /var/crash:

panic: assertion "family != AF_UNSPEC" failed in ng_iface_bpftap at sys/netgraph7/iface/ng_iface.c:481

The KASSERT fires when the stack garbage at &dst.sa_data has a zero low byte β€” roughly 1/256 probability per write, so a tight loop panics within seconds.

Notes for the per-PoC verifier

  • Requires /dev/bpf* write access (root on default installs; or any principal granted BPF via devfs rules).
  • Requires an ng_iface interface attached (ng0 from ngctl mkpeer iface dummy inet, or an existing ng_pppoe / ng_l2tp topology).
  • Verify the fix with git apply findings/poc/DF-0607/fix.diff (read AF from leading 4 bytes of mbuf instead of dst->sa_data, strip 4 bytes); after the fix the panic should no longer occur and the peer hook receives the un-corrupted payload.
  • On non-INVARIANTS kernels the KASSERT is a no-op; the bug instead manifests as silent payload corruption (4-byte AF prefix still attached to the mbuf when forwarded into netgraph).
VERDICT.md verdict full analysis: false positive β€” wrong source tree cited
↓ download raw

DF-0607 β€” Verdict: NOT REPRODUCED (false positive β€” wrong source tree cited)

Summary

The finding cites sys/netgraph7/iface/ng_iface.c:428-431, 481 as containing a bug where ng_iface_output reads uninitialized dst->sa_data for BPF writes on a DLT_NULL interface. While this bug does exist in the cited source file, that file is dead code β€” it is never compiled into any kernel module on this system. The actual loaded ng_iface.ko module is built from sys/netgraph/iface/ng_iface.c (the legacy netgraph tree), which already contains the correct implementation (reads the AF from the mbuf, not from dst->sa_data).

Verification

1. Which source tree is the loaded module from?

$ strings /boot/kernel/ng_iface.ko | grep ng_iface.c
/usr/src/sys/netgraph/iface/ng_iface.c     <-- legacy netgraph, NOT netgraph7

The loaded ng_iface.ko is from sys/netgraph/iface/ng_iface.c. There is no netgraph7 module in /boot/kernel/:

$ ls /boot/kernel/ | grep ng7    # (empty β€” no netgraph7 modules)

netgraph7 is not compiled into the kernel either (no NETGRAPH7 in sys/config/X86_64_GENERIC, no netgraph7 strings in the kernel binary).

2. The cited code (netgraph7) β€” HAS the bug

sys/netgraph7/iface/ng_iface.c:428-431:

if (dst->sa_family == AF_UNSPEC) {
    bcopy(dst->sa_data, &af, sizeof(af));   /* reads uninitialized sa_data! */
    dst->sa_family = af;                    /* truncates uint32_t to uint8_t */
}

And sys/netgraph7/iface/ng_iface.c:481:

KASSERT(family != AF_UNSPEC, ("%s: family=AF_UNSPEC", __func__));

This is a real bug: for DLT_NULL BPF writes, bpf_movein (sys/net/bpf.c:190-193) sets sa_family=AF_UNSPEC and hlen=0, so sa_data is never initialized (the if (hlen != 0) block at bpf.c:253 is skipped). The bcopy reads 4 bytes of stack garbage. If the low byte is 0, sa_family stays AF_UNSPEC and the KASSERT panics.

3. The actual loaded code (netgraph) β€” ALREADY FIXED

sys/netgraph/iface/ng_iface.c:423-430:

if (dst->sa_family == AF_UNSPEC) {
    if (m->m_len < 4 && (m = m_pullup(m, 4)) == NULL)
        return (ENOBUFS);
    dst->sa_family = (sa_family_t)*mtod(m, int32_t *);  /* reads AF from mbuf! */
    m->m_data += 4;                                       /* strips 4-byte AF prefix */
    m->m_len -= 4;
    m->m_pkthdr.len -= 4;
}

This is exactly the fix the finding recommends: read the AF from the leading 4 bytes of the mbuf (DLT_NULL convention) instead of from dst->sa_data, and strip those 4 bytes. The legacy netgraph version already implements this correctly.

4. Cannot create ng_iface for live testing (separate bug)

Even if we wanted to test the legacy module's code path, creating an ng_iface node panics with a separate constructor bug:

panic: try holding ifnet lock in netisr   (ng_eiface, same pattern)
panic: trying to free NULL pointer         (ng_iface, same root cause)

ng_mkpeer processes the creation message in a netisr context, and if_attach called from the constructor panics because ifnet_lock() cannot be held from netisr. This is an unrelated infrastructure bug that prevents creating any netgraph interface nodes on this guest.

Conclusion

NOT REPRODUCED. The finding audited the wrong source tree. The bug exists in sys/netgraph7/iface/ng_iface.c (dead code, never compiled), but the loaded ng_iface.ko module is from sys/netgraph/iface/ng_iface.c which already has the correct implementation. No runtime impact.

The finding's "Recommended fix" (read AF from mbuf) is already implemented in the running code. The netgraph7 version should be fixed if it is ever built, but it is currently dead code.

Impact

None β€” the vulnerable code is not compiled or loaded. The running code is correct.

PoC changes

Authored df-bpf-panic.c (was missing β€” only README.md existed in the PoC folder). The PoC cannot reproduce because: 1. The cited code (netgraph7) is dead code (not loaded). 2. The loaded code (netgraph) already has the fix. 3. A separate constructor bug prevents creating ng_iface nodes.

Fix

A fix.diff is provided for sys/netgraph7/iface/ng_iface.c (the cited file) to fix the dead code, matching the correct implementation already present in sys/netgraph/iface/ng_iface.c. No fix is needed for the running system.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: cited code is dead (netgraph7 never compiled). Loaded module already has the fix. No fix needed for running system.

fix.diff applies RC=0, compiles RC=0. Cannot runtime-test (dead code).
↓ fix.diffn/a -- dead code; loaded module already correct. fix.diff applies+compiles but fixes code that is never loaded.

Confirmed kernel references

Detail

Exploit chain

none -- not a bug in the running system. Dead code has the bug; loaded code already has the fix.

Evidence (decisive lines)

strings /boot/kernel/ng_iface.ko -> '/usr/src/sys/netgraph/iface/ng_iface.c' (NOT netgraph7). netgraph7 source has bcopy(dst->sa_data,...); netgraph source has dst->sa_family=*mtod(m,int32_t*). 0 netgraph7 modules loaded.

PoC changes

Authored df-bpf-panic.c (was missing). Wrote VERDICT.md, fix.diff (fixes dead netgraph7 code), build.sh, run.sh, manifest.json.

Verified recommended fix

fix.diff reads AF from leading 4 bytes of mbuf (DLT_NULL convention) via m_copydata/m_adj. Matches already-correct sys/netgraph/iface/ng_iface.c:423-430. Fixes dead code only. Full git-apply-able diff in findings/poc/DF-0607/fix.diff.

Verdict

NOT REPRODUCED -- false positive: wrong source tree cited. The finding cites sys/netgraph7/iface/ng_iface.c:428-431 (dead code, never compiled). The loaded ng_iface.ko is from sys/netgraph/iface/ng_iface.c which ALREADY has the correct implementation (reads AF from mbuf via mtod, not from uninitialized sa_data). 0 netgraph7 modules in /boot/kernel/. Additionally /dev/bpf* is root-only.