Skip to content

Commit 8c356dd

Browse files
bdracobluetoothbot
authored andcommitted
fix: preserve lifo recency when scope upgrade replaces an entry
In _get_ip_addresses_from_cache_lifo, an in place replacement of an existing address kept the upgraded entry at its original insertion index. After the final reverse to produce LIFO order, an unrelated address that arrived between the unscoped and the scoped variant ended up ahead of the upgrade, breaking the recency guarantee. Pop the existing entry and re append so the later observation wins both in value and in LIFO position.
1 parent 9f8182b commit 8c356dd

2 files changed

Lines changed: 74 additions & 2 deletions

File tree

src/zeroconf/_services/info.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,10 @@ def _get_ip_addresses_from_cache_lifo(
496496
if existing_idx == -1:
497497
address_list.append(ip_addr)
498498
continue
499-
if _has_more_scope_info(ip_addr, address_list[existing_idx]):
500-
address_list[existing_idx] = ip_addr
499+
# Move the re-seen address to the end so the later observation
500+
# wins both in value (scope) and in LIFO position after reverse.
501+
existing = address_list.pop(existing_idx)
502+
address_list.append(ip_addr if _has_more_scope_info(ip_addr, existing) else existing)
501503
address_list.reverse() # Reverse to get LIFO order
502504
return address_list
503505

tests/services/test_info.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,76 @@ def test_has_more_scope_info_returns_false_for_ipv4():
937937
assert _has_more_scope_info(ip4, ip4) is False
938938

939939

940+
def test_scope_upgrade_preserves_lifo_recency_order():
941+
"""A scoped AAAA that upgrades an earlier entry becomes the most recent in LIFO order."""
942+
type_ = "_http._tcp.local."
943+
registration_name = f"reorder.{type_}"
944+
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
945+
host = "reorder.local."
946+
link_local = socket.inet_pton(socket.AF_INET6, "fe80::52e:c2f2:bc5f:e9c6")
947+
ula = socket.inet_pton(socket.AF_INET6, "fdc8:d776:7cca:46ed::2")
948+
949+
zeroconf.cache.async_add_records(
950+
[
951+
r.DNSPointer(
952+
type_,
953+
const._TYPE_PTR,
954+
const._CLASS_IN | const._CLASS_UNIQUE,
955+
120,
956+
registration_name,
957+
),
958+
r.DNSService(
959+
registration_name,
960+
const._TYPE_SRV,
961+
const._CLASS_IN | const._CLASS_UNIQUE,
962+
120,
963+
0,
964+
0,
965+
80,
966+
host,
967+
),
968+
r.DNSAddress(
969+
host,
970+
const._TYPE_AAAA,
971+
const._CLASS_IN | const._CLASS_UNIQUE,
972+
120,
973+
link_local,
974+
scope_id=None,
975+
),
976+
r.DNSAddress(
977+
host,
978+
const._TYPE_AAAA,
979+
const._CLASS_IN | const._CLASS_UNIQUE,
980+
120,
981+
ula,
982+
scope_id=None,
983+
),
984+
r.DNSAddress(
985+
host,
986+
const._TYPE_AAAA,
987+
const._CLASS_IN | const._CLASS_UNIQUE,
988+
120,
989+
link_local,
990+
scope_id=11,
991+
),
992+
]
993+
)
994+
995+
info = ServiceInfo(type_, registration_name)
996+
info.load_from_cache(zeroconf)
997+
# The scoped link-local upgrade is the most recent observation, so it
998+
# has to come first in LIFO order, ahead of the earlier unrelated ULA.
999+
assert info.ip_addresses_by_version(r.IPVersion.V6Only) == [
1000+
ip_address("fe80::52e:c2f2:bc5f:e9c6%11"),
1001+
ip_address("fdc8:d776:7cca:46ed::2"),
1002+
]
1003+
assert info.parsed_scoped_addresses() == [
1004+
"fe80::52e:c2f2:bc5f:e9c6%11",
1005+
"fdc8:d776:7cca:46ed::2",
1006+
]
1007+
zeroconf.close()
1008+
1009+
9401010
# This test uses asyncio because it needs to access the cache directly
9411011
# which is not threadsafe
9421012
@pytest.mark.asyncio

0 commit comments

Comments
 (0)