DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0689

Unsynchronized encaptab list yields use-after-free in encap4_input/encap6_input dispatch

Summary

Global encaptab LIST mutated by encap_attach/attach_func/detach under crit_enter() only (:329,:386) which on DragonFlyBSD is per-CPU interrupt-deferral does NOT serialize other CPUs. encap4_input (:174-223) and encap6_input (:268-298) walk same list take NO lock deref match->psw (:219,:295) match->arg via encap_fillarg (:221,:297) indirect-call (*psw->pr_input)(:223,:298). encap_detach (:417-422) LIST_REMOVE+kfree (:419-420 authors own /*XXX*/ caveat) nothing prevents concurrent reader holding stale pointer. Two concurrent encap_attach also race LIST_INSERT_HEAD :314 corrupt list head. Attacker: unauth remote sends sustained IPPROTO_IPV4(proto4)/IPPROTO_IPV6-in-IPv6(proto41) to host with gif/stf/gre tunnel configured + privileged op (root SYSCAP_RESTRICTEDROOT if.c:2343 SIOCDIFPHYADDR) triggers detach during reader window. Remote packets widen race window arbitrarily. Impact: kernel heap UAF M_IPENCAP slab typical NULL/stale fn-ptr indirect call :223/:298 panic A:H with slab grooming controlled fn-ptr C:L/I:L. Required: tunnel consumer (gif/stf/gre default GENERIC). Fix: lwkt_token across input lookup+dispatch AND attach/detach or refcount on encaptab.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0689 · 7 files
FileTypeDescriptionSize
README.md readme race analysis + root-only recipe 1.6 KB ↓ raw
VERDICT.md verdict this analysis 3.3 KB ↓ raw
build.sh build-script no-op (no binary PoC) 237 B view raw
run.sh run-script no-op 253 B view raw
fix.diff suggested-fix lwkt_token across encaptab walk + mutation 2.5 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme race analysis + root-only recipe
↓ download raw

DF-0689 — encaptab list UAF race (sys/netinet/ip_encap.c)

Status: NOT REPRODUCED (root-only writer side)

The race is structurally real but cannot be triggered by an unprivileged local user:

  • Reader (encap4_input / encap6_input): walks encaptab lockless (ip_encap.c:174, :268). Reachable by anyone who can send IP/IPv6 to the host (so remotely reachable in principle).
  • Writer (encap_detach): requires root-only SIOCDIFPHYADDR (sys/net/if.c:2343 caps_priv_check(cred, SYSCAP_RESTRICTEDROOT)).
  • An unprivileged local user cannot trigger the writer side at all, so they cannot open the race window. The race requires concurrent privileged admin (tearing down a gif/stf/gre) while a remote party floods the tunnel.

Per the audit's realistic-threat test, root→kernel races are game-over (root can already do anything). The bug is a hardening gap, not an unprivileged escalation. We document and provide a fix.diff.

How to (try to) reproduce as root

kldload if_gif.ko
ifconfig gif0 create
ifconfig gif0 tunnel 127.0.0.1 127.0.0.2
# Flooder (separate host or loopback):
#   ping -f 127.0.0.1   # to keep packets flowing into encap4_input
# Concurrently as root, repeatedly:
ifconfig gif0 destroy    # triggers encap_detach during reader walk

The race window is narrow; even on SMP it may need many iterations.

Fix

fix.diff adds a global lwkt_token held across the encaptab walk and dispatch in encap4_input / encap6_input, and across the LIST_INSERT_HEAD / LIST_REMOVE in encap_add / encap_detach. This makes the reader/writer mutually exclusive without changing the data structure.

VERDICT.md verdict this analysis
↓ download raw

DF-0689 — encaptab list UAF race (sys/netinet/ip_encap.c)

Verdict: NOT REPRODUCED (race is structurally real but root-gated on the writer side)

Mechanism

encaptab is a global LIST_HEAD (ip_encap.c:102) walked without a lock by encap4_input (line 174) and encap6_input (line 268). Mutators (encap_attach :329, encap_attach_func :386, encap_detach :412) only hold crit_enter() — per-CPU interrupt-deferral, which does not serialize other CPUs. encap_detach does:

LIST_REMOVE(p, chain);
kfree(p, M_IPENCAP);    /* XXX */                /* line 419-420 */

with no synchronize-with-reader. A concurrent encap4_input / encap6_input that has already loaded ep (or match->psw at line 219/295, or match->arg via encap_fillarg at line 221/297) and is about to dereference (*psw->pr_input)(...) at line 223/298 will read freed memory and call a stale function pointer → panic at A:H, or with slab grooming a controlled indirect call.

Why it doesn't reproduce on this guest

The race window opens only when both of these are concurrent:

  1. Reader side — a remote or local packet that walks encaptab. This requires a tunnel consumer (gif / stf / gre) to be configured so that encap_attach has populated the list. Remotely reachable in principle.

  2. Writer sideencap_detach fires. Detach happens when the tunnel is torn down via SIOCDIFPHYADDR, which is gated by: c case SIOCDIFPHYADDR: /* sys/net/if.c:2336 */ error = caps_priv_check(cred, SYSCAP_RESTRICTEDROOT); /* :2343 */ i.e. root only. An unprivileged local user cannot trigger the writer side at all, so they cannot open the race window.

Per the audit's realistic-threat test, a race that needs root on one side is a root→kernel hardening gap (root can already do anything), not an unprivileged escalation. The finding's likely confidence is reasonable for the structural claim, but its exploitability for a non-root user is nil without a separate root bug.

Reachability attempt

I did not attempt a multi-CPU race demonstration because: - The writer side requires caps_priv_check(SYSCAP_RESTRICTEDROOT). - A successful hit demonstrates root→kernel (game-over by definition). - A non-root trigger does not exist in the cited path.

The bug is therefore documented as a hardening gap with a fix.diff that adds proper mutual exclusion.

Fix

fix.diff introduces a static lwkt_token (encaptab_token) and:

  • acquires it in encap4_input / encap6_input around the LIST walk and the (*psw->pr_input)(...) dispatch (released on every return path);
  • replaces crit_enter()/crit_exit() in encap_attach, encap_attach_func, and encap_detach with the same token (released before kfree in detach so we don't hold it during the allocator).

lwkt_token is the DragonFly idiom for serializing a shared data structure across CPUs without the cost of a mutex; it is the correct synchronization primitive here. The diff applies cleanly (patch -p1 --dry-run succeeds).

Files

  • README.md — root-only race recipe (gif + flood + destroy)
  • fix.diff — lwkt_token serialization of encaptab
  • build.sh / run.sh — no binary PoC (race gated by privileged op)

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

Source-confirmed. encaptab lockless walk vs detach under crit_enter. Root-only (SYSCAP_RESTRICTEDROOT).