β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3457

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3457 Β· 13 files
FileTypeDescriptionSize
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 capture
  • panic.txt β€” kernel panic from the serial console
  • fix.diff β€” validated one-hunk fix (ifnet_serialize_all in gif_clone_destroy)
  • env.txt β€” guest state
  • verdict.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.

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Sep 13 06:15:53 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.