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

icmp6_redirect_input dereferences stale ip6/nd_rd pointers after IP6_EXTHDR_CHECK may reallocate the mbuf (latent UAF read)

Field Value
ID DF-2610
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-825 Expired Pointer Dereference
File sys/netinet6/icmp6.c
Lines 2160-2211
Area net
Confidence speculative
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

icmp6_redirect_input captures ip6 = mtod(m, ...) at line 2160, then IP6_EXTHDR_CHECK at 2187 may replace m via m_pullup (M_LOOP branch, ip6.h:277-306), whose slow path allocates a new mbuf and frees the old chain (uipc_mbuf.c:2122-2153). nd_rd (2188), the 32-byte read of nd_rd_target/nd_rd_dst (2196-2197) and ip6->ip6_hlim (2211) then read freed memory. This violates the invariant the file itself documents at icmp6.c:412 ("m might change if M_LOOP. So, call mtod after this") and that every sibling function honors.

Root cause

icmp6.c:2160 ip6 = mtod(m, struct ip6_hdr *); precedes icmp6.c:2187 IP6_EXTHDR_CHECK(m, off, icmp6len,). In the non-PULLDOWN build the macro's first branch (ip6.h:280-284) reassigns m = m_pullup(m, off+icmp6len) for M_LOOP packets whose first mbuf is short; m_pullup's slow path (first mbuf M_EXT or lacking tail room, uipc_mbuf.c:2122-2134) moves the pkthdr to a fresh mbuf, copies the data, frees each exhausted old mbuf (uipc_mbuf.c:2146 n = m_free(n)), and returns the new chain. All uses of the pre-check ip6 afterwards β€” nd_rd at 2188, redtgt6/reddst6 at 2196-2197, ip6->ip6_hlim at 2211 β€” are dangling. Contrast icmp6_input (mtod at 420 after the check at 411, with the explicit comment), icmp6_notify_error (re-derives at 861 and again at 978), and icmp6_error (re-derives oip6 at 340).

Reachability today is conjunctive: the copy delivered by icmp6.c:787-793 (m_copym) never carries M_LOOP (M_COPYFLAGS, mbuf.h:281-284, excludes it), so only the m_copym-failure fallback at 789 passes an original loopback-delivered (M_LOOP, if_loop.c:274) redirect; the freeing pullup additionally requires an M_EXT short first mbuf plus off+icmp6len <= MHLEN β€” a chain shape current producers do not generate. Defect certain at the code level; exploitation speculative β€” a live invariant violation waiting on any future mbuf-producer change.

Threat model & preconditions

  • Attacker position: local privileged user (raw ICMPv6 socket needs SYSCAP_NONET_RAW, raw_ip6.c:530-533) or traffic routed over lo0, plus mbuf-cluster pressure to force the m_copym NOWAIT failure.
  • Privileges gained or impact: 32+ bytes read from freed mbuf memory flow into redirect validation and potentially rtredirect/nd6_cache_lladdr (route-table poisoning with heap garbage) or a panic.
  • Required config or capabilities: raw-socket capability or loopback routing; mbuf pressure.
  • Reachability: ND_REDIRECT over lo0 under memory pressure today; remotely reachable if any future driver/LRO change produces (M_LOOP, M_EXT-short-first, pullup<=MHLEN) chains.

Proof of concept

Not presently reproducible end-to-end (needs the mbuf shape described); verification of the defect is the code-level trace above plus an instrumented guest run.

Build & run

boot DF guest; enable IPv6 on lo0; as root use a raw ICMPv6 socket to send
to ::1 an ND_REDIRECT (type 139, code 0, hlim 255, >=40-byte ICMPv6 payload,
chain split as [short cluster first mbuf][rest]) while spraying mbufs to
force the m_copym at icmp6.c:787 to fail.

Expected output

kernel reads nd_rd from freed cluster (garbage redtgt6/reddst6 in the
'ICMP6 redirect' log lines at icmp6.c:2234/2246/2264) or panics in
rtpurelookup on a garbage sockaddr.

Impact

Latent UAF-read on redirect input; today root-gated and shape-gated, but a one-line fix removes the class entirely before any producer change makes it remotely reachable.

Re-derive the header pointer after the check, exactly as every sibling in this file does:

--- a/sys/netinet6/icmp6.c
+++ b/sys/netinet6/icmp6.c
@@ -2184,7 +2184,11 @@ icmp6_redirect_input(struct mbuf *m, int off)
 #ifndef PULLDOWN_TEST
    IP6_EXTHDR_CHECK(m, off, icmp6len,);
