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

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_sendredirects default 1, icmp6errppslim default 100pps per in6_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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2611 Β· 13 files
FileTypeDescriptionSize
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.c the 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 tap0 ifi_addrlen to 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_REDIRECT
  • kmem_addrlen.c ifnet_array walker + validated ifi_addrlen rewriter
  • setup.sh historical (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.

VERDICT.md
↓ download raw

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 1 b1 b1 b1 b1 b1 b1, iter 2 b2 b2 ..., iter 3 b3 ... β€” 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 leaked b5 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.c full harness: tap owner, setup, spray, inject, capture, TLLA-pad parser (trigger N [nospray] [patch8])
  • kmem_addrlen.c ifnet walker + validated ifi_addrlen rewriter
  • setup.sh superseded 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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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/).
↓ fix.diffper-fix-DF-2611

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.