zeroconf has unbounded recursion in DNS compression-pointer decoder that allows LAN-local denial of service
Description
Impact
DNSIncoming._decode_labels_at_offset recurses once per DNS-name compression pointer (RFC 1035 §4.1.4). Pointer cycles and label counts were capped, but the chain length of unique forward pointers was not. A single ~3 kB mDNS packet carrying ~1500 chained pointers drives the recursion past CPython's default limit, and RecursionError was not listed in DECODE_EXCEPTIONS, so it escaped DNSIncoming.__init__ and was logged by asyncio's default exception handler.
Any unauthenticated host on the local link (UDP/5353, 224.0.0.251 / ff02::fb) can degrade the mDNS listener; that includes a guest on the same Wi-Fi, a compromised IoT device, or a container on a shared bridge. Replaying at a few hertz produces sustained CPU burn and log flooding, and mDNS-dependent features (HomeKit, Chromecast/Matter, AirPlay, printers) degrade while the attack is in flight.
Patches
Fixed in zeroconf 0.149.5 (PR #1719). Upgrade to >= 0.149.5.
Workarounds
There is no in-process workaround; upgrading is the fix. Otherwise, restrict mDNS (UDP/5353) to trusted Layer-2 segments via AP client isolation, guest-network separation, or host firewall rules.
### Resources - PR #1719, fix - Issue #1713, public tracking issue - RFC 1035 §4.1.4, RFC 6762, CWE-674
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unbounded recursion in python-zeroconf's DNS compression-pointer decoder allows unauthenticated local-link attackers to cause denial of service via crafted mDNS packets.
Vulnerability
DNSIncoming._decode_labels_at_offset in python-zeroconf recurses once per DNS-name compression pointer (RFC 1035 §4.1.4). While pointer cycles and label counts were capped, the chain length of unique forward pointers was not. A single ~3 kB mDNS packet carrying ~1500 chained pointers drives recursion past CPython's default limit (1000). RecursionError was not listed in DECODE_EXCEPTIONS, so it escapes DNSIncoming.__init__ and is logged by asyncio's default exception handler. All versions prior to 0.149.5 are affected [1][2].
Exploitation
An unauthenticated host on the same local link (UDP/5353, multicast addresses 224.0.0.251 / ff02::fb) can send a single crafted mDNS packet with chained compression pointers. No authentication or user interaction is required. Replaying the packet at a few hertz produces sustained CPU burn and log flooding [2].
Impact
Successful exploitation causes denial of service: the mDNS listener degrades, and mDNS-dependent features (HomeKit, Chromecast/Matter, AirPlay, printers) become unavailable while the attack is in flight. No code execution or data disclosure is possible [2].
Mitigation
The vulnerability is fixed in zeroconf version 0.149.5 (PR #1719) [1]. Upgrade to >= 0.149.5. There is no in-process workaround. Network-level mitigation includes restricting mDNS (UDP/5353) to trusted Layer-2 segments via AP client isolation, guest-network separation, or host firewall rules [2].
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: < 0.149.5
Patches
1f9e23592137ffix: bound DNS compression-pointer chain depth in DNSIncoming (#1719)
3 files changed · +33 −5
src/zeroconf/_protocol/incoming.pxd+1 −1 modified@@ -83,7 +83,7 @@ cdef class DNSIncoming: link_py_int=object, linked_labels=cython.list ) - cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers) + cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers, unsigned int depth) @cython.locals(offset="unsigned int") cdef void _read_header(self)
src/zeroconf/_protocol/incoming.py+10 −4 modified@@ -60,7 +60,7 @@ MAX_DNS_LABELS = 128 MAX_NAME_LENGTH = 253 -DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError) +DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError, RecursionError) _seen_logs: dict[str, int | tuple] = {} @@ -409,7 +409,7 @@ def _read_name(self) -> str: labels: list[str] = [] seen_pointers: set[int] = set() original_offset = self.offset - self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers) + self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers, 0) self._name_cache[original_offset] = labels name = ".".join(labels) + "." if len(name) > MAX_NAME_LENGTH: @@ -418,8 +418,14 @@ def _read_name(self) -> str: ) return name - def _decode_labels_at_offset(self, off: _int, labels: list[str], seen_pointers: set[int]) -> int: + def _decode_labels_at_offset( + self, off: _int, labels: list[str], seen_pointers: set[int], depth: _int + ) -> int: # This is a tight loop that is called frequently, small optimizations can make a difference. + if depth > MAX_DNS_LABELS: + raise IncomingDecodeError( + f"DNS compression pointer chain exceeds {MAX_DNS_LABELS} at {off} from {self.source}" + ) view = self.view while off < self._data_len: length = view[off] @@ -457,7 +463,7 @@ def _decode_labels_at_offset(self, off: _int, labels: list[str], seen_pointers: if not linked_labels: linked_labels = [] seen_pointers.add(link_py_int) - self._decode_labels_at_offset(link, linked_labels, seen_pointers) + self._decode_labels_at_offset(link, linked_labels, seen_pointers, depth + 1) self._name_cache[link_py_int] = linked_labels labels.extend(linked_labels) if len(labels) > MAX_DNS_LABELS:
tests/test_protocol.py+22 −0 modified@@ -1011,6 +1011,28 @@ def test_label_compression_attack(): assert len(parsed.answers()) == 1 +def test_dns_compression_pointer_chain_depth_attack() -> None: + """Test our wire parser rejects deeply chained compression pointers without recursing.""" + # Build a packet with one question whose name is a 1500-deep chain of forward + # compression pointers, ending in a root label. Each pointer is 2 bytes, + # so chain length easily exceeds CPython's default recursion limit. + header = b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" + # Question at offset 12: pointer to offset 18 (past the question's type/class). + question_name = bytes([0xC0, 18]) + question_type_class = b"\x00\x01\x00\x01" + chain_depth = 1500 + chain = bytearray() + for i in range(chain_depth): + target = 18 + 2 * (i + 1) + chain.append(0xC0 | (target >> 8)) + chain.append(target & 0xFF) + chain.append(0x00) + packet = header + question_name + question_type_class + bytes(chain) + parsed = r.DNSIncoming(packet, ("1.2.3.4", 5353)) + assert parsed.valid is False + assert parsed.questions == [] + + def test_dns_compression_loop_attack(): """Test our wire parser does not loop forever when dns compression is in a loop.""" packet = (
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
4News mentions
0No linked articles in our index yet.