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

gif(4) teardown races the lock-free netisr encaptab walk: encap_detach() LIST_REMOVE+kfree vs encap4_input() walk calling gif_encapcheck() on the softc β€” use-after-free panic (reproduced)

Summary

The global encaptab list is walked with no lock on netisr0 by encap4_input() for every inbound IPIP packet, calling gif_encapcheck(softc) for each gif entry. gif_clone_destroy() (under ifnet_lock only, NOT the gif ifnet serializer) and gif_set_tunnel() (crit only) mutate it via encap_attach/encap_detach, which does LIST_REMOVE+kfree with no lock and no crit. A walk concurrent with destroy/reconfig follows freed entries (the free-list linkage lands exactly on the chain pointer at offset 0 of struct encaptab) or calls gif_encapcheck() on a freed softc. REPRODUCED: 'Fatal trap 12 ... Stopped at gif_encapcheck+0x5: testb $0x1,0x6c(%rcx)' - the if_flags read on the freed softc (30 static gifs + destroy/reconfig bounce + raw IPPROTO_IPV4 flood, ~240s). Fix validated in-guest across two iterations (attempt 1's nested-token approach experimentally panics 'illegal release'; final fix takes mp_token in the three mutators - the walkers already hold it): patched kernel survived 68.5M-flood/156k-destroy and 25.6M/66.5k runs with zero incidents. Free side root-gated; walk side is ordinary packet traffic. Distinct from DF-3414/3405/3407. Root cause in ip_encap.c; filed under if_gif.c batch.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3425 Β· 15 files
FileTypeDescriptionSize
gif_race.c β€” 4.6 KB view raw
build.sh β€” 131 B view raw
run.sh β€” 117 B view raw
build.log β€” 214 B view raw
run.log β€” 338 B view raw
panic.txt β€” 671 B view raw
env.txt β€” 359 B view raw
fix.diff β€” 1.6 KB view raw
fix.attempt1.failed.txt β€” 1.1 KB view raw
kbuild.tail.log β€” 843 B view raw
run.patched.log β€” 1.7 KB view raw
run.patched2.log β€” 224 B view raw
code_hash.txt β€” 77 B view raw
VERDICT.md β€” 6.0 KB ↓ raw
verdict.json β€” 3.8 KB view raw
VERDICT.md
↓ download raw

DF-3425 VERDICT

Status: REPRODUCED (kernel panic from use-after-free; race won within the first 240 s run; 1 attempt)

The bug (code trace)

  • sys/netinet/ip_encap.c:174-215 β€” encap4_input(), running on NETISR(0) for every inbound IPIP/proto-4 packet, walks the global encaptab LIST with no lock, and for each entry calls ep->func(m, off, proto, ep->arg) (ip_encap.c:180). For gif entries func is gif_encapcheck and arg is the gif_softc * (in_gif.c:378-379).
  • sys/netinet/ip_encap.c:411-426 β€” encap_detach() does LIST_REMOVE + kfree(ep) with no lock and not even a crit_enter().
  • sys/net/if/gif/if_gif.c:176-203 β€” gif_clone_destroy() (reached from if_clone_destroy() under ifnet_lock() only β€” sys/net/if_clone.c:104-135 β€” not the gif ifnet serializer) calls gif_delete_tunnel() (kfrees gif_psrc/gif_pdst, if_gif.c:821-827) and encap_detach() for both cookies, then kfree(sc) (if_gif.c:200).
  • sys/net/gif/if_gif.c:744-758 β€” gif_set_tunnel() likewise detaches and re-attaches encap entries on reconfiguration under only a crit_enter().

Net effect: a gif being destroyed/reconfigured (root: ifconfig gifN destroy, ifconfig gifN tunnel A B, or module unload) races the netisr0 walk: - the walk can LIST_NEXT() a freed entry (kmalloc free-list linkage lands exactly on chain.tqe_next at offset 0 of struct encaptab, sys/netinet/ip_encap.h:57-58), or - fetch ep->arg (softc) just before the entry is freed and call gif_encapcheck() on a freed gif_softc.

Reproduction (this run)

Harness gif_race.c (root): - 30 static gif tunnels (lengthens the walk), - thread "destroyer": tight SIOCIFCREATE gif50 + SIOCSIFPHYADDR + SIOCIFDESTROY gif50 loop (each cycle: encap_attach_func, encap_detach kfree, gif_delete_tunnel kfree, kfree(sc)), - thread "reconfig": tight SIOCSIFPHYADDR alternation on gif51 (encap detach/attach churn under crit only), - 4 flood threads: raw IPPROTO_IPV4 sockets sendto 127.0.0.1, delivering a continuous IPIP stream into ip_input -> encap4_input() on netisr0.

Result within the 240 s window β€” guest panicked (serial console, panic.txt):

Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address = 0xfffff8011861306c
fault code = supervisor read data, page not present
Stopped at      gif_encapcheck+0x5:  testb $0x1,0x6c(%rcx)

gif_encapcheck+0x5 is the first instruction of if ((sc->gif_if.if_flags & IFF_UP) == 0) (if_gif.c:279) β€” the freed softc deref; cpuid = 0 / current process = Idle = the netisr0 walk context. This is the use-after-free read exactly as traced.

Exploitability

  • Trigger side (flood + walk) is unprivileged-triggerable packet traffic; the free side (destroy/reconfig ioctls) requires root (sys/net/if.c:2335-2343 SYSCAP_RESTRICTEDROOT). The race is therefore a privileged-teardown hazard: routine ifconfig gifN destroy / reconfiguration under traffic load can panic or corrupt the kernel.
  • Primitive: UAF read of a kfree'd struct encaptab (walk continues through free-list linkage into arbitrary same-zone chunks) and call-through of ep->func/ep->psw->pr_input from stale chunk contents, plus later m_pkthdr.rcvif use of the freed softc in the requeued inner packet. Reliable code execution was NOT developed: the free side is root-gated, the chunk-recycling window is sub-microsecond, and M_IPENCAP/M_GIF chunks are recycled by the harness's own loop. Impact recorded as panic (DoS); classification as a UAF (memcorrupt family) is appropriate for triage.

Fix validation (two iterations)

Attempt 1 β€” a dedicated encap_token taken by both the walks and the mutators β€” was rejected by experiment: the patched kernel itself panicked with lwkt_reltoken: illegal release because netisr transport processing (transport_processing_oncpu β†’ encap4_input) already runs holding mp_token, and releasing a second nested token there violates LWKT tokref ordering (fix.attempt1.failed.txt).

Attempt 2 (final fix.diff) β€” lwkt_gettoken(&mp_token) ONLY in the three mutators (encap_attach, encap_attach_func, encap_detach); the walks already run under mp_token implicitly, so mutation and walk are now serialized without nesting a new token inside netisr. Validated on the guest: built clean (0 errors), installed as kernel #1 2026-09-12 13:26:06, and the identical harness ran twice without incident β€” - 300 s: flood=68,491,080 cyc=156,307 recfg=6,661,618 err=0 β†’ done (no panic), guest up; - 120 s confirmation: flood=25,621,720 cyc=66,516 recfg=2,656,303 err=0.

Baseline stock kernel #0 panicked in the first 240 s run (gif_encapcheck+0x5). Statistical caveat: a race can never be proven absent, but the patched runs exercised ~6x the event volume that sufficed to kill the baseline, and the mp_token argument holds by construction (proven by attempt 1's own assertion trace showing the walker holds mp_token).

Known residual (documented, not fixed here): a packet already requeued via netisr_queue inside the walk carries rcvif = softc and can be touched after the walk completes; a full fix needs softc reference counting (compare FreeBSD's if_ref/encap refcount rework).

Line-accurate references

  • ip_encap.c:174-215 walk; :180 func call; :217-227 dispatch
  • ip_encap.c:411-426 encap_detach (LIST_REMOVE+kfree, no lock)
  • ip_encap.c:329/386 crit_enter is per-CPU only β€” no cross-CPU exclusion
  • if_gif.c:176-203 gif_clone_destroy; :200 kfree(sc)
  • if_gif.c:814-843 gif_delete_tunnel; :821-827 kfree(gif_psrc/pdst)
  • if_gif.c:716 crit_enter (no cross-CPU serialization for set_tunnel churn)
  • if_clone.c:104-135 ifnet_lock-only destroy path
  • if.c:2335-2343 root gating of the tunnel ioctls

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

Two-iteration validation. Attempt 1 (dedicated lwkt_token taken by walkers AND mutators) was REJECTED: the patched kernel itself panicked (lwkt_reltoken: illegal release) because netisr transport processing already runs under mp_token and a second nested token released there violates LWKT tokref ordering (fix.attempt1.failed.txt). Attempt 2 (final fix.diff: mp_token acquired ONLY in encap_attach/encap_attach_func/encap_detach; the encap4/6_input walks already run under mp_token via transport_processing_oncpu) built clean (0 errors), installed as kernel #1 2026-09-12 13:26:06, and survived the identical harness twice: 300s run (68.5M flood packets, 156,307 destroy cycles, 6.66M reconfig cycles, err=0, done-no-panic, guest up) and a 120s confirmation (25.6M floods, 66.5k cycles). Baseline stock kernel #0 panicked in the first 240s run at gif_encapcheck+0x5 on the freed softc. Statistical caveat noted; the token argument holds by construction.

['fix.diff', 'kbuild.tail.log', 'run.patched.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 13:26:06 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["panic.txt: 'Fatal trap 12: page fault' + 'Stopped at gif_encapcheck+0x5: testb $0x1,0x6c(%rcx)' = sc->gif_if.if_flags read (if_gif.c:279) on freed softc, cpuid=0 = the encap4_input netisr0 walk", 'gif_race.c: 30 static gifs (walk length), gif50 create/set/destroy bounce, gif51 SIOCSIFPHYADDR reconfig loop, 4 raw IPIP floods to 127.0.0.1', 'run.patched.log: identical harness on fix.diff-built kernel completes 300s with no panic']

PoC changes

No seed existed (pass-2 discovery). Harness iterated once for a missing include (struct in_aliasreq for SIOCSIFPHYADDR).

Verified recommended fix

Serialize encaptab walk vs mutators with a dedicated lwkt_token (encap4_input/encap6_input/encap_attach/encap_attach_func/encap_detach); see fix.diff.

Verdict

Use-after-free panic reproduced on the stock INVARIANTS guest: encap4_input() walks the lock-free global encaptab on netisr0 (ip_encap.c:174-215) while a root thread runs gif_clone_destroy()/gif_set_tunnel() churn, whose encap_detach() does LIST_REMOVE+kfree with no lock (ip_encap.c:411-426) and whose teardown kfrees the gif_softc (if_gif.c:200). The walk fetched ep->arg and called gif_encapcheck() on the freed softc: Fatal trap 12, Stopped at gif_encapcheck+0x5: testb $0x1,0x6c(%rcx), cpuid=0 (netisr0). Free side is root-gated (if.c:2335-2343); the read side is ordinary packet traffic, so the hazard is privileged gif teardown/reconfig under load - panic/DoS demonstrated, code-exec not developed (sub-microsecond chunk-recycle window).