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

in6_cksum() sanity check bypassable via u32 wrap of off+len β€” intended panic becomes uncontrolled NULL-mbuf deref (defense-in-depth)

Summary

The 'sanity check' at in6_cksum.c:116 compares m_pkthdr.len < off + len where off and len are u_int32_t: the sum wraps mod 2^32, the intended panic is skipped, and the offset walk then exhausts the chain leaving m==NULL so w = mtod(m,...)+off and mlen = m->m_len - off read through the NULL mbuf - a low-page fault instead of the diagnostic panic. A wrapping len with small off instead hits panic('out of data') at :285-286, also bypassing the first check. NONE today: pass-2 caller matrix proves every in-tree caller derives off/len from the u16 ip6_plen after the ip6_input drop/clamp and pulls up L4 headers first, so max len ~64KiB - a wrap needs off+len >= 2^32. Hardening for future callers (jumbogram path or a ported in6_delayed_cksum). Arithmetic mirror proven on guest (wrap_demo: off=0xFFFFFFFF len=41 -> check PASSES, walk ends m=0x0 -> NULL deref; KLD deliberately not run for an Info finding). Fix: overflow-safe re-statement of the invariant.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3461 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
wrap_demo.c β€” 1.8 KB view raw
build.sh β€” 193 B view raw
build.log β€” 273 B view raw
run.log β€” 222 B view raw
env.txt β€” 255 B view raw
fix.diff β€” 470 B view raw
manifest.json β€” 909 B view raw
verdict.json β€” 2.5 KB view raw

DF-3461 β€” in6_cksum() sanity check bypassable via u32 wrap of off + len

Severity: Info (defense-in-depth; no in-tree caller can reach it) File: sys/netinet6/in6_cksum.c:116-158

