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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0445 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict NPD panic reproduction + fix validation
↓ download 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

  1. enable ALTQ on vtnet0 via DIOCXBEGIN(ALTQ) + DIOCADDALTQ(CBQ root) + DIOCXCOMMIT + DIOCSTARTALTQ. This sets ALTQF_ENABLED on ifp->if_snd.
  2. Open /dev/bpf, BIOCSETIF to vtnet0, BIOCSHDRCMPLT=1.
  3. 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

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 12:48:52 UTC 2026

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.