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)
PoC verification
Evidence pack
findings/poc/DF-3425 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 globalencaptabLIST with no lock, and for each entry callsep->func(m, off, proto, ep->arg)(ip_encap.c:180). For gif entriesfuncisgif_encapcheckandargis thegif_softc *(in_gif.c:378-379).sys/netinet/ip_encap.c:411-426βencap_detach()doesLIST_REMOVE+kfree(ep)with no lock and not even acrit_enter().sys/net/if/gif/if_gif.c:176-203βgif_clone_destroy()(reached fromif_clone_destroy()underifnet_lock()only β sys/net/if_clone.c:104-135 β not the gif ifnet serializer) callsgif_delete_tunnel()(kfreesgif_psrc/gif_pdst, if_gif.c:821-827) andencap_detach()for both cookies, thenkfree(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 acrit_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: routineifconfig 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 ofep->func/ep->psw->pr_inputfrom stale chunk contents, plus laterm_pkthdr.rcvifuse 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
fixedTwo-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']
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
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).
No comments yet.