altq_etherclassify NULL-pointer dereference: mbuf chain walk advances to m_next without NULL check
Summary
altq_etherclassify(:927-930): while(m->m_len<=hlen){hlen-=m->m_len; m=m->m_next;} advances to m_next without checking m!=NULL. If chain ends before hlen consumed (m_next==NULL for last mbuf + preceding m_len exactly equals remaining hlen) -> next iteration evaluates m->m_len on NULL -> panic. Comment(:932-936) "ip header not in single mbuf... todo use m_pulldown" concedes incomplete. Trigger: ALTQ enabled egress + packet first mbuf m_len==14 or 22 with no m_next. Kernel panic local/transitive DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0445 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| altq_etherclassify_npd.c | trigger-source | ALTQ + 14-byte BPF write -> NPD in altq_etherclassify | 6.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o altq_etherclassify_npd altq_etherclassify_npd.c | 131 B | view raw |
| run.sh | run-script | kldload pf.ko; ./altq_etherclassify_npd | 680 B | view raw |
| VERDICT.md | verdict | NPD panic reproduction + fix validation | 3.7 KB | β raw |
| fix.diff | suggested-fix | NULL check in the while-loop; goto bad on too-short chain | 755 B | view raw |
| panic.txt | panic-signature | Fatal trap 12 page fault at altq_etherclassify+0x98 movl 0x18(%rbx),%eax | 668 B | view raw |
| env.txt | environment | uname, pf.ko + bpf available | 934 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-0445 β altq_etherclassify() NULL-pointer dereference
Verdict: REPRODUCED (panic / privileged local DoS).
Mechanism
sys/net/if_ethersubr.c:927-930:
while (m->m_len <= hlen) {
hlen -= m->m_len;
m = m->m_next; // <-- no NULL check
}
The loop advances m = m->m_next without verifying the new m is
non-NULL. If the mbuf chain terminates at exactly hlen bytes (the last
mbuf has m_len <= remaining hlen and m_next == NULL), the next loop
iteration evaluates m->m_len on NULL -> page fault.
For the IP path (no LLC/SNAP), hlen = sizeof(struct ether_header) = 14.
A single-mbuf packet with m_len == 14 and m_next == NULL triggers it.
PoC trigger path
- enable ALTQ on vtnet0 via DIOCXBEGIN(ALTQ) + DIOCADDALTQ(CBQ root) + DIOCXCOMMIT + DIOCSTARTALTQ. This sets ALTQF_ENABLED on ifp->if_snd.
- Open /dev/bpf, BIOCSETIF to vtnet0, BIOCSHDRCMPLT=1.
- Write 14 bytes -- a raw ether_header with type=ETHERTYPE_IP and no payload.
What the kernel does with those 14 bytes: bpfwrite -> bpf_movein (m_len=14, m_next=NULL) -> ifp->if_output = ether_output ether_output: M_PREPEND 14 bytes (m_len becomes 14, the new ether_header; the user-supplied ether_header is in dst->sa_data and gets copied into the prepended slot; payload stays empty) -> ether_output_frame ether_output_frame: ifq_is_enabled() == true -> altq_etherclassify altq_etherclassify: hlen=14, m->m_len=14 while (14 <= 14) { hlen=0; m=m->m_next=NULL; } while (NULL->m_len ...) <-- NPD, page fault
Trigger requires root: enabling ALTQ + BPF writes need root.
Reproduction (unpatched baseline #0)
# kldload pf.ko
# ./altq_etherclassify_npd
ALTQ enabled on vtnet0
BPF attached to vtnet0 via /dev/bpf0
*** WRITING 14-byte ether_header-only frame -- if altq_etherclassify is
reached, EXPECT NPD PANIC ***
Panic signature (dfbsd-qemu/boot.log, see panic.txt):
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x18 instruction pointer = 0x8:0xffffffff80738478 kernel: type 12 trap, code=0 Stopped at altq_etherclassify+0x98: movl 0x18(%rbx),%eax
fault VA = 0x18 is NULL + offsetof(struct mbuf, m_len). movl
0x18(%rbx),%eax is the compiler's load of m->m_len on m == NULL.
Fix validation
Applied fix.diff (add m != NULL to the loop condition; jump to bad
if NULL after the loop), rebuilt the kernel, rebooted into
6.5-DEVELOPMENT #1 (2026-07-18):
# ./altq_etherclassify_npd ALTQ enabled on vtnet0 BPF attached to vtnet0 via /dev/bpf0 *** WRITING 14-byte ether_header-only frame *** bpf write returned 14 bytes (no panic) RUN_EXIT=0
The previously-panicking BPF write now succeeds (altq_etherclassify bails
to bad instead of crashing). No panic signature in boot.log on the
patched kernel -- the fix is VALIDATED. (After the test, vtnet0 may
stop responding because the ALTQ root class has no default child to
enqueue into; that is an unrelated test artifact and not a kernel crash.)
Notes
The comment at if_ethersubr.c:932-936 ("ip header is not in a single mbuf. this should not happen in the current code") acknowledges the kernel normally guarantees the ether_header + IP header are in the first mbuf. The bug is therefore latent for normal traffic, but live-exploitable for any transmit path that produces a single-mbuf frame with m_len == hlen (e.g. BPF write of a header-only frame on an ALTQ-enabled interface, or certain L2 paths through bridge/lagg).
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). altq_etherclassify m=m_next no NULL check -> page fault on NULL. Root-only BPF + ALTQ.
No comments yet.