in6_update_ifa() accepts arbitrary ifra_dstaddr.sin6_len β unprivileged SIOCGLIFADDR kernel-heap pointer leak and privileged SIOCDLIFADDR kernel stack buffer overflow
Summary
in6_update_ifa() validates the prefixmask sockaddr length but never ifra_dstaddr.sin6_len - only its family. On a POINTOPOINT/LOOPBACK interface with /128 prefix the user-supplied sockaddr is copied verbatim into ia->ia_dstaddr, persisting any u_char 0-255 into ia_dstaddr.sin6_len. in6_lifaddr_ioctl() then uses that value as a bcopy() length: SIOCGLIFADDR - which has NO privilege check - reads up to 227 bytes past ia_dstaddr through the tail of struct in6_ifaddr and past the end of the heap object, with bytes 28..127 of iflr.dstaddr (including ia->ia_next and ia6_ndpr kernel heap pointers and adjacent-slab content) copied back to the unprivileged caller. SIOCDLIFADDR (privileged) does the same bcopy into a 128-byte on-stack struct in6_aliasreq at offset 44 - a kernel stack overflow of up to 171 bytes with heap-derived (groomable) data. VERIFIED on guest: unpriv leak 3/3 (kernel pointers 0xfffff8004f3097c0 at offset 64, 0xfffff8004f3098b0 at 120); privileged trigger panics deterministically (Fatal trap 12, ip=0x8:0x0, fp=0x0 - return path smashed). Creating the malformed state requires one privileged SIOCAIFADDR_IN6 (e.g. buggy config tool passing garbage dstaddr) - then any unpriv user reads live kernel heap pointers indefinitely; root gets a deterministic stack smash on delete. Fix validated in-guest (symmetric length check): setup->EINVAL, leak/delete->EADDRNOTAVAIL, guest stays up. The identical IPv4 pattern exists at sys/netinet/in.c:930-931/952-953 (cross-file lead).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3318 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| setup.c | β | 4.4 KB | view raw | |
| leak.c | β | 3.0 KB | view raw | |
| delete.c | β | 1.7 KB | view raw | |
| build.sh | β | 129 B | view raw | |
| run.sh | β | 143 B | view raw | |
| build.log | β | 523 B | view raw | |
| run.log | β | 738 B | view raw | |
| run.2.log | β | 738 B | view raw | |
| run.3.log | β | 738 B | view raw | |
| leak_sample.txt | β | 2.7 KB | view raw | |
| delete.log | β | 52 B | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| console-full.txt | β | 2.2 KB | view raw | |
| setup-run1.log | β | 305 B | view raw | |
| leak-run-linklocal.log | β | 1.1 KB | view raw | |
| fix_run.log | β | 680 B | view raw | |
| fix_build.log | β | 1.3 KB | view raw | |
| fix.diff | β | 820 B | view raw | |
| env.txt | β | 535 B | view raw | |
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw |
DF-3318 β unvalidated ifra_dstaddr.sin6_len in in6_update_ifa() β OOB read (unprivileged leak) + kernel stack OOB write
File: sys/netinet6/in6.c (pass-2 audit, GLM 5.3)
Root cause (one line)
in6_update_ifa() validates ifra_prefixmask.sin6_len (in6.c:901) but
never validates ifra_dstaddr.sin6_len; the whole sockaddr_in6 β
including a user-chosen sin6_len up to 255 β is copied into
ia->ia_dstaddr (in6.c:1052-1067), and in6_lifaddr_ioctl() later uses
ia->ia_dstaddr.sin6_len as a bcopy() length:
in6.c:1570-1572βSIOCGLIFADDR(no privilege check,in6.c:490-499):bcopy(&ia->ia_dstaddr, &iflr->dstaddr, ia->ia_dstaddr.sin6_len)into the 280-byte kernel ioctl buffer at offset 152 β OOB heap read paststruct in6_ifaddr; bytes 28..127 ofiflr->dstaddr(kernel heap contents, incl.ia_nextandia6_ndprpointers) are copied back to the unprivileged caller bysys/kern/sys_generic.c:730.in6.c:1598-1602βSIOCDLIFADDR(privileged):bcopy(&ia->ia_dstaddr, &ifra.ifra_dstaddr, ia->ia_dstaddr.sin6_len)into a 128-byte on-stackstruct in6_aliasreqat offset 44 β kernel stack buffer overflow of up to 171 bytes with kernel-heap-derived data.
Reproduce on the QEMU guest
./build.sh # cc -O -o setup setup.c; cc -O -o leak leak.c; cc -O -o delete delete.c # 1) privileged one-time setup: gif0 + 2001:db8::1/128 --> 2001:db8::2 with # ifra_dstaddr.sin6_len = 200, infinite lifetimes, plus 2001:db8::5/64 # (so crafted_ia->ia_next != NULL): ./setup gif0 200 # 2) unprivileged leak (as nobody) -- expect kernel pointers at dstaddr[64] # and dstaddr[120]: su -m nobody -c "./leak gif0" # 3) privileged kernel stack smash (panics the guest; RIP=0): ./delete gif0
Success criteria (all observed on stock INVARIANTS kernel #0):
leak:dstaddr.sin6_len == 200echoed back from kernel state; bytes 36..51 = the /128 prefix mask; bytes 96..103 =0xfffffffflifetimes; bytes 64..71 =0xfffff8004f3097c0(ia_next, kernel heap pointer); bytes 120..127 =0xfffff8004f3098b0(adjacent slab content, past the end of thein6_ifaddrallocation). Identical across 3 runs asnobody.delete: immediateFatal trap 12,instruction pointer = 0x8:0x0,frame pointer = 0x0β return path ofin6_lifaddr_ioctl()smashed.
Fix
fix.diff β reject ifra_dstaddr.sin6_len > sizeof(struct sockaddr_in6)
in in6_update_ifa(), mirroring the existing prefixmask check one line
above. Validated in-guest (kernel rebuild + rerun): setup now returns
EINVAL and no address/leak/panic is possible.
Contents
setup.c/leak.c/delete.cβ sourcesbuild.sh/run.shβ exact commandsbuild.log,run.log(leak),run.2.log,run.3.log,delete.logβ full untrimmed outputpanic.txtβ serial-console panic from the privileged stack smashleak_sample.txtβ the three leak runs side by sideenv.txtβ guest uname / compilerfix.diffβ the validated fix (never applied to the read-onlysys/tree)VERDICT.md,manifest.json,verdict.jsonβ verdicts
DF-3318 VERDICT
Status: reproduced (unprivileged info leak 3/3 runs + privileged kernel stack smash β panic captured). Impact: kernel heap pointer disclosure to unprivileged users (after one-time privileged misconfiguration) + kernel stack buffer overflow (privileged trigger).
The bug
in6_update_ifa() (sys/netinet6/in6.c) validates the prefixmask
sockaddr length at in6.c:901-902:
if (ifra->ifra_prefixmask.sin6_len > sizeof(struct sockaddr_in6))
return (EINVAL);
but performs no equivalent check for ifra_dstaddr β only the address
family is checked (in6.c:893-896, AF_INET6/AF_UNSPEC on p2p). For a
point-to-point or loopback interface with prefix length 128, the user-supplied
sockaddr_in6 is copied verbatim into the live ifaddr at in6.c:1052-1067:
if (dst6.sin6_family == AF_INET6 && ...) {
...
ia->ia_dstaddr = dst6; /* sin6_len = any u_char, e.g. 200 */
}
ia_dstaddr.sin6_len then becomes a bcopy() length in
in6_lifaddr_ioctl():
SIOCGLIFADDRβin6.c:1570-1572β unprivileged (the privilege check atin6.c:490-499covers only SIOCALIFADDR/SIOCDLIFADDR):
c
if (ifp->if_flags & IFF_POINTOPOINT) {
bcopy(&ia->ia_dstaddr, &iflr->dstaddr,
ia->ia_dstaddr.sin6_len); /* 200 */
The generic ioctl layer allocated exactly 280 bytes for
struct if_laddrreq (sys/kern/sys_generic.c:668-680,
SIOCGLIFADDR = _IOWR('i', 28, struct if_laddrreq),
280 > STK_PARAMETERS=128 β kmalloc(280, M_IOCTLOPS)).
iflr->dstaddr sits at offset 152, so a length of 200 writes 72 bytes
past the allocation (masked in practice by the allocator's 512-byte bin)
and β decisively β reads 172 bytes past ia_dstaddr, i.e. through the
rest of struct in6_ifaddr and past the end of the heap object. The
first 128 bytes of iflr->dstaddr are then copied back to userspace
(sys_generic.c:730).
SIOCDLIFADDRβin6.c:1598-1602β privileged:
c
struct in6_aliasreq ifra; /* 128-byte on-stack local, dstaddr at +44 */
...
bcopy(&ia->ia_dstaddr, &ifra.ifra_dstaddr, ia->ia_dstaddr.sin6_len);
200 bytes written into a 128-byte stack object β kernel stack buffer overflow with kernel-heap-derived (partially groomable) data.
Reproduction log (stock INVARIANTS kernel #0, guest dfbsd 6.5-DEVELOPMENT)
- Setup as root (
setup.c):SIOCAIFADDR_IN6on a freshgif0(POINTOPOINT) β2001:db8::1/128dst2001:db8::2,sin6_len=200, infinite lifetimes; a second address2001:db8::5/64is added afterwards so the craftediais not the tail of the globalin6_ifaddrlist. -
Leak as
nobody(leak.c,SIOCGLIFADDRwithIFLR_PREFIX/128):dstaddr.sin6_len=200echoed back from kernel state, and bytes 28..127 ofdstaddrcontain kernel-side heap data: -
offset 36..51: the /128 prefix mask (16 Γ 0xff)
- offset 64..71:
0xfffff8004f3097c0βia->ia_next, a kernel heap pointer to the2001:db8::5ifaddr - offset 96..103:
0xffffffff_ffffffffβ the ia6t_vltime/ia6t_pltime infinite lifetimes set during setup - offset 120..127:
0xfffff8004f3098b0β content past the end of thein6_ifaddrallocation (cross-object slab read)
Identical output across three consecutive runs (run.log, run.2.log,
run.3.log).
* Stack smash as root (delete.c, SIOCDLIFADDR): immediate
Fatal trap 12: page fault while in kernel mode,
instruction pointer = 0x8:0x0, frame pointer = 0x0 β the smashed
return path of in6_lifaddr_ioctl() (see panic.txt). Guest wedged in
DDB; ssh gone.
Why the impact ceiling is what it is
- The unprivileged manifestation is an info leak of kernel heap
addresses (KASLR-defeating; pointer + slab-layout disclosure), not a
write: creating the malformed
sin6_lenstate requires one privilegedSIOCAIFADDR_IN6(root must misconfigure once β e.g. a buggy config tool). No unprivileged-only path to the OOB write exists, so no uid=0 chain is honestly claimable; this is not reported as privesc. - The privileged manifestation is a deterministic kernel stack
control-flow corruption (RIP=0 observed; the smashed bytes come from
beyond
ia_dstaddrin theM_IFADDRslab, i.e. groomable by the same privileged user) β local DoS at minimum for root, and it elevates the missing check from "cosmetic validation" to memory-corruption class.
Fix validation
fix.diff adds the symmetric length check in in6_update_ifa():
if (ifra->ifra_dstaddr.sin6_len > sizeof(struct sockaddr_in6))
return (EINVAL);
Applied inside the guest's /usr/src, make -j4 nativekernel, rebooted
into the patched kernel (fix.log):
- baseline (stock kernel): setup OK β leak shows
sin6_len=200+ kernel pointers; delete panics the guest (panic.txt). - patched:
./setup gif0 200βSIOCAIFADDR_IN6 failed: Invalid argument(fix.log); no address is created;./leakβEADDRNOTAVAIL(no matching address);./deleteβEADDRNOTAVAIL, guest stays up. Bad behavior gone; correct error returned.
Lines cited
sys/netinet6/in6.c:893-896β family-only dstaddr validationsys/netinet6/in6.c:901-902β prefixmask check (no dstaddr analogue)sys/netinet6/in6.c:930,sys/netinet6/in6.c:1052-1067β bogus sin6_len persisted intoia_dstaddrsys/netinet6/in6.c:1570-1572β unprivileged OOB read (SIOCGLIFADDR)sys/netinet6/in6.c:1598-1602β privileged stack smash (SIOCDLIFADDR)sys/netinet6/in6.c:490-499β SIOCGLIFADR/SIOCGLIFADDR privilege splitsys/kern/sys_generic.c:668-680,730β 280-byte kmalloc + copyout
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src, make -j4 nativekernel RC=0, installkernel, reboot into #1 kernel. Baseline: setup OK -> unpriv leak with kernel pointers -> privileged delete panics (RIP=0). Patched: ./setup gif0 200 -> 'SIOCAIFADDR_IN6 failed: Invalid argument' (rejected at the door); ./leak -> EADDRNOTAVAIL; ./delete -> EADDRNOTAVAIL; guest stays up (uptime 1 min). Bad behavior gone, correct error returned.
fix_run.log (patched-kernel rerun of the exact PoC), fix_build.log (build facts), fix.diff
Confirmed kernel references
Detail
Exploit chain
root (once): SIOCAIFADDR_IN6 on gif0 with 2001:db8::1/128, dst 2001:db8::2, ifra_dstaddr.sin6_len=200, infinite lifetimes; + 2001:db8::5/64 so crafted ia is not the list tail -> unprivileged: SIOCGLIFADDR(IFLR_PREFIX,/128) as nobody -> kernel heap OOB read -> iflr.dstaddr[64..71] = ia->ia_next kernel pointer, dstaddr[120..127] = cross-object slab pointer copied to userspace (KASLR-defeating heap disclosure; repeatable, stable offsets). Privileged variant of the same root cause: SIOCDLIFADDR -> 116+ bytes past a 128-byte stack local -> RIP=0 panic observed (groomable M_IFADDR-adjacent source bytes; full ROP not developed because the trigger itself requires root -- no unprivileged write primitive exists, so no uid=0 chain is honestly claimable).
Evidence (decisive lines)
['run.log/run.2.log/run.3.log: three identical unprivileged leak runs; dstaddr.sin6_len=200 echoed back, kernel heap pointers at offsets 64 and 120', 'leak_sample.txt: all three runs side by side', 'panic.txt + console-full.txt: Fatal trap 12, instruction pointer 0x8:0x0, frame pointer 0x0 after privileged SIOCDLIFADDR', 'fix_run.log: patched kernel -- setup EINVAL, leak EADDRNOTAVAIL, delete EADDRNOTAVAIL, guest stays up', 'fix.diff: one-line symmetric length check in in6_update_ifa()']
PoC changes
Seed PoC (none existed -- new finding): wrote setup.c/leak.c/delete.c from scratch. Two iterations were needed: (1) plain SIOCGLIFADDR returned the auto link-local address -> switched leak.c to IFLR_PREFIX/128 exact matching; (2) the crafted address vanished immediately because ia6t_vltime=0 expired it (dmesg: 'valid lifetime is 0') -> set infinite vltime/pltime in setup.c and added a trailing 2001:db8::5 address so ia_next != NULL.
Verified recommended fix
In in6_update_ifa(), reject ifra_dstaddr.sin6_len > sizeof(struct sockaddr_in6) (mirroring the prefixmask check at in6.c:901); optionally also clamp/validate lengths used as bcopy() sizes in in6_lifaddr_ioctl().
Verdict
in6_update_ifa() never validates ifra_dstaddr.sin6_len (it validates the prefixmask length one line above, in6.c:901), so a privileged SIOCAIFADDR_IN6 on a POINTOPOINT interface persists an attacker-chosen u_char (up to 255) into ia->ia_dstaddr.sin6_len (in6.c:1052-1067). in6_lifaddr_ioctl() then uses that value as a bcopy() length: SIOCGLIFADDR (unprivileged, in6.c:1570-1572) reads up to 227 bytes past ia_dstaddr -- through the rest of struct in6_ifaddr and past the end of the heap object -- and copies the first 128 bytes of the result back to userspace (sys_generic.c:730), disclosing ia_next and adjacent-slab kernel heap pointers (0xfffff8004f3097c0 / 0xfffff8004f3098b0 observed, 3/3 identical runs as nobody); SIOCDLIFADDR (privileged, in6.c:1598-1602) writes up to 255 bytes into the 128-byte on-stack struct in6_aliasreq ifra at offset 44, a kernel stack buffer overflow demonstrated by a deterministic Fatal trap 12 with instruction pointer = 0x0 and frame pointer = 0x0 (return path smashed; panic.txt). Fix validated in-guest: the symmetric sin6_len check in in6_update_ifa() rejects the setup ioctl with EINVAL on the rebuilt kernel and both manifestations disappear while the guest stays up.
No comments yet.