From 0a78e7506b4d6ee8e87af6af80e3b5a5c916bf8c Mon Sep 17 00:00:00 2001 From: Ievgen Bondarenko Date: Thu, 16 Apr 2026 01:43:41 -0700 Subject: [PATCH 1/2] fix: add input validation to prevent remote crash from empty/malformed PDUs Add length checks in from_bytes() for ATT and SMP protocol parsers to prevent IndexError crashes from empty PDUs sent by remote Bluetooth devices. Also add buffer size limit and UTF-8 error handling in HFP protocol to prevent memory exhaustion and decode crashes. - bumble/att.py: validate PDU is non-empty before accessing pdu[0] - bumble/smp.py: validate PDU is non-empty before accessing pdu[0] - bumble/hfp.py: limit buffer to 64KB, handle invalid UTF-8 gracefully These issues can be triggered by a remote Bluetooth device sending malformed packets, causing denial of service on the host. --- bumble/att.py | 2 ++ bumble/hfp.py | 5 ++++- bumble/smp.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bumble/att.py b/bumble/att.py index 07ebe86..49c3130 100644 --- a/bumble/att.py +++ b/bumble/att.py @@ -249,6 +249,8 @@ class ATT_PDU: @classmethod def from_bytes(cls, pdu: bytes) -> ATT_PDU: + if not pdu: + raise ValueError("Empty ATT PDU") op_code = pdu[0] subclass = ATT_PDU.pdu_classes.get(op_code) diff --git a/bumble/hfp.py b/bumble/hfp.py index 7056517..6b8fbb0 100644 --- a/bumble/hfp.py +++ b/bumble/hfp.py @@ -84,12 +84,15 @@ class HfpProtocol: def feed(self, data: bytes | str) -> None: # Convert the data to a string if needed if isinstance(data, bytes): - data = data.decode('utf-8') + data = data.decode('utf-8', errors='replace') logger.debug(f'<<< Data received: {data}') # Add to the buffer and look for lines self.buffer += data + if len(self.buffer) > 65536: + logger.warning("HFP buffer overflow, truncating") + self.buffer = self.buffer[-65536:] while (separator := self.buffer.find('\r')) >= 0: line = self.buffer[:separator].strip() self.buffer = self.buffer[separator + 1 :] diff --git a/bumble/smp.py b/bumble/smp.py index 0f40094..da1c771 100644 --- a/bumble/smp.py +++ b/bumble/smp.py @@ -215,6 +215,8 @@ class SMP_Command: @classmethod def from_bytes(cls, pdu: bytes) -> SMP_Command: + if not pdu: + raise ValueError("Empty SMP PDU") code = CommandCode(pdu[0]) subclass = SMP_Command.smp_classes.get(code) From 444f43f6a3a315912f4c11313ad8f69ffd71aea4 Mon Sep 17 00:00:00 2001 From: Ievgen Bondarenko Date: Thu, 16 Apr 2026 11:24:09 -0700 Subject: [PATCH 2/2] fix: address review feedback - use InvalidPacketError and abort on buffer overflow - att.py: raise core.InvalidPacketError instead of generic ValueError - smp.py: raise core.InvalidPacketError instead of generic ValueError - hfp.py: add MAX_BUFFER_SIZE class constant (64KB) - hfp.py: drop incoming data when it would overflow buffer instead of truncating, preserving existing partial-packet state Per review comments on PR #912 by @zxzxwu. --- bumble/att.py | 4 ++-- bumble/hfp.py | 14 +++++++++++--- bumble/smp.py | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/bumble/att.py b/bumble/att.py index 49c3130..688321b 100644 --- a/bumble/att.py +++ b/bumble/att.py @@ -42,7 +42,7 @@ from typing_extensions import TypeIs from bumble import hci, l2cap, utils from bumble.colors import color -from bumble.core import UUID, InvalidOperationError, ProtocolError +from bumble.core import UUID, InvalidOperationError, InvalidPacketError, ProtocolError from bumble.hci import HCI_Object # ----------------------------------------------------------------------------- @@ -250,7 +250,7 @@ class ATT_PDU: @classmethod def from_bytes(cls, pdu: bytes) -> ATT_PDU: if not pdu: - raise ValueError("Empty ATT PDU") + raise InvalidPacketError("Empty ATT PDU") op_code = pdu[0] subclass = ATT_PDU.pdu_classes.get(op_code) diff --git a/bumble/hfp.py b/bumble/hfp.py index 6b8fbb0..7a782e7 100644 --- a/bumble/hfp.py +++ b/bumble/hfp.py @@ -68,6 +68,8 @@ class HfpProtocolError(ProtocolError): # ----------------------------------------------------------------------------- class HfpProtocol: + MAX_BUFFER_SIZE: ClassVar[int] = 65536 + dlc: rfcomm.DLC buffer: str lines: collections.deque @@ -88,11 +90,17 @@ class HfpProtocol: logger.debug(f'<<< Data received: {data}') + # Drop incoming data if it would overflow the buffer; keep existing + # partial packet state intact so a future clean packet can still parse. + if len(self.buffer) + len(data) > self.MAX_BUFFER_SIZE: + logger.warning( + 'HFP buffer overflow (>%d bytes), dropping incoming data', + self.MAX_BUFFER_SIZE, + ) + return + # Add to the buffer and look for lines self.buffer += data - if len(self.buffer) > 65536: - logger.warning("HFP buffer overflow, truncating") - self.buffer = self.buffer[-65536:] while (separator := self.buffer.find('\r')) >= 0: line = self.buffer[:separator].strip() self.buffer = self.buffer[separator + 1 :] diff --git a/bumble/smp.py b/bumble/smp.py index da1c771..a6b213e 100644 --- a/bumble/smp.py +++ b/bumble/smp.py @@ -36,6 +36,7 @@ from bumble.colors import color from bumble.core import ( AdvertisingData, InvalidArgumentError, + InvalidPacketError, PhysicalTransport, ProtocolError, ) @@ -216,7 +217,7 @@ class SMP_Command: @classmethod def from_bytes(cls, pdu: bytes) -> SMP_Command: if not pdu: - raise ValueError("Empty SMP PDU") + raise InvalidPacketError("Empty SMP PDU") code = CommandCode(pdu[0]) subclass = SMP_Command.smp_classes.get(code)