Skip to content

fix: bound record payload reads against rdlength overrun#1756

Merged
bdraco merged 2 commits into
masterfrom
bound_read_string_record_length
May 20, 2026
Merged

fix: bound record payload reads against rdlength overrun#1756
bdraco merged 2 commits into
masterfrom
bound_read_string_record_length

Conversation

@bdraco
Copy link
Copy Markdown
Member

@bdraco bdraco commented May 20, 2026

Summary

The TXT, HINFO, and A payload readers trusted the declared rdlength without checking it against the buffer; an attacker on the LAN could send a record with rdlength=65535 and only a few real bytes of payload, and we would silently truncate the slice and stuff the corrupt record into the cache. Adds the bounds check _read_string and _read_character_string should have had all along; the existing _read_others catch then skips the record, matching how malformed NSEC bitmaps already get rejected.

Details

Test plan

  • poetry run pytest tests/ (384 passed, 3 skipped)
  • poetry run pre-commit run --files src/zeroconf/_protocol/incoming.py tests/test_protocol.py
  • New regression tests in tests/test_protocol.py cover TXT, HINFO, and A record overrun.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 20, 2026

Merging this PR will not alter performance

✅ 14 untouched benchmarks


Comparing bound_read_string_record_length (c6111f9) with master (4ff6540)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (f4b5066) during the generation of this report, so 4ff6540 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.77%. Comparing base (b22c8ff) to head (c6111f9).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1756   +/-   ##
=======================================
  Coverage   99.77%   99.77%           
=======================================
  Files          33       33           
  Lines        3497     3508   +11     
  Branches      489      493    +4     
=======================================
+ Hits         3489     3500   +11     
  Misses          5        5           
  Partials        3        3           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bdraco bdraco marked this pull request as ready for review May 20, 2026 15:12
@bdraco bdraco requested a review from Copilot May 20, 2026 15:12
@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented May 20, 2026

@bluetoothbot review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens DNSIncoming record parsing to avoid silently accepting truncated/over-advertised RDATA payloads by adding explicit bounds checks in _read_string / _read_character_string, and adds regression tests for TXT, HINFO, and A record truncation cases.

Changes:

  • Add packet-end bounds checks to _read_string() and _read_character_string() to raise IncomingDecodeError on overrun.
  • Add regression tests ensuring malformed TXT/HINFO/A records do not surface in parsed answers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/zeroconf/_protocol/incoming.py Adds bounds checks to prevent slice-truncation from producing corrupted RDATA.
tests/test_protocol.py Adds tests for malformed/truncated TXT, HINFO, and A records.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/zeroconf/_protocol/incoming.py
Comment thread tests/test_protocol.py
@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented May 20, 2026

Good catch, addressed in c6111f9; added a per-record post-check in _read_others that drops the record and resyncs to the declared rdlength end when the inner reads consume a different number of bytes than the rdlength claimed, plus a regression test with a second valid record proving resync recovers it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@bluetoothbot
Copy link
Copy Markdown
Contributor

PR Review — fix: bound record payload reads against rdlength overrun

Solid fix. The two-layer approach — packet-level bounds in _read_string/_read_character_string plus the per-record self.offset != end post-check in _read_others — correctly addresses both the silent-truncation case (PR description) and the record-boundary-overrun case (raised by the Copilot reviewer and addressed in c6111f9). Tests cover TXT, HINFO, and A overruns plus the over-consume/resync case with a follow-on record that proves framing recovers. Type widths stay unsigned int per incoming.pxd, so the comparison stays a pure C unsigned compare in the Cython build with no sign-extension risk. Only nits are CLAUDE.md docstring style on two test cases and a defensive observation about end > _data_len resync; neither blocks merge.


🟢 Suggestions

1. Test docstrings retell production-side rationale (`tests/test_protocol.py`, L895-901)

Per CLAUDE.md: "Test docstrings retelling the production-side story... A test docstring should name what the test pins, in one sentence — not re-explain the bug, the fix, or the surrounding flow."

test_txt_rdlength_overruns_packet_rejected and test_record_consuming_more_than_rdlength_dropped_and_resyncs both have multi-line docstrings that re-explain the underlying Python slicing behavior, the failure mode, and the fix. The HINFO and A-record tests follow the convention with a single line — same treatment should apply to the other two.

Suggested form: keep just the first sentence ("""A TXT record with rdlength past the buffer must not enter the cache.""") and drop the rationale paragraphs — that history belongs in the PR body and the commit message, which already carry it.

    """A TXT record with rdlength past the buffer must not enter the cache.

    Python slicing silently truncates when the slice end exceeds the buffer,
    so without a bounds check in ``_read_string`` a malformed wire frame
    would land in the cache carrying a payload shorter than the rdlength
    declared, leaving the parser desynchronized for downstream records.
    """

Checklist

  • Input validation at boundaries (incoming wire data)
  • No silent truncation of attacker-controlled length fields
  • Error handling propagates to existing decode-skip path
  • No resource leaks introduced
  • Cython type signedness preserved (unsigned int end-to-end)
  • Test coverage for new branches (overrun + resync)
  • Test docstrings follow CLAUDE.md style — suggestion #1
  • Resync offset bounded against _data_len — suggestion #2

Summary

Solid fix. The two-layer approach — packet-level bounds in _read_string/_read_character_string plus the per-record self.offset != end post-check in _read_others — correctly addresses both the silent-truncation case (PR description) and the record-boundary-overrun case (raised by the Copilot reviewer and addressed in c6111f9). Tests cover TXT, HINFO, and A overruns plus the over-consume/resync case with a follow-on record that proves framing recovers. Type widths stay unsigned int per incoming.pxd, so the comparison stays a pure C unsigned compare in the Cython build with no sign-extension risk. Only nits are CLAUDE.md docstring style on two test cases and a defensive observation about end > _data_len resync; neither blocks merge.


Automated review by Kōand23b029
c6111f9

@bdraco bdraco merged commit 5444495 into master May 20, 2026
44 checks passed
@bdraco bdraco deleted the bound_read_string_record_length branch May 20, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: _read_string/_read_character_string skip remaining-byte validation, enabling silent record-data corruption

3 participants