DF-3318 / verdict.json
{ "finding_id": "DF-3318", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "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.", "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": [ "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()" ], "kernel_refs": [ "sys/netinet6/in6.c:901", "sys/netinet6/in6.c:893-896", "sys/netinet6/in6.c:930", "sys/netinet6/in6.c:1052-1067", "sys/netinet6/in6.c:1570-1572", "sys/netinet6/in6.c:1598-1602", "sys/netinet6/in6.c:490-499", "sys/kern/sys_generic.c:668-680", "sys/kern/sys_generic.c:730" ], "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.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline); #1: Thu Sep 10 18:48:19 UTC 2026 (patched)", "runtime_sec": 3120, "guest_dirty": 0, "build_cmd": "cd /tmp/df3318 && cc -O -o setup setup.c && cc -O -o leak leak.c && cc -O -o delete delete.c", "run_cmd": "./setup gif0 200; su -m nobody -c ./leak gif0 # privileged smash: ./delete gif0 (panics)", "code_hash": "sha256(setup.c)=b8073b9bd2d6ee9493e11b82942b060c7989156bcd50ea62eb89a99aba2c3996 sha256(leak.c)=0cad0725cb5609aa4139e95d2ba092bfceb3bb8cbc4f1e0472cb97ae9e3b8602 sha256(delete.c)=c2c22349fb4de0e04b2276792f227090ecbda02f2887aab039cb13d5cd8d33f7", "notes": "Setup requires one privileged SIOCAIFADDR_IN6 (root must misconfigure once, e.g. buggy config tool); the leak trigger itself is unprivileged (SIOCGLIFADDR has no privilege check, in6.c:490-499) and persists until the address is deleted. The stack-smash trigger is privileged (no unpriv write path exists -- impact recorded as leak). The same missing-validation pattern exists on the IPv4 side (sys/netinet/in.c:930-931, 952-953) -- separate file, reported there in its own audit. Guest reset to with-src snapshot after fix validation.", "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().", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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_evidence": "fix_run.log (patched-kernel rerun of the exact PoC), fix_build.log (build facts), fix.diff" } |