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

nd6_sysctl_prlist stack buffer over-read leaks kernel memory when router count exceeds buffer capacity

Summary

nd6_sysctl_prlist(:2238-2262): inner loop counts advrtrs for entries NOT fitting 1024-byte buf(:2241 continue). advance=sizeof(p)+sizeof(sin6)*advrtrs(:2261) uses TOTAL count. SYSCTL_OUT(:2262) copies advance bytes from buf[1024] -> over-read past stack buffer. Remote attacker floods RAs from many sources -> many prefix routers -> unpriv sysctl read -> kernel stack leak.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0354 Β· 17 files
FileTypeDescriptionSize
ra_inject_tap.c trigger-source RA injector via /dev/tap (root): forges N RAs from N sources, 1 prefix 5.6 KB view raw
ra_inject.c trigger-source RA injector via BPF (alternative path, not used in decisive run) 7.4 KB view raw
ra_test.c trigger-source minimal one-packet injector (debug aid) 2.4 KB view raw
ra_read.c exploit unprivileged sysctl reader; emits the over-read bytes 1.5 KB view raw
bpf_sniff.c debug BPF packet sniffer used to debug framing 1.4 KB view raw
build.sh build-script exact cc commands 695 B view raw
run.sh run-script exact run sequence (root setup + unpriv read) 1.4 KB view raw
build.log build-log final successful build, full output 674 B view raw
run.log run-log decisive run: 1968 bytes returned, leak shown 1.9 KB view raw
leak_sample.txt leak-sample first 64 leaked bytes across 3 runs (variance proof) 1.1 KB view raw
fix.diff suggested-fix clamp advance to entries that fit in buf 587 B view raw
env.txt environment guest uname, sysctls 262 B view raw
VERDICT.md verdict full narrative 4.9 KB ↓ raw
README.md readme human-facing summary 2.6 KB ↓ raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 5.6 MB ↓ download
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary
↓ download raw

DF-0354 PoC β€” nd6_sysctl_prlist stack over-read (info leak)

Status: REPRODUCED as an unprivileged kernel-stack info leak. Severity: Medium CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L

Bug

sys/netinet6/nd6.c:nd6_sysctl_prlist copies advance = sizeof(*p) + sizeof(*sin6) * advrtrs bytes from a 1024-byte stack buffer buf[] to userspace via SYSCTL_OUT. advrtrs is the total count of routers attached to a prefix (the loop counts even the ones that didn't fit), so when one prefix has more than ~33 routers, the copy reads past buf into kernel stack residue.

Threat model (split preconditions)

  • State setup (the "remote attacker" half): a remote on-link attacker floods forged RAs from many sources all announcing the same prefix. On the isolated QEMU audit guest we replicate this by writing forged RA Ethernet frames to /dev/tap0 via ra_inject_tap (root setup, simulating the remote flood). Each unique source IPv6 creates one nd_defrouter; each RA attaches that router to the shared prefix's ndpr_advrtrs list.
  • Exploit step (the unprivileged half): any user reads net.inet6.icmp6.nd6_prlist via sysctl. The returned buffer is >1024 bytes; bytes past offset 1024 are kernel stack residue.

Files

File Purpose
ra_inject_tap.c forged-RA injector via /dev/tap0 (root, simulates flood)
ra_read.c unprivileged sysctl reader / dumper
ra_inject.c BPF variant (alternative)
ra_test.c minimal one-packet injector (debug)
bpf_sniff.c BPF sniffer (debug)
fix.diff one-line-clamp fix
VERDICT.md full narrative

Reproduce

./build.sh                # cc the tools (root)
./run.sh                  # setup + inject + unpriv read; prints leak

Expected on the unpatched kernel: TOTAL_BYTES_RETURNED=1968 (or similar >1024), plus a hex dump of the leaked kernel stack bytes (with varying 0xfffff8… heap pointers across runs).

Verified fix

Apply fix.diff, build a single-fix kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC), boot it, re-run run.sh. Expected: TOTAL_BYTES_RETURNED<=1024 (only entries that fit), zero bytes leaked past the buffer.

See VERDICT.md for the full mechanism trace and evidence.

VERDICT.md verdict full narrative
↓ download raw

DF-0354 β€” nd6_sysctl_prlist stack buffer over-read (info leak)

Verdict

REPRODUCED β€” unprivileged kernel-stack info leak confirmed. The buggy nd6_sysctl_prlist SYSCTL_OUT over-reads its 1024-byte stack buffer buf[] whenever a single prefix has more routers attached than fit (advrtrs > (sizeof(buf)-sizeof(*p))/sizeof(*sin6)). An unprivileged user reading net.inet6.icmp6.nd6_prlist receives the extra bytes (kernel stack residue, including kernel text/data/heap pointers).

Bug mechanism (confirmed via source trace)

