diff --git a/meta-python/recipes-devtools/python/python3-zeroconf/CVE-2026-47184.patch b/meta-python/recipes-devtools/python/python3-zeroconf/CVE-2026-47184.patch
new file mode 100644
index 0000000000..73d4c2bd9c
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-zeroconf/CVE-2026-47184.patch
@@ -0,0 +1,558 @@
+From 61d9e147da48ffb5f660e639f6d457e5cda008d4 Mon Sep 17 00:00:00 2001
+From: "J. Nick Koston" <nick@koston.org>
+Date: Sun, 17 May 2026 21:48:40 -0700
+Subject: [PATCH] fix: bound DNSCache record count to prevent unbounded
+ LAN-driven growth (#1718)
+
+(cherry picked from commit 0ad3f37b5b852b8f614d322283d148efb2cef6e4)
+
+CVE: CVE-2026-47184
+Upstream-Status: Backport [https://github.com/python-zeroconf/python-zeroconf/commit/0ad3f37b5b852b8f614d322283d148efb2cef6e4]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ src/zeroconf/_cache.pxd              |  11 +-
+ src/zeroconf/_cache.py               |  79 +++++----
+ src/zeroconf/const.py                |   6 +
+ tests/benchmarks/test_cache_bound.py |  68 ++++++++
+ tests/test_cache.py                  | 241 ++++++++++++++++++++++++++-
+ 5 files changed, 374 insertions(+), 31 deletions(-)
+ create mode 100644 tests/benchmarks/test_cache_bound.py
+
+diff --git a/src/zeroconf/_cache.pxd b/src/zeroconf/_cache.pxd
+index 05a40c0..023304b 100644
+--- a/src/zeroconf/_cache.pxd
++++ b/src/zeroconf/_cache.pxd
+@@ -19,6 +19,7 @@ cdef object _UNIQUE_RECORD_TYPES
+ cdef unsigned int _TYPE_PTR
+ cdef cython.uint _ONE_SECOND
+ cdef unsigned int _MIN_SCHEDULED_RECORD_EXPIRATION
++cdef unsigned int _MAX_CACHE_RECORDS
+ 
+ 
+ @cython.locals(record_cache=dict)
+@@ -31,6 +32,7 @@ cdef class DNSCache:
+     cdef public cython.dict service_cache
+     cdef public list _expire_heap
+     cdef public dict _expirations
++    cdef public unsigned int _total_records
+ 
+     cpdef bint async_add_records(self, object entries)
+ 
+@@ -60,10 +62,17 @@ cdef class DNSCache:
+         service_store=cython.dict,
+         service_record=DNSService,
+         when=object,
+-        new=bint
++        new=bint,
++        is_new=bint
+     )
+     cdef bint _async_add(self, DNSRecord record)
+ 
++    @cython.locals(record=DNSRecord, when_record=tuple)
++    cdef void _async_evict_oldest(self)
++
++    @cython.locals(expire_heap_len="unsigned int")
++    cdef void _maybe_rebuild_heap(self)
++
+     @cython.locals(service_record=DNSService)
+     cdef void _async_remove(self, DNSRecord record)
+ 
+diff --git a/src/zeroconf/_cache.py b/src/zeroconf/_cache.py
+index c7ca847..d323a6a 100644
+--- a/src/zeroconf/_cache.py
++++ b/src/zeroconf/_cache.py
+@@ -37,7 +37,7 @@ from ._dns import (
+     DNSText,
+ )
+ from ._utils.time import current_time_millis
+-from .const import _ONE_SECOND, _TYPE_PTR
++from .const import _MAX_CACHE_RECORDS, _ONE_SECOND, _TYPE_PTR
+ 
+ _UNIQUE_RECORD_TYPES = (DNSAddress, DNSHinfo, DNSPointer, DNSText, DNSService)
+ _UniqueRecordsType = Union[DNSAddress, DNSHinfo, DNSPointer, DNSText, DNSService]
+@@ -72,6 +72,7 @@ class DNSCache:
+         self._expire_heap: list[tuple[float, DNSRecord]] = []
+         self._expirations: dict[DNSRecord, float] = {}
+         self.service_cache: _DNSRecordCacheType = {}
++        self._total_records: int = 0
+ 
+     # Functions prefixed with async_ are NOT threadsafe and must
+     # be run in the event loop.
+@@ -89,15 +90,34 @@ class DNSCache:
+         # replaces any existing records that are __eq__ to each other which
+         # removes the risk that accessing the cache from the wrong
+         # direction would return the old incorrect entry.
+-        if (store := self.cache.get(record.key)) is None:
++        store = self.cache.get(record.key)
++        is_new = store is None or record not in store
++        # Bound total cache size; evict closest-to-expiration entry to
++        # make room before inserting a new record. Prevents a LAN-local
++        # flood of unique-name records from growing the cache without
++        # bound (RFC 6762 §10 advisory caching, defense-in-depth).
++        if is_new and self._total_records >= _MAX_CACHE_RECORDS:
++            self._async_evict_oldest()
++            # The victim may have been the last record under
++            # ``record.key``, in which case ``_remove_key`` deleted
++            # the bucket. Re-fetch before creating below.
++            store = self.cache.get(record.key)
++        if store is None:
+             store = self.cache[record.key] = {}
+-        new = record not in store and not isinstance(record, DNSNsec)
++        new = is_new and not isinstance(record, DNSNsec)
++        if is_new:
++            self._total_records += 1
+         store[record] = record
+         when = record.created + (record.ttl * 1000)
+         if self._expirations.get(record) != when:
+-            # Avoid adding duplicates to the heap
+             heappush(self._expire_heap, (when, record))
+             self._expirations[record] = when
++            # Re-adds of an existing record with a new TTL push a fresh
++            # entry but leave the prior tuple behind as stale, so a peer
++            # that just replays cached records can grow ``_expire_heap``
++            # without ever tripping the cap. Rebuild when stale entries
++            # dominate.
++            self._maybe_rebuild_heap()
+ 
+         if isinstance(record, DNSService):
+             service_record = record
+@@ -106,6 +126,28 @@ class DNSCache:
+             service_store[service_record] = service_record
+         return new
+ 
++    def _async_evict_oldest(self) -> None:
++        """Drop the closest-to-expiration record to make room for a new one."""
++        while self._expire_heap:
++            when_record = heappop(self._expire_heap)
++            record = when_record[1]
++            if self._expirations.get(record) != when_record[0]:
++                continue
++            self._async_remove(record)
++            return
++
++    def _maybe_rebuild_heap(self) -> None:
++        """Rebuild ``_expire_heap`` when stale entries dominate live ones."""
++        expire_heap_len = len(self._expire_heap)
++        if (
++            expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION
++            and expire_heap_len > len(self._expirations) * 2
++        ):
++            self._expire_heap = [
++                entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0]
++            ]
++            heapify(self._expire_heap)
++
+     def async_add_records(self, entries: Iterable[DNSRecord]) -> bool:
+         """Add multiple records.
+ 
+@@ -129,6 +171,7 @@ class DNSCache:
+             _remove_key(self.service_cache, service_record.server_key, service_record)
+         _remove_key(self.cache, record.key, record)
+         self._expirations.pop(record, None)
++        self._total_records -= 1
+ 
+     def async_remove_records(self, entries: Iterable[DNSRecord]) -> None:
+         """Remove multiple records.
+@@ -145,43 +188,23 @@ class DNSCache:
+ 
+         :param now: The current time in milliseconds.
+         """
+-        if not (expire_heap_len := len(self._expire_heap)):
++        if not self._expire_heap:
+             return []
+ 
+         expired: list[DNSRecord] = []
+-        # Find any expired records and add them to the to-delete list
+         while self._expire_heap:
+             when_record = self._expire_heap[0]
+             when = when_record[0]
+             if when > now:
+                 break
+             heappop(self._expire_heap)
+-            # Check if the record hasn't been re-added to the heap
+-            # with a different expiration time as it will be removed
+-            # later when it reaches the top of the heap and its
+-            # expiration time is met.
++            # Skip entries left behind by a TTL re-add; the live tuple is
++            # later in the heap and will be removed when it reaches the top.
+             record = when_record[1]
+             if self._expirations.get(record) == when:
+                 expired.append(record)
+ 
+-        # If the expiration heap grows larger than the number expirations
+-        # times two, we clean it up to avoid keeping expired entries in
+-        # the heap and consuming memory. We guard this with a minimum
+-        # threshold to avoid cleaning up the heap too often when there are
+-        # only a few scheduled expirations.
+-        if (
+-            expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION
+-            and expire_heap_len > len(self._expirations) * 2
+-        ):
+-            # Remove any expired entries from the expiration heap
+-            # that do not match the expiration time in the expirations
+-            # as it means the record has been re-added to the heap
+-            # with a different expiration time.
+-            self._expire_heap = [
+-                entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0]
+-            ]
+-            heapify(self._expire_heap)
+-
++        self._maybe_rebuild_heap()
+         self.async_remove_records(expired)
+         return expired
+ 
+diff --git a/src/zeroconf/const.py b/src/zeroconf/const.py
+index 1db39a4..a17e468 100644
+--- a/src/zeroconf/const.py
++++ b/src/zeroconf/const.py
+@@ -59,6 +59,12 @@ _DNS_OTHER_TTL = 4500  # 75 minutes for non-host records (PTR, TXT etc) as-per R
+ # level of rate limit and safe guards so we use 1/4 of the recommended value
+ _DNS_PTR_MIN_TTL = 1125
+ 
++# Upper bound on the number of records the DNSCache will hold before it
++# starts evicting the closest-to-expiration entry to make room for new
++# arrivals. Bounds the memory a malicious LAN peer can force the cache
++# to retain by multicasting many unique-name records.
++_MAX_CACHE_RECORDS = 10000
++
+ _DNS_PACKET_HEADER_LEN = 12
+ 
+ _MAX_MSG_TYPICAL = 1460  # unused
+diff --git a/tests/benchmarks/test_cache_bound.py b/tests/benchmarks/test_cache_bound.py
+new file mode 100644
+index 0000000..774129e
+--- /dev/null
++++ b/tests/benchmarks/test_cache_bound.py
+@@ -0,0 +1,68 @@
++"""Benchmark for the DNSCache record-count bound + overflow eviction."""
++
++from __future__ import annotations
++
++from collections.abc import Iterator
++from itertools import count
++
++from pytest_codspeed import BenchmarkFixture
++
++from zeroconf import DNSAddress, DNSCache, current_time_millis
++from zeroconf.const import _CLASS_IN, _MAX_CACHE_RECORDS, _TYPE_A
++
++
++def _make_records(count_: int, now: float, prefix: str = "bench") -> list[DNSAddress]:
++    return [
++        DNSAddress(
++            f"{prefix}-{i}.local.",
++            _TYPE_A,
++            _CLASS_IN,
++            120,
++            bytes(((i >> 24) & 0xFF, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF)),
++            created=now + i,
++        )
++        for i in range(count_)
++    ]
++
++
++def _unbounded_records(now: float, prefix: str = "evict") -> Iterator[DNSAddress]:
++    """Unbounded generator of unique-name DNSAddress records."""
++    for i in count():
++        yield DNSAddress(
++            f"{prefix}-{i}.local.",
++            _TYPE_A,
++            _CLASS_IN,
++            120,
++            bytes(((i >> 24) & 0xFF, (i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF)),
++            created=now + i,
++        )
++
++
++def test_cache_add_below_cap(benchmark: BenchmarkFixture) -> None:
++    """Adding records while the cache is well below the cap (no eviction)."""
++    now = current_time_millis()
++    records = _make_records(1000, now)
++
++    @benchmark
++    def _add() -> None:
++        cache = DNSCache()
++        cache.async_add_records(records)
++
++
++def test_cache_add_at_cap_evicts(benchmark: BenchmarkFixture) -> None:
++    """Steady-state add at the cap: every measured insert forces one eviction.
++
++    Pre-fills the cache to ``_MAX_CACHE_RECORDS`` outside the timed body so
++    only the eviction-path adds are measured. Each benchmark iteration
++    pulls one fresh unique record from an unbounded generator, keeping the
++    cache permanently at the cap. The generator avoids the iteration-count
++    cap that a pre-built pool would impose for very fast operations.
++    """
++    now = current_time_millis()
++    cache = DNSCache()
++    cache.async_add_records(_make_records(_MAX_CACHE_RECORDS, now, prefix="fill"))
++    pool = _unbounded_records(now + _MAX_CACHE_RECORDS)
++
++    @benchmark
++    def _evict_one() -> None:
++        cache.async_add_records([next(pool)])
+diff --git a/tests/test_cache.py b/tests/test_cache.py
+index 9d55435..aeb3a2a 100644
+--- a/tests/test_cache.py
++++ b/tests/test_cache.py
+@@ -439,7 +439,9 @@ async def test_cache_heap_multi_name_cleanup() -> None:
+         )
+         cache.async_add_records([record])
+ 
+-    assert len(cache._expire_heap) == min_records_to_cleanup + 5
++    # ``_async_add`` rebuilds ``_expire_heap`` proactively when stale entries
++    # dominate (heap > 2x expirations), so the heap is already capped at
++    # ~one entry per unique record long before ``async_expire`` is called.
+     assert len(cache.async_entries_with_name(name)) == 1
+     assert len(cache.async_entries_with_name(name2)) == 5
+ 
+@@ -473,7 +475,8 @@ async def test_cache_heap_pops_order() -> None:
+         )
+         cache.async_add_records([record])
+ 
+-    assert len(cache._expire_heap) == min_records_to_cleanup + 5
++    # ``_async_add`` proactively rebuilds the heap when stale entries dominate,
++    # so the heap holds only one entry per unique record by this point.
+     assert len(cache.async_entries_with_name(name)) == 1
+     assert len(cache.async_entries_with_name(name2)) == 5
+ 
+@@ -482,3 +485,237 @@ async def test_cache_heap_pops_order() -> None:
+         ts, _ = heappop(cache._expire_heap)
+         assert ts >= start_ts
+         start_ts = ts
++
++
++def _addr(name: str, idx: int, *, ttl: int = 120, created: float | None = None) -> r.DNSAddress:
++    """Build a DNSAddress with idx-derived payload for the bound/eviction tests."""
++    return r.DNSAddress(
++        name,
++        const._TYPE_A,
++        const._CLASS_IN,
++        ttl,
++        bytes((idx & 0xFF, (idx >> 8) & 0xFF, 0, 1)),
++        created=r.current_time_millis() if created is None else created,
++    )
++
++
++def test_cache_size_is_bounded() -> None:
++    """A flood of unique-name records is capped at ``_MAX_CACHE_RECORDS``."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    overflow = 1000
++    flood_size = const._MAX_CACHE_RECORDS + overflow
++
++    cache.async_add_records(_addr(f"flood-{i}.local.", i, created=now + i) for i in range(flood_size))
++
++    total = sum(len(store) for store in cache.cache.values())
++    assert total == const._MAX_CACHE_RECORDS
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++    # FIFO-ish: the earliest-created records (closest to expiration) get
++    # evicted first, so the names that remain are from the tail.
++    for i in range(overflow):
++        assert f"flood-{i}.local." not in cache.cache
++    for i in range(flood_size - overflow, flood_size):
++        assert f"flood-{i}.local." in cache.cache
++
++
++def test_cache_eviction_empty_heap_returns_without_evicting() -> None:
++    """Eviction tolerates an empty ``_expire_heap`` (invariant-violation safety net)."""
++    cache = r.DNSCache()
++    # By the cache invariant every record in ``_total_records`` has a heap
++    # entry, so eviction should never see an empty heap. Force the broken
++    # state directly to pin the defensive behaviour: ``_async_evict_oldest``
++    # returns without raising and the subsequent insert still lands. Since
++    # eviction can't free space, the counter is allowed to drift past the
++    # cap by exactly one — pinned so a future change to the recovery
++    # semantics (e.g., refusing the add or clamping) fails this test.
++    cache._total_records = const._MAX_CACHE_RECORDS
++    cache._expire_heap = []
++    cache.async_add_records([_addr("post-empty.local.", 0)])
++    assert "post-empty.local." in cache.cache
++    assert cache._total_records == const._MAX_CACHE_RECORDS + 1
++
++
++def test_cache_eviction_skips_stale_heap_entries() -> None:
++    """Eviction skips stale heap entries left by TTL re-adds."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    cache.async_add_records(
++        _addr(f"stale-{i}.local.", i, created=now + i) for i in range(const._MAX_CACHE_RECORDS)
++    )
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++
++    # Re-add the closest-to-expiration record with a longer TTL; the prior
++    # ``(when, record)`` tuple stays as stale, eviction must skip it.
++    victim_name = "stale-0.local."
++    cache.async_add_records([_addr(victim_name, 0, ttl=7200, created=now)])
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++
++    cache.async_add_records([_addr("trigger.local.", 0xFFFF, created=now + const._MAX_CACHE_RECORDS)])
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++    assert victim_name in cache.cache
++    assert "stale-1.local." not in cache.cache
++
++
++def test_cache_eviction_victim_shares_key_with_new_record() -> None:
++    """Inserting a record whose key collides with the eviction victim keeps it reachable."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    cache.async_add_records(
++        _addr(f"filler-{i}.local.", i, created=now + 1000 + i) for i in range(const._MAX_CACHE_RECORDS - 1)
++    )
++
++    # Insert at "shared.local." with the earliest expiration so eviction
++    # picks it. ``_remove_key`` then deletes ``cache["shared.local."]``.
++    shared_key = "shared.local."
++    cache.async_add_records([_addr(shared_key, 0x0102, created=now)])
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++
++    # Adding a new record under the SAME key: a pre-eviction-captured
++    # ``store`` would write into an orphaned dict; the fix re-resolves.
++    new_shared = _addr(shared_key, 0x0506, created=now + 999)
++    cache.async_add_records([new_shared])
++
++    assert shared_key in cache.cache, "new record orphaned: cache bucket missing"
++    assert new_shared in cache.cache[shared_key]
++    assert cache.async_get_unique(new_shared) == new_shared
++    total = sum(len(store) for store in cache.cache.values())
++    assert total == cache._total_records
++
++
++def test_cache_dnsnsec_at_cap_evicts_prior_record() -> None:
++    """A single DNSNsec arriving at the cap evicts one prior record and stays reachable."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    cache.async_add_records(
++        _addr(f"fill-{i}.local.", i, created=now + i) for i in range(const._MAX_CACHE_RECORDS)
++    )
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++
++    nsec = r.DNSNsec(
++        "nsec-arrival.local.",
++        const._TYPE_NSEC,
++        const._CLASS_IN,
++        120,
++        "nsec-arrival.local.",
++        [const._TYPE_A],
++    )
++    cache.async_add_records([nsec])
++
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++    assert nsec in cache.cache[nsec.key]
++    # The earliest-created fill record is gone (FIFO-ish eviction).
++    assert "fill-0.local." not in cache.cache
++
++
++def test_cache_dnsnsec_flood_is_bounded() -> None:
++    """DNSNsec records honour ``_MAX_CACHE_RECORDS`` (no bypass via the ``new`` flag)."""
++    cache = r.DNSCache()
++    overflow = 100
++    cache.async_add_records(
++        r.DNSNsec(
++            f"nsec-{i}.local.",
++            const._TYPE_NSEC,
++            const._CLASS_IN,
++            120,
++            f"nsec-{i}.local.",
++            [const._TYPE_A],
++        )
++        for i in range(const._MAX_CACHE_RECORDS + overflow)
++    )
++    assert cache._total_records == const._MAX_CACHE_RECORDS
++    total = sum(len(store) for store in cache.cache.values())
++    assert total == const._MAX_CACHE_RECORDS
++
++
++def test_cache_re_add_flood_does_not_grow_heap_unbounded() -> None:
++    """Replaying cached records with shifting TTLs cannot grow ``_expire_heap`` unbounded."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    # Stay below the cache cap so eviction never fires; the attack here is
++    # heap growth via re-add, not cap saturation. Clear the
++    # ``_MIN_SCHEDULED_RECORD_EXPIRATION`` floor so the rebuild engages.
++    record_count = 200
++    cache.async_add_records(_addr(f"flood-{i}.local.", i, created=now) for i in range(record_count))
++    assert cache._total_records == record_count
++
++    # 10 cycles x ``record_count`` stale pushes each. Without
++    # ``_maybe_rebuild_heap`` firing inside ``_async_add``, the heap would
++    # grow to ~11 x record_count.
++    for cycle in range(10):
++        cache.async_add_records(
++            _addr(f"flood-{i}.local.", i, ttl=7200 + cycle, created=now) for i in range(record_count)
++        )
++
++    # Heap is bounded near the rebuild threshold; ``+ record_count`` of slack
++    # to stay resilient to where in a re-add cycle the rebuild last fired.
++    assert len(cache._expire_heap) <= 2 * len(cache._expirations) + record_count
++    assert cache._total_records == record_count
++
++
++def test_cache_eviction_decrements_total_records() -> None:
++    """Natural removal (goodbyes, expirations) keeps ``_total_records`` in sync."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++    records = [_addr(f"sync-{i}.local.", i, created=now) for i in range(50)]
++    cache.async_add_records(records)
++    assert cache._total_records == 50
++
++    cache.async_remove_records(records[:20])
++    assert cache._total_records == 30
++
++    cache.async_expire(now + (200 * 1000))
++    assert cache._total_records == 0
++    assert not cache.cache
++
++
++def test_cache_total_records_invariant_under_mixed_ops() -> None:
++    """``_total_records`` stays equal to the sum of bucket sizes across all touched paths."""
++    cache = r.DNSCache()
++    now = r.current_time_millis()
++
++    def actual() -> int:
++        return sum(len(store) for store in cache.cache.values())
++
++    addrs = [_addr(f"mix-{i}.local.", i, created=now + i) for i in range(20)]
++    cache.async_add_records(addrs)
++    assert cache._total_records == actual() == 20
++
++    # Re-add of an identical record: no increment.
++    cache.async_add_records([addrs[0]])
++    assert cache._total_records == actual() == 20
++
++    # DNSService writes service_cache too — counter still matches cache size.
++    svc = r.DNSService("svc.local.", const._TYPE_SRV, const._CLASS_IN, 120, 0, 0, 80, "host.local.")
++    cache.async_add_records([svc])
++    assert cache._total_records == actual() == 21
++    cache.async_remove_records([svc])
++    assert cache._total_records == actual() == 20
++
++    # DNSNsec is stored but excluded from the "new" return; counter tracks it anyway.
++    nsec = r.DNSNsec("nsec.local.", const._TYPE_NSEC, const._CLASS_IN, 120, "nsec.local.", [const._TYPE_A])
++    cache.async_add_records([nsec])
++    assert cache._total_records == actual() == 21
++    cache.async_remove_records([nsec])
++    assert cache._total_records == actual() == 20
++
++    # Shared-key insert/remove: emptying the bucket drops the cache key but
++    # counter decrements only by the records that left.
++    shared_a = _addr("shared.local.", 0x0101, created=now)
++    shared_b = _addr("shared.local.", 0x0202, created=now)
++    cache.async_add_records([shared_a, shared_b])
++    assert cache._total_records == actual() == 22
++    cache.async_remove_records([shared_a, shared_b])
++    assert cache._total_records == actual() == 20
++    assert "shared.local." not in cache.cache
++
++    cache.async_expire(now + (200 * 1000))
++    assert cache._total_records == actual() == 0
++    assert not cache.cache
++
++    # Full-cap eviction loop: counter never grows past the cap, never drifts.
++    cap_records = [_addr(f"cap-{i}.local.", i, created=now + i) for i in range(const._MAX_CACHE_RECORDS + 50)]
++    for rec in cap_records:
++        cache.async_add_records([rec])
++        assert cache._total_records == actual()
++    assert cache._total_records == const._MAX_CACHE_RECORDS
diff --git a/meta-python/recipes-devtools/python/python3-zeroconf_0.148.0.bb b/meta-python/recipes-devtools/python/python3-zeroconf_0.148.0.bb
index aa1bb11065..cc68ffbf96 100644
--- a/meta-python/recipes-devtools/python/python3-zeroconf_0.148.0.bb
+++ b/meta-python/recipes-devtools/python/python3-zeroconf_0.148.0.bb
@@ -7,6 +7,7 @@ SRC_URI[sha256sum] = "03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f
 
 SRC_URI += "file://CVE-2026-47180.patch \
             file://CVE-2026-47183.patch \
+            file://CVE-2026-47184.patch \
 "
 
 inherit pypi python_poetry_core cython
