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)
PoC verification
Evidence pack
findings/poc/DF-0689 · 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
encaptablockless (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:2343caps_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.
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:
-
Reader side — a remote or local packet that walks
encaptab. This requires a tunnel consumer (gif/stf/gre) to be configured so thatencap_attachhas populated the list. Remotely reachable in principle. -
Writer side —
encap_detachfires. Detach happens when the tunnel is torn down viaSIOCDIFPHYADDR, 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_inputaround the LIST walk and the(*psw->pr_input)(...)dispatch (released on every return path); - replaces
crit_enter()/crit_exit()inencap_attach,encap_attach_func, andencap_detachwith the same token (released beforekfreein 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 encaptabbuild.sh/run.sh— no binary PoC (race gated by privileged op)
Fix verification
not_testablecompile 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).
No comments yet.