DF-3432 / verdict.json
{ "finding_id": "DF-3432", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "udp6_connect() (sys/netinet6/udp6_usrreq.c:581) evaluates IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr) at :597 before any sa_len validation \u2014 the first length check on the path is in6_pcbladdr()s (in6_pcb.c:282), reached only via in6_pcbconnect() at udp6_usrreq.c:608, after the read. getsockaddr() kmallocs exactly the user-supplied sockaddr length, so len<28 reads past the allocation (IN6_IS_ADDR_V4MAPPED window = offsets 8..19). Verified on the guest as an unprivileged user (uid maxx): grooming the shared 24-byte M_SONAME slab chunk via a rejected bind(24) then connect(17) makes stale heap bytes 17..19 \u2014 outside the 17-byte sockaddr \u2014 complete the v4-mapped pattern and deterministically flip connect() from EINVAL to EADDRNOTAVAIL, 20/20 vs 0/20 differential across 3 runs. When the pattern completes, the return happens at udp6_usrreq.c:598, so in6_pcbconnect (and DF-3415s read at in6_pcb.c:344) is never reached: the deciding read for that branch is this findings. Impact ceiling is a comparison oracle on stale heap bytes (they only select an errno; in6_pcbladdr still rejects short sockaddrs, no connection state is established from OOB data). Severity Low, kernleak family \u2014 the udp6_usrreq.c instance of DF-3357/3372/3415 (the v4 twin udp_send lacks the pre-validation that udp6_send has at udp6_usrreq.c:686-693, which is why DF-3357 is Medium and this is Low).", "exploit_chain": "unprivileged connect(2) on AF_INET6 SOCK_DGRAM with a 17-byte sockaddr -> getsockaddr kmalloc(17) [24-byte M_SONAME chunk reusing a groomed free chunk] -> udp6_connect reads sin6_addr bytes 8..19 incl. 3 bytes past the allocation at udp6_usrreq.c:597 -> IN6_IS_ADDR_V4MAPPED true/false selected by attacker-groomed stale heap bytes -> true: EADDRNOTAVAIL returned at :598 (short-circuit before in6_pcbconnect); false: falls to in6_pcbconnect -> in6_pcbladdr finally validates sa_len -> EINVAL. Observable errno oracle. No path to state establishment or privilege: validation still rejects; leak-class only.", "evidence": [ "run.log: three independent runs, each A groom 00 ff ff : EADDRNOTAVAIL=20 EINVAL=0 vs B groom 00 00 00 : EADDRNOTAVAIL=0 EINVAL=20 \u2014 variants differ only in heap bytes outside the connect() sockaddr", "udp6_connect_oob.c: PoC source (groom bind(24) + trigger connect(17) on 24-byte shared slab zone)", "VERDICT.md: full code trace (udp6_usrreq.c:596-608), slab mechanics, honest attribution vs DF-3415, impact ceiling", "fix.diff: sa_len validation at top of udp6_connect(), git apply --check passes" ], "kernel_refs": [ "sys/netinet6/udp6_usrreq.c:596", "sys/netinet6/udp6_usrreq.c:597", "sys/netinet6/udp6_usrreq.c:598", "sys/netinet6/udp6_usrreq.c:608", "sys/netinet6/udp6_usrreq.c:686", "sys/netinet6/in6_pcb.c:282", "sys/netinet6/in6_pcb.c:344", "sys/kern/uipc_syscalls.c:1513", "sys/netinet6/in6.h:316" ], "poc_changes": "No seed PoC existed (finding discovered in this pass-2 run). Authored from scratch, reusing the verified DF-3415 groom design (17-byte trigger / 24-byte groom share the 24-byte slab zone; kfree preserves bytes 8..23). Worked first build/run; no source fixes needed.", "attempts": 2, "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", "runtime_sec": 9, "guest_dirty": 0, "build_cmd": "cd /tmp/df3432 && cc -O2 -Wall -o udp6_connect_oob udp6_connect_oob.c", "run_cmd": "su -m maxx -c /tmp/df3432/udp6_connect_oob (x3 runs via run.sh)", "code_hash": "1e461cbca79d4c0a4ed64afc6c68bda123444eac7bdd92eb4b7bb6167ccb3d54", "notes": "attempts=2: first build/run reproduced; second stability run (3x20 rounds) reproduced with 100% stable differential. Guest left clean: PoC files removed from /tmp, no kernel state change, guest stays up. Attribution: for the false branch (variant B) the errno oracle cannot externally separate this findings read (udp6_usrreq.c:597, first) from DF-3415s (in6_pcb.c:344, reached later in the same syscall); for the true branch (variant A) the short-circuit return at :598 means this findings read decided \u2014 certain by trace. The task_log lead about udp6_output.c sharing the DF-3357 pattern was REFUTED: udp6_send validates sa_len==28 and family at udp6_usrreq.c:686-693 BEFORE calling udp6_output, its only caller; udp6_output.c copies sin6 only after that contract (udp6_output.c:166-175, KASSERT at :161). udp6_ctlinputs DF-0630 missing-replymsg is still present in-tree at udp6_usrreq.c:434-435 (fix status: unfixed).", "recommended_fix": "Validate nam->sa_len == sizeof(struct sockaddr_in6) at the top of udp6_connect() before the first sin6 field read (mirror rip6_bind/rip6_connect/in6_pcbbind); see fix.diff.", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff authored (pure precondition insertion, identical in shape to the family fix) and verified to apply cleanly with git apply --check against the read-only sys/ tree; not rebuilt in-guest \u2014 Low-severity errno-oracle leak, and this batchs conditional-verify policy reserves the kernel rebuild/reboot cycle for Critical/High or memcorrupt/privesc findings (same policy recorded for DF-3415). With the patch, every sa_len!=28 connect() returns EINVAL before any address byte is read, eliminating the oracle by construction.", "fix_evidence": "findings/poc/DF-3432/fix.diff (git apply --check passes against sys/)" } |