Rule-chain mutation raced with concurrent ip6_fw_chk: lock-free linked list + kfree after crit_exit -> UAF on SMP
Summary
ip6_fw_chain global singly-linked list consumed LOCK-FREE by ip6_fw_chk(called from ip6_input/ip6_output any CPU). Mutated by add/del/zero/flush/GET with only crit_enter/crit_exit (masks interrupts on CURRENT CPU only, no SMP serialization). del_entry6(:912-916)+FLUSH(:1130-1134) call kfree AFTER crit_exit. IPV6_FW_GET(:1106)+ZERO(:945) walkers hold NO crit. Thread running ip6_fw_chk on CPU A holding chain/f pointers while CPU B frees that rule -> UAF. SKIPTO inner loop(:741-744) derefs chain->next->rule on node whose neighbors just unlinked/freed. No refcount, no token, no RCU. Practical: flood IPv6 pkts during rule edit. Fix: dedicated mutex/token across entire chk iteration AND del+flush kfree; or refcount+deferred free.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0513 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0513.c | trigger-source | races ip6_fw_chk (sender thread) against setsockopt(IPV6_FW_ADD/DEL) (mutator thread) as unpriv user | 5.0 KB | view raw |
| build.sh | build-script | cc -O2 -o df0513 df0513.c -lpthread | 155 B | view raw |
| run.sh | run-script | ./df0513 20 (extend seconds for wider race window) | 534 B | view raw |
| VERDICT.md | verdict | code-path analysis, why-no-panic, fix rationale | 4.5 KB | β raw |
| fix.diff | suggested-fix | introduce lwkt_token ip6_fw_token + hold across chk walk + all mutators; move kfree inside crit | 3.5 KB | view raw |
| run.log | run-log | 20s race output: 21k adds + 21k dels + 28k chk calls, no panic | 397 B | view raw |
| env.txt | environment | uname + cc version | 188 B | 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-0513 β IPv6 firewall chain mutation raced with concurrent ip6_fw_chk
Verdict: NOT-REPRODUCED at runtime (race window too tight on default GENERIC);
code-path CONFIRMED real and exercisable from an unprivileged user.
Mechanism (confirmed at code level)
ip6_fw_chain is a global singly-linked list (sys/net/ip6fw/ip6_fw.c:115)
walked lock-free by ip6_fw_chk() (sys/net/ip6fw/ip6_fw.c:498) on every
IPv6 packet (called from sys/netinet6/ip6_input.c:318 and
sys/netinet6/ip6_output.c:612). The mutators only take
crit_enter()/crit_exit(), which masks interrupts on the current CPU
but does NOT serialize against other CPUs running ip6_fw_chk:
del_entry6(sys/net/ip6fw/ip6_fw.c:902-923):LIST_REMOVE+crit_exit()+kfree(fcp->rule)+kfree(fcp)β kfree happens AFTER crit_exit, so a concurrent walker on another CPU can be derefing the rule while it's being freed.ip6_fw_ctlFLUSH path (sys/net/ip6fw/ip6_fw.c:1126-1135): same kfree-after-crit_exit pattern.zero_entry6(sys/net/ip6fw/ip6_fw.c:944-950) andIPV6_FW_GET(sys/net/ip6fw/ip6_fw.c:1106): walk the chain with no crit at all.- The SKIPTO inner loop (sys/net/ip6fw/ip6_fw.c:741-744) keeps walking
chain->chain.le_next->ruleacross multiple nodes, widening any race window against a concurrent delete.
Secondary observation: NO PRIVILEGE CHECK on the mutator path
ip6_output.c cases IPV6_FW_ADD/DEL/FLUSH/ZERO/GET (sys/netinet6/
ip6_output.c:1534-1551, 1760-1774) dispatch directly to
ip6_fw_ctl_ptr with no priv_check / caps_priv_check_self.
Once the ip6fw module is loaded (one-time root setup, realistic for
any IPv6 router), any unprivileged user can add/del/flush firewall
rules via setsockopt(IPPROTO_IPV6, IPV6_FW_*). The PoC exploits
this to drive the race entirely from the maxx (uid 1001) account.
Reproduction attempt (runtime)
PoC df0513.c runs two threads as the unprivileged maxx user:
- Thread A: opens AF_INET6 SOCK_DGRAM, floods packets to ::1:9999 to
drive
ip6_outputβip6_fw_chk. - Thread B: opens AF_INET6 SOCK_DGRAM, alternates
setsockopt(IPV6_FW_ADD)andsetsockopt(IPV6_FW_DEL)for rule numbers 500..599.
On the buggy kernel:
DF-0513: racing ip6_fw_chk against chain mutation for 20s [sender] sent 0 pkts, errors 28325 <- 28k chk calls [mutator] add=21446 del=21446 err=0 (iters 21446) <- 21k add+21k del cycles
A second run with 4 concurrent adders + 4 senders + 1 flusher for 30 s
drove ~520 k add/del cycles and ~159 k flushes β still no panic. Guest
stayed up; dmesg showed only the (informational) len=X, want 200
messages from earlier probes.
Why no panic despite the race being exercised millions of times:
The window between LIST_REMOVE and kfree is very narrow (a few
instructions), LIST_REMOVE only rewrites the neighbor's next/prev
pointers (leaving the victim's pointers intact), and the freed slab
memory stays mapped and readable (its old contents remain valid until
reused). INVARIANTS slab checks (chunk_mark_free bitmap / WEIRD_ADDR
poison) only trip on the next allocation of the freed chunk, not on a
UAF read by a concurrent walker. Winning the race reliably would need
either CPU pinning (the guest has no cpuset utility) or a tighter
SKIPTO inner-loop pattern β both beyond the scope of a 30 s flood.
Conclusion: code-path confirmed, race real, runtime panic not triggered in the test window. Medium severity is appropriate (the race exists and is unprivileged-reachable, but is not a one-shot reproducible panic).
Fix
fix.diff introduces a lwkt_token ip6_fw_token and takes it across:
- the entire ip6_fw_chk walk (acquire at entry, release before each
return),
- add_entry6, del_entry6, zero_entry6, IPV6_FW_GET, IPV6_FW_FLUSH
mutators (acquire around the crit section).
- Additionally moves kfree(rule) and kfree(fcp) INSIDE the
crit_enter/exit window in del_entry6 and FLUSH so the free is
atomic w.r.t. the local CPU and the token-held walker.
lwkt_token is the DragonFly idiom for this kind of global state β
it's a non-recursive token that serializes all takers across CPUs and
is automatically released/reacquired on msgport switch, so it's safe
to hold across the (non-sleeping) chain walk.
Build / run
ssh dfbsd 'kldload ip6fw' # one-time root setup
ssh dfbsd-maxx 'mkdir -p poc/DF-0513'
scp findings/poc/DF-0513/{df0513.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0513/
ssh dfbsd-maxx 'cd poc/DF-0513 && sh ./build.sh && sh ./run.sh 20'
# watch dmesg / dfbsd-qemu/boot.log for panic signature
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. ip6_fw_chain lock-free walk vs mutation -> UAF race. 500k+ cycles no panic. Also missing priv_check on setsockopt.
No comments yet.