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)
PoC verification
Evidence pack
findings/poc/DF-0354 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/tap0viara_inject_tap(root setup, simulating the remote flood). Each unique source IPv6 creates onend_defrouter; each RA attaches that router to the shared prefix'sndpr_advrtrslist. - Exploit step (the unprivileged half): any user reads
net.inet6.icmp6.nd6_prlistvia sysctl. The returned buffer is>1024bytes; 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.
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).
char buf[1024]on the stack (line 2201).- Inner loop (lines 2238β2255) iterates the per-prefix router list.
When
sin6[advrtrs+1] > pe(no room in buf) it executesadvrtrs++; continue;(lines 2240β2242) β i.e. it counts the router anyway without writing thesin6entry. - Line 2256:
p->advrtrs = advrtrs;(total count, used for the user-visibleadvrtrsfield β informational). - Line 2261:
advance = sizeof(*p) + sizeof(*sin6) * advrtrs;uses the total count, not the count that fit. - Line 2262:
SYSCTL_OUT(req, buf, advance)copiesadvancebytes frombuf. Sinceadvance > sizeof(buf),copyoutreadsadvance - 1024bytes pastbuffrom 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'sA:Lwould 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/tapRA injector used in the repro (preferred)ra_test.cβ minimal one-packet injector (sanity check)ra_read.cβ unprivileged sysctl reader / dumperbpf_sniff.cβ BPF sniffer (debugging aid)fix.diffβ the verified one-line-clamp fixbuild.sh,run.shβ exact build/run commandsbuild.logβ final build outputrun.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, sysctlsVERDICT.mdβ this filemanifest.jsonβ machine-readable catalog
Fix verification
fixedvalidated
see evidence pack
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.
No comments yet.