+   /*
+    * m may have been reallocated by m_pullup() in the M_LOOP case
+    * (see the comment in icmp6_input()); re-derive ip6.
+    */
+   ip6 = mtod(m, struct ip6_hdr *);
    nd_rd = (struct nd_redirect *)((caddr_t)ip6 + off);
 #else
    IP6_EXTHDR_GET(nd_rd, struct nd_redirect *, m, off, icmp6len);

References

  • icmp6.c:412 (documented invariant), ip6.h:277-306 (IP6_EXTHDR_CHECK)
  • uipc_mbuf.c:2122-2153 (m_pullup slow path frees old chain)

Timeline

  • 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2610 Β· 11 files
FileTypeDescriptionSize
redir6.c β€” 4.7 KB view raw
ic6stat.c β€” 2.1 KB view raw
raw137listen.c β€” 2.2 KB view raw
build.sh β€” 169 B view raw
run.sh β€” 1.5 KB view raw
run.log β€” 7.0 KB view raw
build.log β€” 75 B view raw
env.txt β€” 429 B view raw
fix.diff β€” 557 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
verdict.json β€” 4.9 KB view raw
VERDICT.md
↓ download raw

DF-2610 β€” icmp6_redirect_input stale ip6/nd_rd (latent UAF-read)

What this investigates

icmp6_redirect_input captures ip6 = mtod(m, ...) at icmp6.c:2160 BEFORE IP6_EXTHDR_CHECK at icmp6.c:2187. In the non-PULLDOWN build (no PULLDOWN_TEST anywhere in sys/ β€” verified) that macro can replace m via m_pullup when the mbuf is M_LOOP and short (netinet/ip6.h:277-306), and m_pullup's slow path frees the old first mbuf (uipc_mbuf.c:2125-2153). Every later use β€” nd_rd (2188), redtgt6/reddst6 (2196-2197), ip6->ip6_hlim (2211) β€” would then read freed memory. This violates the file's own documented invariant (icmp6.c:412: "m might change if M_LOOP. So, call mtod after this").

Verdict

Code-level defect CONFIRMED in the current tree (identical guest /usr/src, md5-matched). Live manifestation: NOT REPRODUCIBLE from userspace on this kernel β€” the reallocating m_pullup branch is provably unreachable for any packet a userspace process can cause to be delivered to icmp6_redirect_input. The finding's own assessment ("latent class violation", confidence: speculative) is accurate.

Live evidence (run.log, full untrimmed)

  • Path traversal proven: raw-socket ND_REDIRECT (type 137) sends to ::1 with src forced to fe80::1 and hlim 255 traverse the entire validation block β€” dmesg shows ICMP6 redirect rejected; not equal to gw-for-src=::0001 (must be same): (src=fe80:0002::0001 dst=::0001 tgt=fe80:0002::) which is the log at icmp6.c:2241-2246, i.e. execution passed the nd_rd reads (2196-2197), the hlim check (2211) and rtpurelookup (2227). icp6s_inhist[137] counted every send (1 -> 71 over the run). raw137listen confirms the kernel-chosen source/hlim on the wire (from fe80::1 scope=2 ... hlim=255).
  • No corruption observed: sends at payload sizes 40..8000 (single mbufs AND [40B header mbuf][cluster] chains β€” lo0= 23 multi-mbuf inputs in ip6 stats), multi-iov sendmsg shapes, and a 50-packet burst all left the guest alive, no INVARIANTS trip, no panic (uptime after).

Why the stale pointer cannot fire today (trace)

  1. The dispatch copy n = m_copym(m, 0, M_COPYALL, M_NOWAIT) (icmp6.c:787) never carries M_LOOP: M_COPYFLAGS (sys/sys/mbuf.h:281-284) excludes it. With !M_LOOP the macro takes the M_EXT/plain else-branches (ip6.h:285-297) which either pass (data contiguous) or free-and-return β€” never m_pullup.
  2. The only M_LOOP packet that can reach icmp6_redirect_input is the ORIGINAL loopback mbuf, delivered solely when that m_copym fails (icmp6.c:789-791) β€” requiring cluster-zone exhaustion.
  3. For the pullup to reallocate (free the first mbuf ip6 points into), the first mbuf must be M_EXT (or lack room) with off+icmp6len <= MHLEN (uipc_mbuf.c:2125-2135).
  4. No userspace producer creates such a chain: sosend sizes the first mbuf to the full send via m_getl(resid, ...) (uipc_socket.c:866-887) β€” first mbuf is either a full/small internal mbuf or a FULL cluster; rip6_output's M_PREPEND (raw_ip6.c:326) then splits off a new internal header mbuf (cluster has no headroom). So chains are always [internal hdr][...], taking the in-place pullup path (uipc_mbuf.c:2119-2124) that leaves the first mbuf (and ip6) valid. Want > MCLBYTES (e.g. 2100/3000/8000-byte sends) makes m_pullup return NULL -> macro frees and returns before line 2188. bpf/tun injection produce single mbufs.
  5. Therefore the dangling dereference requires a future mbuf-producer change (e.g. an LRO/driver change emitting M_EXT-short-first chains over loopback, plus mbuf pressure for step 2). A one-line fix removes the class.

