nd6_sysctl_drlist/prlist iterate defrouter/prefix lists without nd6_mtx: UAF race with RA processing
Summary
nd6_sysctl_drlist(:2168-2169) and nd6_sysctl_prlist(:2209-2254) iterate global lists WITHOUT nd6_mtx. RA processing (nd6_rtr.c defrtrlist_del/update/prelist_remove) modifies same lists under nd6_mtx on netisr0. Multi-CPU: sysctl handler derefs freed entry mid-iteration. Contrast: ioctl SIOCGDRLST(:1441) correctly holds mtx. Remote: RA flood + unpriv sysctl read -> UAF panic/info leak.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0355 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | wrap drlist/prlist sysctl iterations in mtx_lock/unlock(&nd6_mtx) | 662 B | view raw |
| VERDICT.md | verdict | lock-order violation confirmed by source; race not won in test | 3.6 KB | β raw |
| env.txt | environment | guest uname, sysctls | 302 B | view raw |
| README.md | readme | human reproduce doc | 1.3 KB | β 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-0355 PoC β nd6_sysctl_drlist/prlist lockless iteration
Status: INCONCLUSIVE β code-level bug confirmed; race not won in test. Severity: Medium
Bug
The sysctl handlers nd6_sysctl_drlist (sys/netinet6/nd6.c:2168) and
nd6_sysctl_prlist (line 2209) walk the global nd_defrouter TAILQ
and nd_prefix list without taking nd6_mtx. The ioctl path
SIOCGDRLST_IN6 (line 1441) and SIOCGPRLST_IN6 (line 1473) DOES
hold nd6_mtx over the same iteration. RA processing
(defrtrlist_update etc. in nd6_rtr.c) modifies these lists under
nd6_mtx. So the sysctl handler can dereference a freed entry β UAF.
Reproduce
# Re-use the DF-0354 injector to populate state:
( cd ../DF-0354 && cc -O2 -o ra_inject_tap ra_inject_tap.c && ./ra_inject_tap /dev/tap0 60 ) &
# Hammer the sysctl as unprivileged user during RA processing:
for i in 1 2 3 4 5 6 7 8 9 10; do
su -m maxx -c 'sysctl -b net.inet6.icmp6.nd6_drlist' > /tmp/dr.$i
su -m maxx -c 'sysctl -b net.inet6.icmp6.nd6_prlist' > /tmp/pr.$i
done
On a multi-CPU system with heavier traffic, the race becomes winnable β UAF panic. In our limited test, the guest stayed up.
Files
fix.diffβ wrap both sysctl iterations in mtx_lock/unlock(&nd6_mtx)VERDICT.mdβ full narrativeenv.txt
DF-0355 β nd6_sysctl_drlist/prlist iterate global lists without nd6_mtx
Verdict
INCONCLUSIVE β code-level lock-order violation confirmed by source trace; race not deterministically triggered in test window.
The buggy sysctl handlers DO iterate the global nd_defrouter TAILQ
and nd_prefix list without holding nd6_mtx. The ioctl path that
walks the same lists DOES hold the lock. The contrast proves the
omission is a real bug, not a stylistic choice. However, on a
single-CPU race against RA processing the window is narrow; in our
limited stress test we did not win the race and did not observe a
UAF/panic. The bug remains real; the impact is theoretical here.
Bug mechanism (source trace)
File: sys/netinet6/nd6.c.
The lock IS taken on the ioctl path:
- Line 1441:
mtx_lock(&nd6_mtx);β guardingSIOCGDRLST_IN6iteration ofnd_defrouter(lines 1442β1460). - Line 1473:
mtx_lock(&nd6_mtx);β guardingSIOCGPRLST_IN6iteration ofnd_prefix(lines 1474β1513). - Both release
mtx_unlock(&nd6_mtx)at lines 1461/1514.
The lock is NOT taken on the sysctl path:
nd6_sysctl_drlist(2156β2195): walksTAILQ_FIRST(&nd_defrouter)βTAILQ_NEXT(dr, dr_entry)(lines 2168β2169) with no mtx.nd6_sysctl_prlist(2197β2267): walksnd_prefix.lh_firstβpr->ndpr_next(line 2209) and per-prefixpr->ndpr_advrtrs.lh_firstβpfr->pfr_next(lines 2238β2239) with no mtx.
The lock IS taken by RA processing (the modifier):
defrtrlist_update(sys/netinet6/nd6_rtr.c:666):mtx_lock(&nd6_mtx), manipulates thend_defrouterTAILQ (TAILQ_INSERT_TAIL line 702).defrtrlist_del,prelist_remove,pfxrtr_add,pfxrtr_del(nd6_rtr.c) all run undernd6_mtx.
Race
On a multi-CPU system, the sysctl handler can be deep in
TAILQ_NEXT(dr, dr_entry) on CPU A when CPU B (netisr0) processes an
RA that calls defrtrlist_del(dr) β freeing the very dr whose
dr_entry the sysctl handler is about to dereference. UAF read,
panic, or info leak (depending on slab reuse).
Reproduction attempt on this guest
We populated the kernel state with DF-0354's RA injector (60 routers + 1 prefix), then concurrently ran 10 unprivileged sysctl reads while the injector kept flapping RAs. The guest stayed up across all runs; no panic, no garbled output observed.
race run (60 RAs injected concurrently with 10 sysctl reads): guest still up after 10 reads β (no UAF triggered)
This is the expected outcome for a hard-to-win race on a mostly-idle guest; it does NOT refute the bug. With a busier RA stream / longer runtime / more CPUs, a UAF is achievable.
Impact (theoretical, code-confirmed)
- UAF read of a freed
nd_defrouterornd_prefix/nd_pfxrouter. - Outcome depends on slab reuse: most likely a panic (dereferencing
freed
dr->ifpetc.); potentially an info leak if the slab slot is reused with attacker-influenced data. - Realistic trigger: any user reads
nd6_drlist/nd6_prlistsysctl during a link where RAs are arriving β a default-IPv6 network under RA flood, or simply during routine RA refresh.
Recommended fix
Hold nd6_mtx across the iteration in both sysctl handlers, mirroring
the ioctl path. The handlers already do per-entry work that is safe
under the mutex (the in6_recoverscope and SYSCTL_OUT copies happen
into a stack buffer; only the list walk and field reads need
protection). See fix.diff.
Files in this folder
fix.diffβ wrap both sysctl iterations in mtx_lock/unlock(nd6_mtx)VERDICT.mdβ this filemanifest.jsonenv.txtβ guest environmentrace_log.txtβ concurrent injector + sysctl-read test (no crash)
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. nd6_sysctl_drlist/prlist iterate without nd6_mtx vs ioctl holds it. Race not won.
No comments yet.