fix(unmarshaller): cap message size at 128 MiB per d-bus spec#653
Conversation
A forged header claiming body_len=0xFFFFFFFF previously caused _read_to_pos to attempt buffering ~4 GiB before validation, enabling a remote DoS against any subscriber on a shared bus (BlueZ-adjacent services, NetworkManager clients, etc.). Validate _body_len and _header_len in _read_header before any allocation or recvmsg sized by them, raising InvalidMessageError if either field — or their sum — exceeds 128 MiB. closes #641
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #653 +/- ##
==========================================
- Coverage 89.16% 89.11% -0.05%
==========================================
Files 29 29
Lines 3488 3492 +4
Branches 602 603 +1
==========================================
+ Hits 3110 3112 +2
- Misses 228 229 +1
- Partials 150 151 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@bluetoothbot review |
There was a problem hiding this comment.
Pull request overview
This PR hardens dbus_fast’s unmarshaller against attacker-controlled length fields by enforcing the D-Bus 128 MiB message-size cap early in header parsing, preventing oversized buffering/reads and mitigating a remote memory-exhaustion DoS (issue #641).
Changes:
- Add a shared 128 MiB
MAX_MESSAGE_SIZEconstant (plus a Cython-typed_MAX_MESSAGE_SIZE) in the unmarshaller. - Validate
body_len,header_len, and their combination in_read_header()and raiseInvalidMessageErrorwhen exceeding the cap. - Add tests that forge minimal headers and assert oversized lengths are rejected.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/dbus_fast/_private/unmarshaller.py |
Introduces the max-size constants and performs early header/body length validation. |
src/dbus_fast/_private/unmarshaller.pxd |
Declares _MAX_MESSAGE_SIZE for Cython compilation. |
tests/test_marshaller.py |
Adds tests that forge headers to confirm oversized length fields are rejected. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot review on #653 flagged that the cap on body_len/header_len doesn't account for HEADER_SIGNATURE_SIZE (16) + up to 7 bytes of header-to-body alignment padding, so the actual recv is up to 23 bytes over the 128 MiB ceiling. The DoS vector is gigabyte-scale buffering — 23 bytes of slack doesn't change that, and the simpler check is easier to reason about. Document the trade-off in place.
PR Review — fix(unmarshaller): cap message size at 128 MiB per d-bus specTargeted, well-documented DoS fix. The dual-name 🟢 Suggestions1. Consider adding a boundary-pass test at the exact cap (`tests/test_marshaller.py`, L1017)The four new tests all assert rejection above the cap. A complementary positive test — e.g. 2. Short-circuit ordering covers overflow but worth a note (`src/dbus_fast/_private/unmarshaller.py`, L811-820)Minor: the three-way OR is correct because the Checklist
SummaryTargeted, well-documented DoS fix. The dual-name |
What
Enforce the D-Bus spec 128 MiB total-message-size cap in the unmarshaller.
_read_headernow validatesbody_lenandheader_len(and their sum) immediately after reading them, before_msg_lenis computed and before_read_to_poscan size an allocation orrecvmsgfrom those untrusted values.Why
_read_headerpreviously consumed both length fields as rawuint32s from the wire, then handedHEADER_SIGNATURE_SIZE + _msg_lento_read_to_pos. A single 28-byte forged header claimingbody_len = 0xFFFFFFFFforced the victim to attempt to buffer ~4 GiB before any validation — a remote DoS against any peer that subscribes to messages on a shared system bus.Captured as #641; the TODO at
message.py:324noted the missing cap but only on the outbound (marshal) side.How
MAX_MESSAGE_SIZE = 134_217_728(Python-importable) and_MAX_MESSAGE_SIZE(cdef'dunsigned int) so the bounds check compiles to a native C compare on the hot path._read_header, raiseInvalidMessageErrorif either field — or their sum — exceeds the cap. The check runs before_msg_lenis computed, so no allocation or socket read is ever attempted at the attacker-chosen size.Tests
Four new tests in
tests/test_marshaller.pybuild forged 16-byte headers (no body bytes follow) and assertInvalidMessageErroris raised — the absence of a body proves validation runs before any body read:test_unmarshall_rejects_oversized_body_lentest_unmarshall_rejects_oversized_header_lentest_unmarshall_rejects_oversized_combined_sizetest_unmarshall_rejects_max_uint32_body_len_big_endian— the originalbody_len=0xFFFFFFFFpayload from the issuecloses #641