Fix

fix.diff re-derives ip6 = mtod(m, ...) after the check, exactly as icmp6_input (420-421), icmp6_notify_error (861/978) and icmp6_error (340) already do. Kernel-rebuild validation not performed: the bug did not reproduce live (nothing to observe a behavioral delta against); the change is mechanical and mirrors the sibling functions' proven pattern.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix kernel not built: the defect never manifests at runtime on this kernel (unreachable mbuf shape), so there is no observable behavioral delta to validate a patched kernel against. fix.diff is a mechanical pointer re-derivation identical in shape to the already-correct sibling functions (icmp6.c:420, 861, 978, 340); compile-correctness follows the same pattern.

VERDICT.md reachability trace (uipc_socket.c:866 m_getl, raw_ip6.c:326 M_PREPEND, uipc_mbuf.c:2119-2135 in-place-vs-realloc conditions, mbuf.h:281-284 M_COPYFLAGS) plus run.log survival of all chain shapes.
↓ fix.diffper-fix-DF-2610

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: dmesg line 'ICMP6 redirect rejected; not equal to gw-for-src=::0001 (must be same): (src=fe80:0002::0001 dst=::0001 tgt=fe80:0002::)' (log at icmp6.c:2241-2246, i.e. execution passed the would-be-stale reads at 2196-2197); ic6stat inhist[137] 1->71 across all sends; 'ALIVE_AFTER_SENDS' + uptime after chain/burst shapes; raw137listen confirming wire src=fe80::1 scope=2 hlim=255.

PoC changes

Standalone toolkit written from scratch (redir6 sender, ic6stat binary-stats decoder, raw137listen observer). Two material fixes found during verification: (1) sender originally used ICMPv6 type 139 (NI query) instead of 137 (ND_REDIRECT) - corrected after ic6stat inhist and the nodeinfo responder exposed the mix-up; (2) nd_rd_dst pointed at an unroutable address so rtpurelookup returned NULL silently - changed to the queried destination (local host route) to make the gateway-validation log fire as the traversal oracle.

Verified recommended fix

Re-derive ip6 = mtod(m, struct ip6_hdr *) immediately after IP6_EXTHDR_CHECK in icmp6_redirect_input, mirroring icmp6_input/icmp6_notify_error/icmp6_error (see fix.diff).

Verdict

Source-level defect CONFIRMED verbatim in the current tree (guest /usr/src md5-identical to audit tree): icmp6.c:2160 takes ip6=mtod(m) BEFORE IP6_EXTHDR_CHECK at 2187, and the non-PULLDOWN branch compiles (no PULLDOWN_TEST anywhere in sys/), so nd_rd (2188), redtgt6/reddst6 (2196-2197) and ip6->ip6_hlim (2211) would dangle if the check's m_pullup reallocated the mbuf - violating the file's own invariant at icmp6.c:412. LIVE MANIFESTATION NOT REPRODUCIBLE from userspace on this kernel: (1) the dispatch copy at icmp6.c:787 strips M_LOOP (M_COPYFLAGS, mbuf.h:281-284), so the copy always takes the macro's non-pullup branches (ip6.h:285-297); (2) the M_LOOP original reaches icmp6_redirect_input only if that m_copym fails (cluster exhaustion); (3) the reallocating m_pullup path (uipc_mbuf.c:2125-2135) requires an M_EXT-or-no-room FIRST mbuf with off+icmp6len<=MHLEN, but sosend always sizes the first mbuf to the full send via m_getl (uipc_socket.c:866-887) and rip6_output's M_PREPEND (raw_ip6.c:326) splits off an internal header mbuf - so loopback chains are always [internal-hdr][cluster...] and take the in-place pullup (uipc_mbuf.c:2119-2124) that keeps ip6 valid; want>MCLBYTES makes m_pullup NULL and the macro frees+returns before line 2188. Live evidence: 70+ type-137 redirects of every shape (40..8000 bytes, multi-iov, bursts) traversed the full validation block (dmesg rejection log from icmp6.c:2241-2246; icp6s_inhist[137] 1->71) with zero panics/corruption. Defect is a latent class violation as filed (confidence: speculative); the one-line re-derive fix is warranted.