Build / run (guest, DragonFly 6.5-DEVELOPMENT, stock kernel #0)

ssh dfbsd 'cat > /tmp/wrap_demo.c' < wrap_demo.c
vm.sh run_root 'cd /tmp && cc -O2 -Wall -o wrap_demo wrap_demo.c && ./wrap_demo'

Expected output

off=0xffffffff len=41 -> off+len=0x00000028 (40) wrapped=1
m->m_pkthdr.len=100: PASSES β€” sanity check BYPASSED
after walk: m=0x0 off=0xffffff9b -> w = mtod(m)+off = read (struct mbuf*)NULL->m_data -> kernel panic

This is a userland arithmetic mirror of the kernel code path, not a kernel reproduction: the finding is Info/unreachable, so no kernel PoC was run (a KLD that called in6_cksum(m, nxt, 0xFFFFFFFF, 41) would panic the guest β€” proving only what the mirror already shows).

What it demonstrates

  1. in6_cksum.c:116: m->m_pkthdr.len < off + len β€” off and len are u_int32_t; off + len wraps mod 2^32, the comparison uses wrapped arithmetic, the "sanity" panic does not fire.
  2. The offset walk (:150-156) exhausts the chain with off still huge, m == NULL.
  3. :157-158: w = mtod(m, ...) + off; mlen = m->m_len - off; dereference the NULL mbuf β†’ low-page fault β†’ panic (not a controlled panic message).

Why Info

Every in-tree caller derives off/len from bounded quantities (u16 ip6_plen after the ip6_input.c:628-638 clamp/trim, plus each transport's IP6_EXTHDR_CHECK pull-up) β€” none can produce a wrapping sum. See VERDICT.md for the full caller matrix.

VERDICT.md
↓ download raw

VERDICT β€” DF-3461

Status: untested at kernel level (by design β€” Info/unreachable finding); arithmetic mirror reproduced on guest toolchain. Impact: none (hardening). Confidence: certain (code behavior); reachability from any in-tree caller: none.

Root cause

sys/netinet6/in6_cksum.c:116:

if (m->m_pkthdr.len < off + len) {
        panic("in6_cksum: mbuf len (%d) < off+len (%d+%d)", ...);
}

off and len are u_int32_t (in6_cksum.c:91). off + len is computed in unsigned 32-bit; values near 2^32 wrap to small numbers, the comparison then passes, and the intended "should never happen" panic is skipped. The mbuf walk (:150-156) then consumes the whole chain while off remains huge, exits with m == NULL, and :157-158

w = (u_int16_t *)(mtod(m, u_char *) + off);
mlen = m->m_len - off;

read m->m_data through the NULL pointer β†’ low-page fault panic (uncontrolled, not the intended message). A wrapping len alone (with small off) instead drains the chain in the final loop and hits the panic("out of data") at :285-286 β€” also bypassing the intended first panic, same root cause.

Reachability analysis (why Info)

Caller matrix (every in6_cksum() call site), pass-2 verified:

Caller off/len derivation bounded?
icmp6.c:404,440 icmp6len = m->m_pkthdr.len - *offp, icmp6len >= 4 checked yes
icmp6.c:2093,2612 self-built packets, plen == m_pkthdr.len - 40 yes
udp6_usrreq.c:146-169 plen = ntohs(ip6_plen) - off + 40, ulen == plen required; IP6_EXTHDR_CHECK(m, off, 8) first yes
tcp_input.c:655,695 (via tcp6_input tcp_input.c:629-634 which pulls up β‰₯ sizeof(struct tcphdr)) tlen = 40 + plen - off0; ip6_input clamped pkthdr.len == 40 + plen yes
raw_ip6.c:151 len = m->m_pkthdr.len - *offp yes
raw_ip6.c:418, udp6_output.c:249, tcp_output.c:1072, tcp_subr.c:676, tcp_syncache.c:1308, mld6.c:468, nd6_nbr.c:580,1043 locally-built mbufs, consistent by construction yes
ip6_mroute.c:1705 (PIM, mrouter only) cksumlen ∈ {8, pimlen} after IP6_EXTHDR_CHECK(m, off, minlen); pimlen ≀ clamped payload yes

The clamp that anchors everything: ip6_input.c:628-638 β€” packets shorter than 40 + ip6_plen are dropped; longer ones are trimmed to exactly 40 + ip6_plen. Largest possible len is therefore ~64 KiB β€” a wrap needs off + len β‰₯ 2^32, impossible from any caller.

The mirror wrap_demo.c (built with the guest toolchain, run.log) shows the bypass arithmetic exactly; a KLD calling in6_cksum(m, nxt, 0xFFFFFFFF, 41) would convert this into a guest panic, but that only demonstrates what the mirror already proves and would dirty an otherwise clean guest for an Info finding β€” deliberately not done.

Fix

Overflow-safe re-statement of the same invariant (see fix.diff):

if (len > (u_int32_t)m->m_pkthdr.len ||
    off > (u_int32_t)m->m_pkthdr.len - len)
        panic(...);

fix.diff authored against the read-only sys/ tree; not applied, not kernel- built (Info finding β€” fix validation not applicable; fix_status: not_testable).

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.

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (guest-built mirror: 'sanity check BYPASSED', 'after walk: m=0x0'); VERDICT.md caller-matrix table; fix.diff

PoC changes

n/a β€” finding authored in this run; mirror written fresh

Verified recommended fix

Replace m->m_pkthdr.len < off + len at in6_cksum.c:116 with the overflow-safe len > (u_int32_t)m->m_pkthdr.len || off > (u_int32_t)m->m_pkthdr.len - len (see fix.diff)

Verdict

Info hardening: the in6_cksum() sanity check at sys/netinet6/in6_cksum.c:116 computes off+len in unsigned 32-bit; a wrapping pair bypasses the intended panic and the mbuf walk then ends with m==NULL so lines 157-158 dereference the NULL mbuf (uncontrolled low-page fault instead of the diagnostic panic). Pass-2 caller matrix proves no in-tree caller can supply wrapping off/len (ip6_input.c:628-638 clamps pkthdr.len to 40+ip6_plen; icmp6/udp6/tcp6/raw_ip6 all derive off/len from the clamped chain and pull up L4 headers first), so this is unreachable defense-in-depth. The userland arithmetic mirror (wrap_demo.c, run.log) demonstrates the bypass + NULL-walk exactly; no kernel PoC run for an Info finding. Overflow-safe re-statement of the invariant authored as fix.diff (applies cleanly, not built).