Skip to content

Commit 85ac2ae

Browse files
committed
refactor: share _mark_seen helper between _logger and _protocol.incoming
Lift the bound-check + eviction logic out of the four QuietLogger classmethods and the parser-side `_log_exception_debug` and into a single `_mark_seen(seen, key)` in `_logger.py`. Both call sites still keep their own `_seen_logs` set (no behavior change vs the previous commit — there are still two independent dedup pools, each capped at `_MAX_SEEN_LOGS`); only the eviction policy is now defined in one place. `_MAX_SEEN_LOGS` likewise moves to `_logger.py` only.
1 parent 42220db commit 85ac2ae

4 files changed

Lines changed: 31 additions & 39 deletions

File tree

src/zeroconf/_logger.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
import logging
2727
import sys
28-
from typing import Any, ClassVar
28+
from typing import Any
2929

3030
log = logging.getLogger(__name__.split(".", maxsplit=1)[0])
3131
log.addHandler(logging.NullHandler())
@@ -40,41 +40,41 @@ def set_logger_level_if_unset() -> None:
4040

4141

4242
_MAX_SEEN_LOGS = 256
43+
_seen_logs: set[str] = set()
4344

4445

45-
class QuietLogger:
46-
_seen_logs: ClassVar[set[str]] = set()
46+
def _mark_seen(seen: set[str], key: str) -> bool:
47+
"""Record ``key`` in ``seen`` and return True if it was newly added.
4748
48-
@classmethod
49-
def _mark_seen(cls, key: str) -> bool:
50-
"""Record ``key`` and return True if it was newly added."""
51-
if key in cls._seen_logs:
52-
return False
53-
# Keys can carry caller-supplied fields (peer addresses, packet
54-
# offsets); clear when full so a malicious peer can't grow the
55-
# set without bound.
56-
if len(cls._seen_logs) >= _MAX_SEEN_LOGS:
57-
cls._seen_logs.clear()
58-
cls._seen_logs.add(key)
59-
return True
49+
Bounds the set so callers passing attacker-influenced keys (peer
50+
addresses, packet offsets) cannot grow it without bound.
51+
"""
52+
if key in seen:
53+
return False
54+
if len(seen) >= _MAX_SEEN_LOGS:
55+
seen.clear()
56+
seen.add(key)
57+
return True
6058

59+
60+
class QuietLogger:
6161
@classmethod
6262
def log_exception_warning(cls, *logger_data: Any) -> None:
63-
first_time = cls._mark_seen(str(sys.exc_info()[1]))
63+
first_time = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
6464
logger = log.warning if first_time else log.debug
6565
logger(*(logger_data or ["Exception occurred"]), exc_info=True)
6666

6767
@classmethod
6868
def log_exception_debug(cls, *logger_data: Any) -> None:
69-
first_time = cls._mark_seen(str(sys.exc_info()[1]))
69+
first_time = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
7070
log.debug(*(logger_data or ["Exception occurred"]), exc_info=first_time)
7171

7272
@classmethod
7373
def log_warning_once(cls, *args: Any) -> None:
74-
logger = log.warning if cls._mark_seen(args[0]) else log.debug
74+
logger = log.warning if _mark_seen(_seen_logs, args[0]) else log.debug
7575
logger(*args)
7676

7777
@classmethod
7878
def log_exception_once(cls, exc: Exception, *args: Any) -> None:
79-
logger = log.warning if cls._mark_seen(args[0]) else log.debug
79+
logger = log.warning if _mark_seen(_seen_logs, args[0]) else log.debug
8080
logger(*args, exc_info=exc)

src/zeroconf/_protocol/incoming.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
DNSText,
3838
)
3939
from .._exceptions import IncomingDecodeError
40-
from .._logger import log
40+
from .._logger import _mark_seen, log
4141
from .._utils.time import current_time_millis
4242
from ..const import (
4343
_FLAGS_QR_MASK,
@@ -63,7 +63,6 @@
6363
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError)
6464

6565

66-
_MAX_SEEN_LOGS = 256
6766
_seen_logs: set[str] = set()
6867
_str = str
6968
_int = int
@@ -183,16 +182,7 @@ def _initial_parse(self) -> None:
183182

