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 / groupnetworkon hardened setups). - Privileges gained or impact: local denial of service (kernel panic).
The KASSERT fires whenever the stack garbage at
&dst.sa_datahappens to have a zero low byte β roughly 1/256 probability per write on x86_64, so a tightwrite()loop panics the machine within seconds. No confidentiality or integrity impact. - Required config or capabilities: write access to
/dev/bpf*; anng_ifaceinterface attached (common inng_pppoe/ng_l2tp/ng_nattopologies). - Reachability:
write()to/dev/bpfN(bound to anng_iface) βbpfwriteβifp->if_outputβng_iface_outputβ reads uninitializeddst->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 anng_ifaceinterface (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.
Recommended fix
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_moveinsetssa_family=AF_UNSPECandhlen=0forDLT_NULL;bpfwritepasses uninitialized&dsttoif_output.sys/netgraph7/iface/ng_iface.c:625βbpfattach(ifp, DLT_NULL, ...).sys/config/X86_64_GENERIC:56β default-configINVARIANTS.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0607 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 (tightwrite()loop on/dev/bpfNbound to anng_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_ifaceinterface attached (ng0fromngctl mkpeer iface dummy inet, or an existingng_pppoe/ng_l2tptopology). - Verify the fix with
git apply findings/poc/DF-0607/fix.diff(read AF from leading 4 bytes of mbuf instead ofdst->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).
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_testablenot_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).
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.
No comments yet.