DF-3433 / verdict.json
{ "finding_id": "DF-3433", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Use-after-free reproduced on the stock INVARIANTS guest (#0). kldunload if_stf (stfmodevent MOD_UNLOAD, if_stf.c:210-224) tears the softc down with no serialization against (a) the lock-free encaptab walk (ip_encap.c:174-215 calls stf_encapcheck on ep->arg) and (b) mbufs already decapsulated by in_stf_input carrying rcvif=&sc->sc_if into the async NETISR_IPV6 queue (if_stf.c:611,629). With stf0 actually configured (2002:0a63:0001::1/16 over a tap v4 anchor) and debug.use_weird_array=1 (kfree poisons freed chunks with 0xdeadc0de, kern_slaballoc.c:1566-1571) the teardown faulted deterministically: Fatal trap 9, cpuid 0, current process Idle (netisr), Stopped at nd6_purge+0xc9 cmpb $0x12,0x1(%rdx) - the if_detach/in6_ifdetach teardown walking memory freed by the concurrent unload/decap interleaving, ~4000 unload cycles / ~24M injected 6to4 frames / ~80s. The poison knob only makes the post-free access visible; on stock kernels the same interleave silently reads/writes freed memory (IFNET_STAT_INC/in6_ifstat_inc write through freed if_afdata). Honest negative included: the bare walk-instant variant (DF-3425s exact gif shape, 79.1M proto-41 floods + 1969 kldunload cycles) did NOT panic - the stf mutator is the ~120ms kldunload only, so the walk window is much narrower than gifs ioctl-speed reconfig churn; the async rcvif vector is the wide, reproducible one. Trigger is root-gated (kldunload); read side is ordinary peer traffic.", "exploit_chain": "not developed: root-gated kldunload + peer flood -> teardown walks freed/poisoned memory (panic demonstrated). Constrained write-after-free primitive exists via in6_ifstat_inc/IFNET_STAT_INC through the freed sc and freed if_afdata; slab-grooming the recycled 2-4KB softc chunk before the teardown walk is the plausible escalation path, not pursued here.", "evidence": [ "panic.txt:45-59: Fatal trap 9: general protection fault, current process: Idle, Stopped at nd6_purge+0xc9: cmpb $0x12,0x1(%rdx) (non-canonical poison pointer, cpuid 0)", "stf_async_race.c: tap-injected matching 6to4 frames (in_stf_input decaps + netisr_queue with rcvif=softc) vs kldunload/kldload+reconfig bounce", "setup_async.sh: stf0 inet6 2002:0a63:0001::1/16 + tap0 10.99.0.1/24 + sysctl debug.use_weird_array=1", "run.log: baseline walk-instant attempt - 79,135,119 floods / 1,969 unload cycles / no panic (documented negative for the narrow sub-vector)", "run.patched.log: identical async harness on the fix.diff kernel - no panic" ], "kernel_refs": [ "sys/net/stf/if_stf.c:210", "sys/net/stf/if_stf.c:214", "sys/net/stf/if_stf.c:222", "sys/net/stf/if_stf.c:250", "sys/net/stf/if_stf.c:611", "sys/net/stf/if_stf.c:626", "sys/net/stf/if_stf.c:629", "sys/netinet/ip_encap.c:174", "sys/netinet/ip_encap.c:180", "sys/netinet/ip_encap.c:427" ], "poc_changes": "No seed (pass-2 discovery). Three harness iterations: (1) bouncer state machine started out-of-sync with the pre-loaded module - made stateless; (2) tap MAC discovery via SIOCGIFADDR returned garbage/zeros (dgram socket AND tap fd) - switched the frame dst MAC to broadcast; (3) the async vector required stf0 to actually MATCH (encapcheck) - added tap0 v4 anchor + 2002:0a63:0001::1/16 per-cycle reconfig and debug.use_weird_array=1 to make the post-free deref fault deterministically.", "attempts": 4, "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": 560, "guest_dirty": 1, "build_cmd": "cc -O2 -pthread -o /tmp/stf_async_race /tmp/stf_async_race.c", "run_cmd": "sh /tmp/setup_async.sh && /tmp/stf_async_race 120", "code_hash": "4e214185b3146e2acbe6dbbcb3bbc11203c48eb95b8e3673ce2994da2e457dc4", "notes": "DF-3425 (gif twin) reproduced the same lock-free-walk defect and validated the mp_token fix; this run proves the stf instance and adds the wider async rcvif vector (queued NETISR_IPV6 mbufs outlive the softc with no refcount). Residual after fix.diff: a full softc refcount (if_ref-style) would be needed to protect queued packets past a down/up transition even with the EBUSY guard removed by an admin who first sets the interface down.", "recommended_fix": "Serialize encaptab mutators with mp_token (walkers already hold it) + refuse MOD_UNLOAD while stf0 is IFF_UP (EBUSY); see fix.diff.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 16:56:02 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Two-kernel validation. Kernel #2 (16:27:37, stf-EBUSY + ip_encap mp_token + state fixes) and the final fix-v3 kernel #1 (16:56:02, same stf/encap parts) both ran the identical async harness: 48.1M injected 6to4 frames with 43,749 kldunload attempts in run 1 and 18.9M frames / 17,617 attempts in the 60s re-check - every unload refused with EBUSY (stf0 configured/UP), unload_cyc=0, no panic, guest stayed up; baseline stock kernel #0 panicked at ~4,000 successful unload cycles (nd6_purge, trap 9). The EBUSY-while-up guard structurally removes the unsynchronized teardown; the mp_token change covers the encap walk for any remaining unload path (interface down first).", "fix_evidence": [ "run.patched.log", "kbuild.tail.log", "fix.diff" ] } |