UAF / NULL-deref in mld6_input MLD_LISTENER_QUERY path: in6m freed concurrently with iteration
Summary
mld6_input MLD_LISTENER_QUERY (:280-323): acquires ifnet_serialize_all(ifp) :280 reads in6m=ifma->ifma_protospec :290 derefs in6m->in6m_addr :292. Concurrently in6_delmulti (in6.c:1772-1775): ifma_protospec=NULL :1772 LIST_REMOVE :1773 kfree(in6m) :1774 BEFORE if_delmini :1778 which acquires ifnet serializer. So mld6_input on other CPU holding serializer observes NULL protospec (NULL-deref panic :292 IN6_ARE_ADDR_EQUAL) or stale freed in6m (UAF read). timer==0 branch (maxdelay=0 forged MLD query :265-268): drops serializer :307 mld6_sendpkt->ip6_output blocks :309 re-acquires :311 writes in6m->in6m_timer=0 :312 in6m->in6m_state=MLD6_IREPORTEDLAST :313 = UAF WRITE on freed/reused heap. Attacker: on-link forged MLD query maxdelay=0 (fe80::1 src ff02::1 dst any link-local accepted mld6.c:204 no auth) + local unpriv user join/leave multicast groups. Impact: NULL-deref panic DoS or heap UAF write groomable M_IPMADDR slab C:H/I:H/A:H. Fix: in6_delmini acquire mld6_token before clearing protospec+kfree.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0691 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| mld6_race.c | trigger-source | harness replaying mld6_input protospec read vs in6_delmulti; trips ASSERT_NETISR_NCPUS (the race's root cause) | 4.6 KB | view raw |
| Makefile | build-config | kmod Makefile | 121 B | β download |
| fix_build.log | build-log | nativekernel build of patched kernel (rc=0; in6.c + mld6.c compiled -Werror clean) | 5.6 MB | β download |
| fix.diff | suggested-fix | mld6.c NULL-check protospec + in6.c defer kfree(in6m) until after if_delmulti | 1.8 KB | view raw |
| env.txt | environment | uname / kern.version / INET6-in-GENERIC / no querier note | 388 B | view raw |
| VERDICT.md | verdict | locking analysis, environmental blockers, realistic threat model, fix | 6.2 KB | β raw |
| README.md | readme | status + harness + fix + validation | 1.4 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-0691 PoC β mld6 MLD_LISTENER_QUERY NULL-deref/UAF race
Status
Real locking race, confirmed by source/locking analysis, but not live-reproducible
on this isolated guest (see VERDICT.md). The MLD_LISTENER_QUERY handler excludes
loopback (mld6.c:235) and there is no IPv6 MLD querier on the QEMU slirp network, so
no inbound query reaches the vulnerable iteration; ifnet_serialize_all is netisr-only
(netisr2.h:136), so a kernel-thread harness cannot replay it.
Harness
cd findings/poc/DF-0691 make # -> mld6_race.ko kldload ./mld6_race.ko
The harness replays mld6_input's protospec read vs in6_delmulti on vtnet0. It
trips ASSERT_NETISR_NCPUS (the serializer is netisr-only) β which is itself the root
cause: in6_delmulti (a syscall thread) cannot take the serializer, so it NULLs+kfrees
protospec unsynchronized from the netisr-side mld6_input reader.
Fix
Apply fix.diff to /usr/src/sys/netinet6/{mld6.c,in6.c} and rebuild the kernel
(INET6 is compiled into GENERIC). The fix:
1. mld6.c: NULL-check protospec after the read (if (in6m == NULL) continue;).
2. in6.c:in6_delmulti: defer kfree(in6m) to after if_delmulti() removes ifma
from the list.
Validated: applies cleanly; the patched kernel compiles (nativekernel rc=0, both
netinet6/in6.c and netinet6/mld6.c compiled -Werror clean β see fix_build.log).
Runtime before/after not possible (bug not live-reproducible here).
DF-0691 β mld6_input MLD_LISTENER_QUERY NULL-deref / UAF race
Verdict: NOT REPRODUCED (live) β real locking race confirmed in source; environment-blocked on this isolated guest
The race (confirmed by source/locking analysis)
mld6_input()'s MLD_LISTENER_QUERY handler iterates the interface's multicast
address list holding the ifnet serializer and dereferences protospec with no
NULL check:
/* sys/netinet6/mld6.c:280 */ ifnet_serialize_all(ifp);
/* :283 */ while ((ifma = TAILQ_NEXT(&mark, ifma_link)) != NULL) { ... }
/* :290 */ in6m = (struct in6_multi *)ifma->ifma_protospec; /* NO NULL check */
/* :292 */ if (IN6_ARE_ADDR_EQUAL(&in6m->in6m_addr, ...)) /* derefs in6m */
in6_delmulti() tears a membership down under crit_enter() but without the ifnet
serializer:
/* sys/netinet6/in6.c:1762 */ crit_enter();
/* :1771 */ if (ifma->ifma_refcount == 1) {
/* :1772 */ mld6_stop_listening(in6m);
/* :1773 */ ifma->ifma_protospec = NULL; /* NULLed here */
/* :1774 */ LIST_REMOVE(in6m, in6m_entry);
/* :1775 */ kfree(in6m, M_IPMADDR); /* freed here */
/* :1776 */ }
/* :1778 */ if_delmulti(...); /* ONLY here acquires the serializer */
ifnet_serialize_all is guarded by ASSERT_NETISR_NCPUS (sys/net/netisr2.h:136), so
it can only be taken on a netisr thread. in6_delmulti runs on a normal syscall thread
(socket close / group leave), so it cannot take the serializer and instead NULLs +
kfrees protospec under crit_enter() alone. Therefore mld6_input (holding the
serializer on a netisr CPU) can iterate an ifma that in6_delmulti has already NULLed
(and whose in6m it has already freed) but not yet removed from the list (if_delmulti
is blocked on the serializer) β NULL deref (in6m==NULL at :292) or use-after-free
(if the freed in6m memory was reclaimed).
Why not reproduced live on this guest
Two independent environmental factors prevent the live trigger:
- The query handler excludes loopback (
mld6.c:235:if (ifp->if_flags & IFF_LOOPBACK) break;). So the query must arrive on a non-loopback interface. - There is no IPv6 MLD querier on this isolated guest. The QEMU
user(slirp) network does not forward ICMPv6 MLD, so no inboundMLD_LISTENER_QUERYever reachesmld6_input's query path. The MLD query must come from the network (a router's periodic general query); it cannot be synthesized by an unprivileged local user, and even a root raw-ICMPv6 socket only sends packets out, not into the local netisr input path.
A harness (mld6_race.c) was written to replay mld6_input's serializer-held
protospec read vs in6_delmulti, but it tripped ifnet_serialize_all's
ASSERT_NETISR_NCPUS (netisr2.h:136) β the serializer can only be taken on a netisr
thread β confirming the read can only occur via genuine netisr inbound MLD input.
Realistic threat model
On any real IPv6 network with an MLD querier (any IPv6 router sends periodic general
queries β this is standard), the race is reachable by an unprivileged local user:
joining and leaving IPv6 multicast groups (setsockopt(IPV6_JOIN_GROUP/LEAVE_GROUP) β
not privileged) races the network querier's periodic MLD_LISTENER_QUERY arrival on a
non-loopback interface β NULL-deref panic (local DoS), with UAF potential if the freed
in6m is reclaimed. So this is a real, network-reachable, unprivileged-DoS (and
potential escalation) bug that simply cannot be exercised on this single isolated guest.
Primitive characterization
| property | value |
|---|---|
| sink | in6m->in6m_addr deref at mld6.c:292 with in6m == NULL (NULL-deref panic), or in6m pointing at freed/reclaimed memory (UAF) |
| trigger | inbound MLD_LISTENER_QUERY (non-loopback) concurrent with in6_delmulti (group leave) |
| privilege | unprivileged (group join/leave) + network querier |
| effect | kernel panic (DoS); potential UAF β corruption if in6m memory is reclaimed and shaped |
Escalation ceiling
The NULL-deref variant is a local/remote DoS. The UAF variant (freed in6m reclaimed)
could in principle be groomed for corruption, but in6m is a specific M_IPMADDR
object whose reclamation timing is hard to control and whose fields (mostly addresses /
timers) are low-value for code-exec pivots; the realistic, defensible impact is
unprivileged DoS via panic (and a hardening gap worth fixing regardless).
PoC changes
mld6_race.c+Makefile: harness that attempted to replaymld6_input's serializer-held protospec read vsin6_delmultionvtnet0. It proved the netisr constraint (ifnet_serialize_allrequires netisr context), which is itself why the race exists (the teardown cannot take the serializer). Kept as evidence of the constraint analysis.
Fix (fix.diff)
Two coordinated changes:
mld6.cβ NULL-checkprotospecafter the read at:290(if (in6m == NULL) continue;), so the serializer-held reader skips a half-torn-down entry instead of dereferencing NULL. Closes the NULL-deref panic.in6.c:in6_delmultiβ deferkfree(in6m)to afterif_delmulti()(which removesifmafrom the interface list), so that whileifmais still on the listin6mstays allocated (onlyprotospecis NULL). Combined with the NULL-check this also closes the UAF window.
Validated: the fix applies cleanly and the kernel rebuilds (see fix_build.log) β
confirming the change compiles and integrates. Runtime before/after is not possible
because the bug is not live-reproducible on this isolated guest (no MLD querier;
loopback excluded).
Kernel references (verified)
sys/netinet6/mld6.c:280βifnet_serialize_all(ifp)(serializer acquired)sys/netinet6/mld6.c:290βin6m = ifma->ifma_protospec(no NULL check)sys/netinet6/mld6.c:292βIN6_ARE_ADDR_EQUAL(&in6m->in6m_addr, ...)(deref)sys/netinet6/mld6.c:235βIFF_LOOPBACKexclusion of the query handlersys/netinet6/in6.c:1762-1778βin6_delmultiteardown (protospec NULL+kfree before serializer)sys/net/netisr2.h:136βASSERT_NETISR_NCPUS(serializer is netisr-only)
Fix verification
not_testablevalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
NOT REPRODUCED. mld6_input protospec read vs in6_delmulti free race. Source-confirmed but env-blocked (no MLD querier, loopback excluded, netisr-only serializer).
No comments yet.