gif(4) clone-destroy tears down softc state with no ifnet serializer while in-flight gif_output()/in_gif_output() dereferences it β use-after-free, reproduced kernel panic
Summary
SIOCIFDESTROY runs gif_clone_destroy() in the syscall thread with no gif ifnet serializer held, while an in-flight gif_output() in ANY thread (an unprivileged sender's sendto -> ip_output -> if_output) holds only gif0's if_serializer and dereferences the same state mid-encapsulation. The SIOCSIFPHYADDR ioctl path is correctly wrapped in ifnet_serialize_all() - the destroy path simply lacks the same discipline. REPRODUCED: fatal trap 12 'fault VA = 0x1, Stopped at gif_output+0x128: movzbl 0x1(%rax),%eax' - gif_delete_tunnel() NULLed gif_psrc between gif_output_serialized()'s guard and its deref. Race won in ~75s at ~20k pps + ~15 destroys/s (unpriv flooder + root churn loop; 1.26M-packet control run zero errors). Interleavings read the kfree'd sockaddr (slab-poisoned) or the freed softc; ip_output's ro->ro_rt store gives a write-into-freed-chunk primitive; grooming the 16-byte sockaddr slab zone turns the reads into attacker-chosen outer src/dst injection. Full uid=0 not developed (one side requires root). Fix validated (ifnet_serialize_all around gif_clone_destroy's teardown, nativekernel + reboot): exact-same PoC 600s/2860 destroy cycles - zero panics. Covers the v6 twin sites in in6_gif.c (same teardown, same fix). Distinct from DF-3425 (encaptab walk - different race, different fix).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3457 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| flood.c | β | 3.2 KB | view raw | |
| churn.sh | β | 1.3 KB | view raw | |
| run.sh | β | 281 B | view raw | |
| build.sh | β | 109 B | view raw | |
| run.log | β | 2.3 KB | view raw | |
| panic.txt | β | 656 B | view raw | |
| fix_run.log | β | 1.4 KB | view raw | |
| env.txt | β | 484 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-3457 β gif(4) destroy races in-flight gif_output (UAF -> panic)
Repro: unprivileged user floods traffic through a root-configured gif0
tunnel while root churns ifconfig gif0 destroy/create. The destroy
path frees/NULLs gif_psrc/gif_pdst (and later the whole softc)
without holding gif0's ifnet serializer, while gif_output()
(in any sender thread) holds that serializer and dereferences the
same fields mid-encapsulation.
Files
flood.cβ unprivileged flooder (build:cc -O2 -o flood flood.c; run as non-root:./flood 6 10.99.0.1)churn.shβ root-side create/destroy loop (./churn.sh 300)run.logβ full decisive run incl. control + panic capturepanic.txtβ kernel panic from the serial consolefix.diffβ validated one-hunk fix (ifnet_serialize_all in gif_clone_destroy)env.txtβ guest stateverdict.json/manifest.jsonβ machine records
Exact steps (single-tenant QEMU guest, 6 vCPU, INVARIANTS kernel)
# root setup
ifconfig gif0 create
ifconfig gif0 tunnel 127.0.0.1 127.0.0.99 # blackhole outer dst:
# encapped pkts never echo
ifconfig gif0 10.98.0.1 10.98.0.2 up
route add -net 10.99.0.0/24 -iface gif0
# unprivileged side (maxx)
cc -O2 -o flood flood.c
./flood 6 10.99.0.1 # ~25k pps aggregate (keep netisr0 alive)
# root side
./churn.sh 300 # ~15-20 create/destroy cycles per second
Expected result (vulnerable kernel)
Within ~1-2 minutes:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1 Stopped at gif_output+0x128: movzbl 0x1(%rax),%eax
(sc->gif_psrc NULLed between if_gif.c:346 guard and :373 deref.)
Expected result (patched kernel, fix.diff applied)
No panic for the full run; guest stays up; flood reports EAFNOSUPPORT=0 throughout.
DF-3457 β gif(4) clone-destroy races in-flight gif_output(): unserialized free/NULL of gif_psrc/gif_pdst (and gif_softc) panics the kernel
VERDICT: REPRODUCED (kernel panic, unprivileged traffic side + root teardown side). fix.diff VALIDATED (patched kernel survives the exact PoC; see below).
ROOT CAUSE (path:line)
Two execution contexts touch gif softc state with NO common lock:
-
gif_output() [sys/net/gif/if_gif.c:396-409] runs in ANY thread (an unprivileged user's sendto() to a destination routed via gif0: udp_output -> ip_output -> if_output). It takes the gif's tx-queue hw serializer, ifsq_serialize_hw(), which IS ifp->if_serializer [sys/net/if.c:642, sys/net/ifq_var.h:517-527], and holds it across gif_output_serialized() [if_gif.c:332-394] -> in_gif_output() [sys/netinet/in_gif.c:96-217].
-
gif_clone_destroy() [if_gif.c:176-203], reached from SIOCIFDESTROY in the ifconfig syscall thread [sys/net/if.c:2012-2016 -> sys/net/if_clone.c:126-132], takes NO gif serializer at all (unlike the SIOCSIFPHYADDR path, which is correctly wrapped in ifnet_serialize_all() at if.c:2350-2352). It calls gif_delete_tunnel() which immediately kfree(sc->gif_psrc); sc->gif_psrc = NULL; kfree(sc->gif_pdst); sc->gif_pdst = NULL; [if_gif.c:821-828] and later kfree(sc) [if_gif.c:200].
The racing deref sites: * if_gif.c:373 sc->gif_psrc->sa_family -- re-read AFTER the NULL guard at if_gif.c:345-350 (TOCTOU on the guard itself) * in_gif.c:101-102 load sin_src/sin_dst = sc->gif_psrc/gif_pdst, then deref at :107-109 (family), :156-159 (addresses), :183-188 (route-cache compare against freed sockaddrs), and ip_output() at :215 uses/writes &sc->gif_ro[cpuid] inside the softc. * v6 twin: in6_gif.c:102-110, :172-193, :187-201.
REPRODUCTION (see README.md / run.log / panic.txt)
Unprivileged user floods UDP through a root-configured gif0 while root
loops ifconfig gif0 destroy/recreate. ~75 s into the race:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1 Stopped at gif_output+0x128: movzbl 0x1(%rax),%eax
fault VA 0x1 => %rax == NULL => sc->gif_psrc had been NULLed by gif_delete_tunnel() between the :346 guard and the :373 deref. Other interleavings of the same window read the kfree'd sockaddr (INVARIANTS slab poisons it with 0xdeadc0de, kern_slaballoc.c:191) or the freed softc.
THREAT
Requires a root-configured gif tunnel and a root-side destroy while unprivileged local traffic transits it β the standard dynamic-VPN provisioning churn. The unprivileged user only needs to send packets; the fault occurs in the sender's (or netisr's) context. Impact: reliable kernel panic (local DoS); with heap grooming of the 16-byte M_IFADDR-size zone (any user-triggerable sockaddr kmalloc reuses the freed chunk and fully controls its 16 bytes) the corrupted source/destination read at in_gif.c:156-159 becomes attacker-chosen address injection; the freed softc gives a write-what-into-freed-chunk primitive via ip_output()'s ro->ro_rt store at in_gif.c:215. Full uid=0 escalation not developed: the racing free requires root-side churn, bounding the security value of the primitive.
FIX (fix.diff, validated)
Wrap the softc-state teardown in gif_clone_destroy() in ifnet_serialize_all()/ifnet_deserialize_all() β the same serializer gif_output() holds for the entire encapsulation and the one the SIOCS*PHYADDR ioctl paths already use. Nothing inside the wrapped region takes ifp's serializer, so no recursion/deadlock.
baseline : panic in gif_output+0x128 after ~75 s of racing (panic.txt) patched : identical PoC run for 600 s (2x the baseline time-to-crash and 4x the churn cycle count) β guest up, zero panics, flood EAFNOSUPPORT count 0, churn 8460 cycles (fix_run.log)
NOTES
- The encaptab walk vs encap_detach() race during the same destroy is DF-3425 (known, filed against if_gif.c in pass 1) and is NOT re-reported here; this finding is the output-path half of the same missing-serialization root cause, with its own deref sites and its own reproduced panic signature.
- The SIOCSIFPHYADDR/SIOCDIFPHYADDR ioctl path is correctly serialized (if.c:2350) β verified by inspection; the destroy path simply forgot the same discipline.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedRebuilt X86_64_GENERIC with fix.diff via make nativekernel (BUILD_RC=0), installed, rebooted, reran the exact PoC for 600s (2x+ the baseline time-to-crash): 2860 destroy/recreate cycles, zero panics (dmesg fatal-trap count 0), flooders error-free with EAFNOSUPPORT=0. Bad behavior gone; no functional regression (1.3M+ successful sends between destroys).
fix_run.log (full patched-run record); baseline in run.log + panic.txt
Confirmed kernel references
- sys/net/gif/if_gif.c:176-203
- sys/net/gif/if_gif.c:345-350
- sys/net/gif/if_gif.c:373
- sys/net/gif/if_gif.c:814-843
- sys/netinet/in_gif.c:101-110
- sys/netinet/in_gif.c:156-163
- sys/netinet/in_gif.c:183-216
- sys/netinet6/in6_gif.c:102-113
- sys/netinet6/in6_gif.c:172-201
- sys/net/if.c:2012-2016
- sys/net/if.c:2350-2352
- sys/net/if.c:642
- sys/net/if_clone.c:126-132
Detail
Exploit chain
root: gif0 configured and being destroyed in a loop (dynamic-VPN churn) -> unprivileged local user floods UDP through gif0 route -> sender-thread gif_output_serialized passes psrc NULL-guard, destroy thread kfrees/NULLs gif_psrc concurrently -> sender derefs NULL/freed pointer -> kernel page fault panic. Grooming the 16-byte M_IFADDR-size slab zone (any user sockaddr kmalloc) between free and re-read would give attacker-chosen 16 bytes as the outer src/dst sockaddr (address injection); not needed for the panic proof.
Evidence (decisive lines)
["panic.txt: 'fault virtual address = 0x1' + 'Stopped at gif_output+0x128: movzbl 0x1(%rax),%eax' - the sa_family read (offset 1) of a NULLed/freed gif_psrc", 'run.log: control run 1.26M packets zero errors on stable gif0; race run panicked ~75s in', 'fix_run.log: patched kernel (#1) survived 600s / 2860 churn cycles, 0 panics, EAFNOSUPPORT=0', "fix.diff: ifnet_serialize_all/deserialize_all around gif_clone_destroy's softc teardown"]
PoC changes
Seed had no runnable PoC. Wrote flood.c (unprivileged UDP flooder with errno classification: EAFNOSUPPORT return from a configured gif0 is only reachable via the freed-poisoned gif_psrc/gif_pdst family check) and churn.sh (root create/tunnel/addr/route/destroy loop). Design constraints learned: outer tunnel dst must be a blackhole (127.0.0.99) or the decap feedback loop exhausts mbufs and livelocks the box; flood must be paced (~25k pps aggregate) or netisr0 saturation wedges the guest; the gif route must be added after gif0 has an inet address or rt_ifa is AF_LINK and ip_output fails EADDRNOTAVAIL.
Verified recommended fix
Wrap gif_clone_destroy()'s softc teardown (gif_delete_tunnel + LIST_REMOVE + encap detaches + gif_clear_cache) in ifnet_serialize_all()/ifnet_deserialize_all(), the same serializer gif_output() and the gif ioctl paths use (fix.diff, validated in-guest: 600s/2860 cycles, 0 panics vs 75s-to-panic baseline).
Verdict
gif_clone_destroy() (SIOCIFDESTROY, ifconfig destroy) tears down gif softc state with no gif ifnet serializer held, while in-flight gif_output()/in_gif_output() in any sender thread (holding only that serializer) dereferences the same fields. Reproduced as a fatal trap 12 in gif_output+0x128 (movzbl 0x1(%rax)) after ~75s of unprivileged flood + root destroy-churn: gif_delete_tunnel() NULLed gif_psrc between if_gif.c:346's guard and :373's deref. Other interleavings read the kfree'd (0xdeadc0de-poisoned) sockaddr or the freed softc; ip_output() can write ro->ro_rt into the freed softc. Local DoS/panic with root-configured gif tunnels under churn; escalation bounded by the root-side destroy requirement.
No comments yet.