DF-3307 / verdict.json
{ "finding_id": "DF-3307", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Reproduced on the QEMU guest (DragonFly 6.5-DEVELOPMENT). ip6_output() caches exthdrs.ip6e_hbh/dest1/rthdr pointers into the packet chain before pfil_run_hooks() and, after the hooks legally replace the chain, the fragmentation path both READS nextproto and WRITES IPPROTO_FRAGMENT through those stale pointers (ip6_output.c:778-790) into freed memory. With a demo PFIL_OUT hook that deep-copies and frees the original chain, the emitted fragments carry hbh.nxt=17 instead of 44 (tcpdump hex 0x11 vs 0x2c at byte 40), the fragment header is unreachable, every receiver parses a bogus UDP header, and the datagram is never delivered; a passthrough control run on the same kernel shows nxt=44 and delivery, isolating the mbuf replacement as the only variable. The underlying primitive is a 1-byte write-after-free into the mbuf-cluster slab plus a 1-byte stale read that becomes the wire protocol byte of every fragment (kernel heap corruption class); the reliable demonstrated impact is silent packet corruption (dos). Trigger is root-gated (hbh option privilege check) plus a chain-replacing pfil hook, hence severity Low, not an unprivileged escalation.", "exploit_chain": "root UDP6 socket -> sendmsg(::1, 5000B payload, 256B IPV6_HOPOPTS cmsg) with lo0 MTU lowered to 1500 -> ip6_output chains hbh, runs PFIL_OUT hook -> hook deep-copies chain, m_freem(old), returns copy with rv=0 (legal pfil API) -> fragmentation path dereferences freed exthdrs.ip6e_hbh: stale read feeds ip6f_nxt, stale write of 0x2c lands in freed cluster -> emitted packet structurally corrupt; freed cluster is in the reusable cluster pool (heap corruption primitive). Unprivileged escalation: not available (options privilege-gated).", "evidence": [ "run.log: vulnerable kernel TEST run, tcpdump -X shows byte 40 = 0x11 (17) and truncated-udplength 3 misparse", "run.2.log: CONTROL run (hook passthrough) and FIXED run both show byte 40 = 0x2c (44)", "VERDICT.md: full A/B/C matrix and kernel reference lines", "instr-ip6_input.c: the demo PFIL_OUT hook compiled into the test kernels (pfildemo_* sysctls)", "fix.diff: verified fix re-locating the next-header field via m_getptr()" ], "kernel_refs": [ "sys/netinet6/ip6_output.c:164", "sys/netinet6/ip6_output.c:658", "sys/netinet6/ip6_output.c:662", "sys/netinet6/ip6_output.c:778", "sys/netinet6/ip6_output.c:785", "sys/netinet6/ip6_output.c:786", "sys/net/pfil.c:pfil_run_hooks" ], "poc_changes": "Original sketch assumed a loadable kld pfil hook; kldloading ANY freshly built module panics this guests linker (strcmp GPF), so the hook was compiled into instrumented quickkernel builds instead. Trigger was adapted twice for host-side caps the sketch ignored: sendmsg ancillary data is capped at MLEN (uipc_syscalls.c:935-936 -> hbh shrunk from 2048 to 256 bytes) and UDP6 sends are size-capped (EMSGSIZE at 60000B -> payload 5000B); lo0 MTU lowered to 1500 to force fragmentation; ip6h_len arithmetic fixed ((len+1)*8). A cluster-reclaim canary for direct read-proof stayed inconclusive (cluster cache did not return the hbh cluster in-window); wire-level A/B/C used as decisive proof instead.", "attempts": 8, "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 behavior reproduced on rebuilt kernels #5/#6 of the same source; fix validated on #7: Thu Sep 10 12:19:45 UTC 2026)", "runtime_sec": 4500, "guest_dirty": 0, "build_cmd": "cp instr-ip6_input.c /usr/src/sys/netinet6/ip6_input.c && cd /usr/src && make quickkernel KERNCONF=X86_64_GENERIC -DNO_WERROR -j6 && make installkernel KERNCONF=X86_64_GENERIC && cc -O2 -o trigger trigger.c", "run_cmd": "ifconfig lo0 mtu 1500; tcpdump -i lo0 -U -w cap.pcap ip6 & sleep 1; sysctl net.inet6.ip6.pfildemo_enable=1; ./trigger; sysctl net.inet6.ip6.pfildemo_replaced; pkill tcpdump", "code_hash": "5d4189260648ad263c359d99a7e59faae6a7618d3f45aef32edb2a5562743bf3", "notes": "Guest was reset to the clean-source snapshot after verification (guest_dirty=0). The demo hook is PoC instrumentation only and is NOT part of fix.diff. kld route on this image is broken for freshly-built modules (see VERDICT.md kld note); stock /boot/kernel modules load fine.", "recommended_fix": "After pfil_run_hooks, re-locate the last unfragmentable extension headers next-header byte in the current mbuf chain (m_getptr walk) instead of dereferencing the pre-hook exthdrs[] pointers; see fix.diff.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #7: Thu Sep 10 12:19:45 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Applied fix.diff to the guest /usr/src copy, quickkernel+installkernel, rebooted, re-ran the exact TEST (pfildemo_enable=1, chain replaced, pfildemo_replaced=1): hbh.nxt = 0x2c (44) on the wire and the 5000-byte datagram was delivered - the previously-observed corruption is gone with no behavioral regression in the control run.", "fix_evidence": "run.2.log FIXED section (tcpdump byte-40 = 2c, receiver: got 5000 bytes); env.txt records kernel #7 build/install." } |