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)
PoC verification
Evidence pack
findings/poc/DF-3461 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
in6_cksum.c:116:m->m_pkthdr.len < off + lenβoffandlenareu_int32_t;off + lenwraps mod 2^32, the comparison uses wrapped arithmetic, the "sanity" panic does not fire.- The offset walk (
:150-156) exhausts the chain withoffstill huge,m == NULL. :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 β 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
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).
No comments yet.