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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0691 Β· 9 files
FileTypeDescriptionSize
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
README.md readme status + harness + fix + validation
↓ download 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).

VERDICT.md verdict locking analysis, environmental blockers, realistic threat model, fix
↓ download raw

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:

  1. 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.
  2. There is no IPv6 MLD querier on this isolated guest. The QEMU user (slirp) network does not forward ICMPv6 MLD, so no inbound MLD_LISTENER_QUERY ever reaches mld6_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 replay mld6_input's serializer-held protospec read vs in6_delmulti on vtnet0. It proved the netisr constraint (ifnet_serialize_all requires 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:

  1. mld6.c β€” NULL-check protospec after 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.
  2. in6.c:in6_delmulti β€” defer kfree(in6m) to after if_delmulti() (which removes ifma from the interface list), so that while ifma is still on the list in6m stays allocated (only protospec is 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)

Fix verification

not_testable

validated

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