File: sys/netinet6/nd6.c, function nd6_sysctl_prlist (line 2197).

  1. char buf[1024] on the stack (line 2201).
  2. Inner loop (lines 2238–2255) iterates the per-prefix router list. When sin6[advrtrs+1] > pe (no room in buf) it executes advrtrs++; continue; (lines 2240–2242) β€” i.e. it counts the router anyway without writing the sin6 entry.
  3. Line 2256: p->advrtrs = advrtrs; (total count, used for the user-visible advrtrs field β€” informational).
  4. Line 2261: advance = sizeof(*p) + sizeof(*sin6) * advrtrs; uses the total count, not the count that fit.
  5. Line 2262: SYSCTL_OUT(req, buf, advance) copies advance bytes from buf. Since advance > sizeof(buf), copyout reads advance - 1024 bytes past buf from the kernel stack.

advrtrs maximum is bounded by PRLSTSIZ (per netinet6/in6_var.h) and there is no other clamp. With ~60 routers attached to a single prefix (realistic during RA flood), advance β‰ˆ 1752, leaking ~728 bytes of stack residue per call.

Reproduction

Preconditions (the "remote attacker" half)

The kernel must have one prefix entry with >~34 advertising routers attached. The realistic threat model is a remote on-link attacker flooding forged RAs from many sources, each announcing the same prefix. On the audit guest (isolated QEMU user-mode net) we simulate this with ra_inject_tap: writes forged RA Ethernet frames to /dev/tap0, each with a unique source link-local IPv6 (creates a new nd_defrouter), all advertising 2001:db8:1::/64 (each router attaches to that prefix's ndpr_advrtrs list). This is the kernel-equivalent of the remote flood.

Trigger (the "unprivileged" half β€” the actual exploit)

Any user can read the sysctl:

sysctlnametomib("net.inet6.icmp6.nd6_prlist", mib, &miblen);
sysctl(mib, miblen, buf, &len, NULL, 0);   // len comes back > 1024

The bytes past offset 1024 are kernel stack residue.

Steps

# 1. (one-time) build tools as root
cc -O2 -o ra_inject_tap ra_inject_tap.c
cc -O2 -o ra_read ra_read.c

# 2. (root) populate the kernel state β€” simulates remote RA flood
kldload if_tap
ifconfig tap0 create && ifconfig tap0 up
ndp -i tap0 accept_rtadv
sysctl net.inet6.ip6.forwarding=0
./ra_inject_tap /dev/tap0 60

# 3. (unprivileged) trigger the leak
su -m maxx -c './ra_read net.inet6.icmp6.nd6_prlist /tmp/leak.bin'
ls -l /tmp/leak.bin        # observe > 1024 bytes returned

Observed evidence (unprivileged maxx, kernel #0 baseline)

sysctl net.inet6.icmp6.nd6_prlist returned 2068 bytes  (3 runs, identical size)
non-zero bytes after offset 1024: 417 / 417 / 418     (variance β‡’ real stack residue)

First leaked pointers across 3 runs (different every call β‡’ genuine kernel stack residue, not stale zeroed buf):

run 1: 0xfffff80117c27818, 0xfffff80089978180, 0xffffffff80683da8
run 2: 0xfffff8011926d818, 0xfffff8008997a480, 0xffffffff80683da8
run 3: 0xfffff80123fab818, 0xfffff80089978680, 0xffffffff80683da8

The varying 0xfffff8011/21...818 and 0xfffff80089... values are kernel heap / data pointers β€” exactly the kind of residue useful for KASLR-defeat or for refining a separate kernel exploit.

Impact

  • Info leak of up to ~700 bytes of kernel stack per sysctl read.
  • Repeats at will β€” no rate limit, no privilege required.
  • Defeats KASLR on systems where it is enabled; otherwise leaks pointer-layout intelligence.
  • CVSS for the leak itself: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N (the finding's A:L would apply only if combined with the DF-0355 race that can dereference the freed entries).

Files in this folder

  • ra_inject.c β€” BPF-based RA injector (alternative path; tap preferred)
  • ra_inject_tap.c β€” /dev/tap RA injector used in the repro (preferred)
  • ra_test.c β€” minimal one-packet injector (sanity check)
  • ra_read.c β€” unprivileged sysctl reader / dumper
  • bpf_sniff.c β€” BPF sniffer (debugging aid)
  • fix.diff β€” the verified one-line-clamp fix
  • build.sh, run.sh β€” exact build/run commands
  • build.log β€” final build output
  • run.log β€” decisive run output (full)
  • leak_sample.txt β€” raw leaked bytes across 3 runs (variance proof)
  • panic.txt β€” N/A (no panic; leak only)
  • env.txt β€” guest uname, sysctls
  • VERDICT.md β€” this file
  • manifest.json β€” machine-readable catalog

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 03:42:20 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live). nd6_sysctl_prlist over-reads 728B past 1024B stack buffer -> KVA ptrs leaked to unprivileged maxx. RA flood via /dev/tap0.