icmp6_redirect_output leaves target-link-layer-address option padding uninitialized (second leak site beyond DF-0329)
| Field | Value |
|---|---|
| ID | DF-2611 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-908 Use of Uninitialized Memory |
| File | sys/netinet6/icmp6.c |
| Lines | 2496-2516 |
| Area | net |
| Confidence | certain |
| Discovered | 2026-08-28 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | variant (same class as DF-0329, distinct site) |
Summary
When emitting an ND_REDIRECT with a target link-layer address option, the
option is length len = (2 + ifp->if_addrlen + 7) & ~7 but only
2 + ifp->if_addrlen bytes are ever written. For interfaces whose
if_addrlen is not 6, the pad bytes between the copied link address and the
option end are uninitialized mbuf-cluster heap bytes and are transmitted on
the wire. DF-0329 documented the same class for the redirected-header option;
this is the distinct TLLA site a DF-0329-only fix would leave live.
Root cause
icmp6.c:2496-2497 computes len = sizeof(*nd_opt) + ifp->if_addrlen
rounded up to 8; icmp6.c:2506-2511 then writes nd_opt_type/nd_opt_len
(2 bytes) and bcopy(LLADDR(sdl), lladdr, ifp->if_addrlen) β nothing ever
zeroes bytes [2+if_addrlen, len). The containing mbuf is a fresh
m_getb(IPV6_MMTU) cluster (icmp6.c:2412) with m_len advanced only over
written bytes, but the transmitted option region includes the pad
(p += len at 2511, m->m_pkthdr.len = m->m_len = p - ip6 at 2516, checksum
at 2611 covers it). For if_addrlen == 8 (FireWire/IEEE-1394 IP, EUI-64)
6 uninitialized bytes leak per redirect; for if_addrlen in 1..5, 9..13, β¦
up to 7 bytes leak; Ethernet (6) leaks none, which is why this is rarely seen.
Threat model & preconditions
- Attacker position: on-link sniffer adjacent to a DragonFly IPv6 router
(
ip6_forwarding=1,ip6_sendredirectsdefault 1,icmp6errppslimdefault 100pps perin6_proto.c:349/395/397) with an outgoing interface whose link-layer address length is not 6 bytes. - Privileges gained or impact: 1-7 bytes of kernel heap (cluster tail β can contain fragments of previously processed packets) per redirect.
- Required config or capabilities: router role + non-6-byte-addrlen interface (e.g. fwip if_addrlen=8).
- Reachability: transit traffic the router redirects toward the odd-addrlen interface with a resolved neighbor triggers emission; the leak is passive to capture.
Proof of concept
Build & run
DF guest as IPv6 router between an if_addrlen!=6 interface (e.g. fwip) and a transit network; send packets through the router so ip6_forward.c invokes icmp6_redirect_output toward the odd-addrlen interface with a resolved neighbor; capture with: tcpdump -i fwip icmp6
Expected output
bytes [2+if_addrlen, len) of the ND_OPT_TARGET_LINKADDR option vary across runs / match previously-freed cluster contents instead of being zero β same evidence pattern as DF-0329's leak_sample.txt.
Impact
Slow remote kernel-heap info leak, gated on a router configuration with a non-Ethernet-addrlen output interface.
Recommended fix
Zero the pad region after copying the address:
--- a/sys/netinet6/icmp6.c
+++ b/sys/netinet6/icmp6.c
@@ -2506,6 +2506,8 @@
nd_opt = (struct nd_opt_hdr *)p;
nd_opt->nd_opt_type = ND_OPT_TARGET_LINKADDR;
nd_opt->nd_opt_len = len >> 3;
lladdr = (char *)(nd_opt + 1);
bcopy(LLADDR(sdl), lladdr, ifp->if_addrlen);
+ /* zero the 8-byte-alignment pad, it is transmitted */
+ bzero(lladdr + ifp->if_addrlen,
+ len - sizeof(*nd_opt) - ifp->if_addrlen);
p += len;
References
- DF-0329 (same class, redirected-header option site)
- RFC 4861 Β§4.6.1 (option length rounding)
Timeline
- 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2611 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| tap_rdr.c | β | 9.9 KB | view raw | |
| kmem_addrlen.c | β | 4.5 KB | view raw | |
| setup.sh | β | 1.0 KB | view raw | |
| build.sh | β | 134 B | view raw | |
| run.sh | β | 765 B | view raw | |
| run.log | β | 47.4 KB | view raw | |
| leak_sample.txt | β | 2.8 KB | view raw | |
| build.log | β | 547 B | view raw | |
| env.txt | β | 429 B | view raw | |
| fix.diff | β | 704 B | view raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| README.md | β | 1.4 KB | β raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-2611 β redirect TLLA pad leak PoC
Files
tap_rdr.cthe whole harness in one binary:trigger N [nospray] [patch8]β owns tap0 (exclusive, destroy-on-close), performs all setup (address, static ND entries, scoped gateway route, forwarding), optionally rewrites tap0ifi_addrlento 8 via/root/poc/kmem_addrlen, sprays the cluster cache with a per-iteration pattern, injects a transit frame, captures and parses the emitted ND_REDIRECTkmem_addrlen.cifnet_array walker + validated ifi_addrlen rewritersetup.shhistorical (superseded by in-process setup)build.sh,run.sh,run.log(FULL untrimmed),leak_sample.txt,env.txt,fix.diff
Reproduce
guest# sh build.sh guest# sh run.sh
Expected: see manifest.json reproduce.expected. Bottom line: with
if_addrlen != 6 the target-link-layer-address option leaks 1-7 bytes of
uninitialized kernel heap per redirect; with 6 (Ethernet) it leaks none.
Caveat
QEMU provides no FireWire/EUI-64 NIC, so the non-Ethernet address length
is simulated by a privileged /dev/kmem rewrite of ifi_addrlen
(validated against three known interfaces first). Everything downstream β
option rounding, partial write, transmitted pad, checksum coverage β is
stock kernel behavior on the unmodified running kernel.
DF-2611 β icmp6_redirect_output TLLA option padding leak
What this proves
icmp6_redirect_output builds the target-link-layer-address option with
len = (2 + ifp->if_addrlen + 7) & ~7 (icmp6.c:2496-2497) but writes
only the 2-byte option header + ifp->if_addrlen address bytes
(icmp6.c:2506-2511). Nothing zeroes the pad region
[2+if_addrlen, len), yet p += len and the final
m->m_pkthdr.len = m->m_len = p - ip6 (icmp6.c:2516) TRANSMIT it. For
any interface whose link-layer address length is not 6, 1-7 bytes of
uninitialized mbuf-cluster heap are put on the wire per redirect.
Live reproduction (run.log, leak_sample.txt)
QEMU cannot emulate FireWire (the natural if_addrlen=8 ND interface), so
the harness reproduces the finding's exact precondition by rewriting
tap0's ifi_addrlen 6 -> 8 through /dev/kmem (offsets validated against
three known interfaces first: vtnet0 type=6/addrlen=6/mtu=1500, lo0
type=24/addrlen=0/mtu=16384, tap0 type=6/addrlen=6/mtu=1500) β a
privileged stand-in for plugging in an EUI-64 interface; the leaked code
path (bcopy length, option rounding, checksum coverage) is identical.
Sequence per iteration: dirty the cluster cache with a recognizable UDP
pattern (0xb0 | iter), inject a transit Ethernet/IPv6/UDP frame into
tap0, capture the ND_REDIRECT the kernel emits.
Results (35/35 redirects captured):
- Control, stock tap0 (addrlen 6): TLLA option
02 01 | 6 MAC bytes, option length 8 β zero pad bytes, exactly as the finding states for Ethernet. - addrlen 8 + spray, 20/20 redirects: TLLA option length 16, bytes
[10,16)= 6 bytes of the spray pattern, e.g. iter 1b1 b1 b1 b1 b1 b1, iter 2b2 b2 ..., iter 3b3 ...β the pad is the immediately-freed cluster's contents: attacker-influenced stale heap memory, checksummed and transmitted. - addrlen 8, no spray, 10/10 redirects: first 4 leaked
00 00 00 00 00 00(naturally clean cluster), then 6 leakedb5 b5 b5 b5 b5 b5β the pattern from the previous, already-finished spray run, still resident in the cluster cache. This decouples the leak from the spray mechanism itself: the pad bytes are whatever the heap last held, including fragments of unrelated previously-processed packets.
Quantification: 6 pad bytes per redirect for addrlen=8 (1-7 for other non-6 addrlens); 100% of redirects leak (20/20 sprayed, 10/10 unsprayed); variability across runs: pattern tracks heap history (b1..b4f sprayed, b5 stale, 00 virgin) β a byte-for-byte kernel-heap disclosure primitive on the wire, passive to any on-link sniffer.
Preconditions (as filed)
Router role (net.inet6.ip6.forwarding=1, net.inet6.ip6.redirect=1
default), transit traffic redirected out an interface with
if_addrlen != 6 and a resolved next-hop. The harness creates all of
this with tap0 + static ND entries + a scoped gateway route
(route add ... fe80::42:1%tap0 β note: an unscoped link-local gateway
sockaddr makes rt_llroute()'s rtlookup(rt_gateway) miss and the
forward fails with EHOSTUNREACH instead of emitting a redirect).
Files
tap_rdr.cfull harness: tap owner, setup, spray, inject, capture, TLLA-pad parser (trigger N [nospray] [patch8])kmem_addrlen.cifnet walker + validatedifi_addrlenrewritersetup.shsuperseded by tap_rdr's in-process setup (tap0 is destroy-on-close; kept for the record)build.sh,run.sh,run.log,leak_sample.txt,env.txt,fix.diff
Fix
fix.diff zeroes len - sizeof(nd_opt) - if_addrlen bytes after the
bcopy (icmp6.c:2510). Kernel-rebuild validation was not performed
(info-leak class, not memory corruption β the mandatory-rebuild rule does
not apply); the change is a mechanical 5-line bzero mirroring DF-0329's
verified fix for the redirected-header-option site.
Fix verification
not_testablefix.diff authored (5-line pad bzero at icmp6.c:2510) but single-fix kernel not built/rebooted: this is an info-leak finding, not memory corruption, so the mandatory rebuild rule does not apply, and the change is mechanically identical to DF-0329's already-verified fix for the sibling redirected-header-option site. Baseline leak is fully characterized above for later A/B if a maintainer wants it.
fix.diff; VERDICT.md; DF-0329 pack (verified fix for the sibling site, findings/poc/DF-0329/).
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log + leak_sample.txt: control 'TLLA bytes: 02 00 00 00 00 01' (5x, option len 8); spray phase 'TLLA bytes: 02 00 00 00 00 01 00 00 b1 b1 b1 b1 b1 b1' .. 'b4' wrapping over 20 iters (option 'type=2 len=16'); no-spray phase '00 00 00 00 00 00' x4 then 'b5 b5 b5 b5 b5 b5' x6 (stale pattern from the previous run - leak decoupled from the spray). kmem validation lines: vtnet0 type=6 addrlen=6 mtu=1500 / lo0 type=24 addrlen=0 mtu=16384 / tap0 type=6 addrlen=6->WROTE 8 (readback).
PoC changes
No seed. Toolkit written from scratch; material discoveries during bring-up: (1) /dev/tap is exclusive-open and destroy-on-close, so one process must own tap0 end-to-end (setup in-process); (2) the gateway route MUST carry a scoped link-local gateway ('fe80::42:1%tap0') or rt_llroute()'s rtlookup(rt_gateway) misses and the forward dies with EHOSTUNREACH (ICMPv6 unreach code 3) instead of emitting a redirect; (3) TLLA pad analysis must parse options at frame offset 94 (14 eth + 40 ip6 + 40 nd_redirect); (4) the if_addrlen mutation must happen while the harness holds tap0 (ifnet_array is rebuilt on interface create/destroy).
Verified recommended fix
bzero(lladdr + ifp->if_addrlen, len - sizeof(*nd_opt) - ifp->if_addrlen) after the LLADDR bcopy in icmp6_redirect_output (see fix.diff); same class as DF-0329's verified redirected-header fix.
Verdict
REPRODUCED live on the stock INVARIANTS guest: icmp6_redirect_output transmits the 8-byte-alignment pad of the ND_OPT_TARGET_LINKADDR option as uninitialized mbuf-cluster heap memory. With tap0's ifi_addrlen rewritten 6->8 (via /dev/kmem, offsets pre-validated against vtnet0/lo0/tap0 known values - a privileged stand-in for the EUI-64/fwip interface QEMU cannot emulate; the leaked code path is identical), every emitted redirect (35/35 across three phases) carried a 16-byte TLLA option of which bytes [10,16) were stale cluster contents: 20/20 redirects leaked the immediately-preceding UDP spray pattern (b1,b2,b3... incrementing per iteration - byte-perfect attribution to the freed cluster), and 10/10 no-spray redirects leaked '00' x6 (virgin cluster) then 'b5' x6 - the pattern of the EARLIER finished spray run still resident in the heap. Control with stock addrlen=6: option length 8, zero pad bytes, exactly as the finding states for Ethernet. Quantified: 6 pad bytes per redirect for addrlen=8 (1-7 for other non-6 addrlens), 100% emission rate, contents = whatever the cluster cache last held (attacker-influenced when on-path traffic grooms it), passively sniffable on-link.
No comments yet.