All of lore.kernel.org
 help / color / mirror / Atom feed
From: ankur.tyagi85@gmail.com
To: openembedded-devel@lists.openembedded.org
Cc: Ankur Tyagi <ankur.tyagi85@gmail.com>
Subject: [oe][meta-python][wrynose][PATCH 9/33] python3-zeroconf: patch CVE-2026-47184
Date: Mon,  7 Sep 2026 22:22:53 +1200	[thread overview]
Message-ID: <20260907102318.2459883-9-ankur.tyagi85@gmail.com> (raw)
In-Reply-To: <20260907102318.2459883-1-ankur.tyagi85@gmail.com>

From: Ankur Tyagi <ankur.tyagi85@gmail.com>

Details:
https://nvd.nist.gov/vuln/detail/cve-2026-47184

Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
 .../python3-zeroconf/CVE-2026-47184.patch     | 558 ++++++++++++++++++
 .../python/python3-zeroconf_0.148.0.bb        |   1 +
 2 files changed, 559 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-zeroconf/CVE-2026-47184.patch

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


  parent reply	other threads:[~2026-09-07 10:23 UTC|newest]

Thread overview: 37+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-07 10:22 [oe][meta-oe][wrynose][PATCH 1/33] libnfs: patch CVE-2026-53689 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 2/33] libnfs: patch CVE-2026-57918 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 3/33] libssh: ignore CVE-2025-14821 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 4/33] libssh: mark CVEs patched ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 5/33] libssh: ignore CVE-2025-59842 ankur.tyagi85
2026-09-15  1:29   ` Anuj Mittal
2026-09-15  1:54     ` Ankur Tyagi
2026-09-07 10:22 ` [oe][meta-networking][wrynose][PATCH 6/33] miniupnpd: patch CVE-2026-5720 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-python][wrynose][PATCH 7/33] python3-zeroconf: patch CVE-2026-47180 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-python][wrynose][PATCH 8/33] python3-zeroconf: patch CVE-2026-47183 ankur.tyagi85
2026-09-07 10:22 ` ankur.tyagi85 [this message]
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 10/33] valkey: mark CVE-2026-56684 and CVE-2026-63639 patched ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-oe][wrynose][PATCH 11/33] libyang: patch CVE-2026-41401 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-networking][wrynose][PATCH 12/33] tinyproxy: patch CVE-2026-31842 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-networking][wrynose][PATCH 13/33] tinyproxy: patch CVE-2026-54387 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-networking][wrynose][PATCH 14/33] tinyproxy: patch CVE-2026-55202 ankur.tyagi85
2026-09-07 10:22 ` [oe][meta-webserver][wrynose][PATCH 15/33] nginx: mark CVE-2026-1642 patched ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 16/33] open62541: patch CVE-2026-11946 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-oe][wrynose][PATCH 17/33] smarty: mark CVEs patched ankur.tyagi85
2026-09-15  1:28   ` Anuj Mittal
2026-09-15  1:53     ` Ankur Tyagi
2026-09-07 10:23 ` [oe][meta-oe][wrynose][PATCH 18/33] tesseract: patch CVE-2026-73066 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-oe][wrynose][PATCH 19/33] tesseract: patch CVE-2026-73067 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 20/33] wolfssl: mark CVEs patched ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 21/33] wolfssl: patch CVE-2026-10098 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 22/33] wolfssl: patch CVE-2026-10512 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 23/33] wolfssl: ignore CVE-2026-12340 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 24/33] wolfssl: patch CVE-2026-55958 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 25/33] wolfssl: patch CVE-2026-6091 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 26/33] wolfssl: patch CVE-2026-6092 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 27/33] wolfssl: patch CVE-2026-6094 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 28/33] wolfssl: patch CVE-2026-6291 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 29/33] wolfssl: patch CVE-2026-6325 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 30/33] wolfssl: patch CVE-2026-6412 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 31/33] wolfssl: patch CVE-2026-6450 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 32/33] wolfssl: patch CVE-2026-6731 ankur.tyagi85
2026-09-07 10:23 ` [oe][meta-networking][wrynose][PATCH 33/33] wolfssl: patch CVE-2026-7531 ankur.tyagi85

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260907102318.2459883-9-ankur.tyagi85@gmail.com \
    --to=ankur.tyagi85@gmail.com \
    --cc=openembedded-devel@lists.openembedded.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.