184183
@classmethod
185184
def _log_exception_debug(cls, *logger_data: Any) -> None:
186-
log_exc_info = False
187-
exc_str = str(sys.exc_info()[1])
188-
if exc_str not in _seen_logs:
189-
# The dedup key embeds attacker-controlled fields (peer IP/port,
190-
# byte offsets); clear when full so a malicious peer can't grow
191-
# the set without bound.
192-
if len(_seen_logs) >= _MAX_SEEN_LOGS:
193-
_seen_logs.clear()
194-
_seen_logs.add(exc_str)
195-
log_exc_info = True
185+
log_exc_info = _mark_seen(_seen_logs, str(sys.exc_info()[1]))
196186
log.debug(*(logger_data or ["Exception occurred"]), exc_info=log_exc_info)
197187

198188
def answers(self) -> list[DNSRecord]:

tests/test_logger.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
from unittest.mock import call, patch
77

8+
from zeroconf import _logger
89
from zeroconf._logger import _MAX_SEEN_LOGS, QuietLogger, set_logger_level_if_unset
910

1011

@@ -25,7 +26,7 @@ def test_loading_logger():
2526

2627
def test_log_warning_once():
2728
"""Test we only log with warning level once."""
28-
QuietLogger._seen_logs = set()
29+
_logger._seen_logs.clear()
2930
quiet_logger = QuietLogger()
3031
with (
3132
patch("zeroconf._logger.log.warning") as mock_log_warning,
@@ -48,7 +49,7 @@ def test_log_warning_once():
4849

4950
def test_log_exception_warning():
5051
"""Test we only log with warning level once."""
51-
QuietLogger._seen_logs = set()
52+
_logger._seen_logs.clear()
5253
quiet_logger = QuietLogger()
5354
with (
5455
patch("zeroconf._logger.log.warning") as mock_log_warning,
@@ -71,7 +72,7 @@ def test_log_exception_warning():
7172

7273
def test_llog_exception_debug():
7374
"""Test we only log with a trace once."""
74-
QuietLogger._seen_logs = set()
75+
_logger._seen_logs.clear()
7576
quiet_logger = QuietLogger()
7677
with patch("zeroconf._logger.log.debug") as mock_log_debug:
7778
quiet_logger.log_exception_debug("the exception")
@@ -86,16 +87,16 @@ def test_llog_exception_debug():
8687

8788
def test_seen_logs_is_bounded():
8889
"""Distinct keys must not grow ``_seen_logs`` without bound."""
89-
QuietLogger._seen_logs = set()
90+
_logger._seen_logs.clear()
9091
with patch("zeroconf._logger.log.warning"), patch("zeroconf._logger.log.debug"):
9192
for i in range(_MAX_SEEN_LOGS + 5):
9293
QuietLogger.log_warning_once(f"warning-{i}")
93-
assert len(QuietLogger._seen_logs) <= _MAX_SEEN_LOGS
94+
assert len(_logger._seen_logs) <= _MAX_SEEN_LOGS
9495

9596

9697
def test_log_exception_once():
9798
"""Test we only log with warning level once."""
98-
QuietLogger._seen_logs = set()
99+
_logger._seen_logs.clear()
99100
quiet_logger = QuietLogger()
100101
exc = Exception()
101102
with (

tests/test_protocol.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import zeroconf as r
1616
from zeroconf import DNSHinfo, DNSIncoming, DNSText, const, current_time_millis
17+
from zeroconf._logger import _MAX_SEEN_LOGS
1718
from zeroconf._protocol import incoming as _incoming_module
1819

1920
from . import has_working_ipv6
@@ -971,9 +972,9 @@ def test_seen_logs_is_bounded():
971972
b"\x00\x01\x00\x04\xc0\xa8\xd0\x06"
972973
)
973974
_incoming_module._seen_logs.clear()
974-
for port in range(_incoming_module._MAX_SEEN_LOGS + 5):
975+
for port in range(_MAX_SEEN_LOGS + 5):
975976
r.DNSIncoming(packet, ("1.2.3.4", port))
976-
assert len(_incoming_module._seen_logs) <= _incoming_module._MAX_SEEN_LOGS
977+
assert len(_incoming_module._seen_logs) <= _MAX_SEEN_LOGS
977978

978979

979980
def test_label_length_attack():

0 commit comments

Comments
 (0)