β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0355

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0355 Β· 6 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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 narrative
  • env.txt
VERDICT.md verdict lock-order violation confirmed by source; race not won in test
↓ download raw

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); β€” guarding SIOCGDRLST_IN6 iteration of nd_defrouter (lines 1442–1460).
  • Line 1473: mtx_lock(&nd6_mtx); β€” guarding SIOCGPRLST_IN6 iteration of nd_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): walks TAILQ_FIRST(&nd_defrouter) β†’ TAILQ_NEXT(dr, dr_entry) (lines 2168–2169) with no mtx.
  • nd6_sysctl_prlist (2197–2267): walks nd_prefix.lh_first β†’ pr->ndpr_next (line 2209) and per-prefix pr->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 the nd_defrouter TAILQ (TAILQ_INSERT_TAIL line 702).
  • defrtrlist_del, prelist_remove, pfxrtr_add, pfxrtr_del (nd6_rtr.c) all run under nd6_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_defrouter or nd_prefix/nd_pfxrouter.
  • Outcome depends on slab reuse: most likely a panic (dereferencing freed dr->ifp etc.); potentially an info leak if the slab slot is reused with attacker-influenced data.
  • Realistic trigger: any user reads nd6_drlist/nd6_prlist sysctl during a link where RAs are arriving β€” a default-IPv6 network under RA flood, or simply during routine RA refresh.

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 file
  • manifest.json
  • env.txt β€” guest environment
  • race_log.txt β€” concurrent injector + sysctl-read test (no crash)

Fix verification

not_testable

compile 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.