if_stf teardown races its in-flight decap consumers: unguarded MOD_UNLOAD frees the softc under the lock-free encaptab walk and past the async NETISR_IPV6 queue carrying rcvif=&sc->sc_if - use-after-free (DF-3425 gif twin, wider async vector)
Summary
stfmodevent(MOD_UNLOAD) has no in-use guard and no serialization against (a) encap4_input's lock-free encaptab walk calling stf_encapcheck (mutators under crit only - the DF-3425 defect, stf instance), and (b) mbufs already decapsulated by in_stf_input carrying rcvif=&sc->sc_if into netisr_queue(NETISR_IPV6) which ip6_input touches after if_detach freed it. REPRODUCED: Fatal trap 9 (GP on 0xdeadc0de-poisoned freed pointer) at nd6_purge+0xc9 in the if_detach/in6_ifdetach teardown on netisr0, ~80s/~4000 unload cycles/~24M injected 6to4 frames with debug.use_weird_array=1 proving post-free access; the bare walk-instant variant (79.1M floods/1969 cycles) did NOT panic - honest negative on that half, the stf mutator is only kldunload-speed. Root-gated kldunload under peer traffic (same trust boundary as the reproduced DF-3425 twin): freed-memory reads AND writes (IFNET_STAT_INC/in6_ifstat_inc through freed sc/if_afdata) - panic demonstrated; constrained write-after-free primitive (slab-groomable 2-4KB softc chunk) not escalated. Fix validated (EBUSY-while-UP + ip_encap mp_token, two patched kernels): 48M+19M frames, all unloads refused, no panic. M_LENCHECKED hunt for stf was negative (v6 path re-validates plen - not a missed DF-3321 family member).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3433 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| stf_race.c | β | 4.8 KB | view raw | |
| stf_async_race.c | β | 5.7 KB | view raw | |
| setup_async.sh | β | 453 B | view raw | |
| run_async.sh | β | 130 B | view raw | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 130 B | view raw | |
| build.log | β | 496 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| panic.txt | β | 2.3 KB | view raw | |
| env.txt | β | 159 B | view raw | |
| fix.diff | β | 4.2 KB | view raw | |
| run.patched.log | β | 723.1 KB | β download | |
| kbuild.tail.log | β | 5.5 MB | β download | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
Fix verification
fixedTwo-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).
['run.patched.log', 'kbuild.tail.log', 'fix.diff']
Confirmed kernel references
Detail
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 (decisive lines)
["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']
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.
Verified recommended fix
Serialize encaptab mutators with mp_token (walkers already hold it) + refuse MOD_UNLOAD while stf0 is IFF_UP (EBUSY); see fix.diff.
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-3425's 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 gif's ioctl-speed reconfig churn; the async rcvif vector is the wide, reproducible one. Trigger is root-gated (kldunload); read side is ordinary peer traffic.
No comments yet.