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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3318 Β· 23 files
FileTypeDescriptionSize
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 past struct in6_ifaddr; bytes 28..127 of iflr->dstaddr (kernel heap contents, incl. ia_next and ia6_ndpr pointers) are copied back to the unprivileged caller by sys/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-stack struct in6_aliasreq at 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 == 200 echoed back from kernel state; bytes 36..51 = the /128 prefix mask; bytes 96..103 = 0xffffffff lifetimes; bytes 64..71 = 0xfffff8004f3097c0 (ia_next, kernel heap pointer); bytes 120..127 = 0xfffff8004f3098b0 (adjacent slab content, past the end of the in6_ifaddr allocation). Identical across 3 runs as nobody.
  • delete: immediate Fatal trap 12, instruction pointer = 0x8:0x0, frame pointer = 0x0 β€” return path of in6_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 β€” sources
  • build.sh / run.sh β€” exact commands
  • build.log, run.log (leak), run.2.log, run.3.log, delete.log β€” full untrimmed output
  • panic.txt β€” serial-console panic from the privileged stack smash
  • leak_sample.txt β€” the three leak runs side by side
  • env.txt β€” guest uname / compiler
  • fix.diff β€” the validated fix (never applied to the read-only sys/ tree)
  • VERDICT.md, manifest.json, verdict.json β€” verdicts
VERDICT.md
↓ download raw

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():

  1. SIOCGLIFADDR β€” in6.c:1570-1572 β€” unprivileged (the privilege check at in6.c:490-499 covers 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).

  1. 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_IN6 on a fresh gif0 (POINTOPOINT) β€” 2001:db8::1/128 dst 2001:db8::2, sin6_len=200, infinite lifetimes; a second address 2001:db8::5/64 is added afterwards so the crafted ia is not the tail of the global in6_ifaddr list.
  • Leak as nobody (leak.c, SIOCGLIFADDR with IFLR_PREFIX/128): dstaddr.sin6_len=200 echoed back from kernel state, and bytes 28..127 of dstaddr contain 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 the 2001:db8::5 ifaddr
  • offset 96..103: 0xffffffff_ffffffff β€” the ia6t_vltime/ia6t_pltime infinite lifetimes set during setup
  • offset 120..127: 0xfffff8004f3098b0 β€” content past the end of the in6_ifaddr allocation (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_len state requires one privileged SIOCAIFADDR_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_dstaddr in the M_IFADDR slab, 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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 18:48:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.