From 0d0a152fdbf4b7dcf32c159463fe59bebff57243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20V=C3=A1zquez=20Blanco?= Date: Mon, 10 Aug 2026 14:53:23 +0200 Subject: [PATCH] layers/bluetooth: Add an HCI_Event_Vendor handler registration mechanism AI-Assisted: no --- scapy/layers/bluetooth.py | 71 ++++++++++++++++++++++++- test/scapy/layers/bluetooth.uts | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 8f6474f7d50..f308fefe45d 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -2762,12 +2762,81 @@ class HCI_Event_Vendor(Packet): Bluetooth Core 5.4, Vol 4, Part E, section 5.4.4 reserves 0xFF for vendor-specific debugging events; the format of the parameters is - vendor-defined, so the data is exposed as a raw byte string. + vendor-defined, so by default the parameters are exposed as a raw ``data`` + byte string. + + Several vendors reuse this single event code with incompatible parameter + layouts, so ``code=0xFF`` alone is not enough to pick a dissector and + :func:`~scapy.packet.bind_layers` cannot be used. Instead a vendor + registers a dissector with :meth:`register_handler`, providing a + ``check(body)`` that recognises its own parameter layout (typically a + fixed leading subcode). :meth:`dispatch_hook` then dissects a matching + body with that class, so several vendor contribs coexist. When no check + claims the body, ``HCI_Event_Vendor`` itself is used and the body stays in + ``data``, exactly as before. + + Handlers should subclass ``HCI_Event_Vendor`` and set ``match_subclass``, + so that ``HCI_Event_Vendor in pkt`` keeps matching vendor events:: + + class HCI_Event_Vendor_Foo(HCI_Event_Vendor): + name = "HCI_Vendor_Foo" + match_subclass = True + fields_desc = [XByteField("subcode", 0xa5), + ByteField("value", 0)] + + @classmethod + def check(cls, body): + return body[:1] == b"\\xa5" + + HCI_Event_Vendor.register_handler(HCI_Event_Vendor_Foo) """ name = "HCI_Vendor_Specific" fields_desc = [StrLenField("data", b"", length_from=lambda pkt: pkt.underlayer.len)] + registered_handlers = {} + + @classmethod + def register_handler(cls, handler_cls, check=None): + """ + Registers a vendor dissector for the ``code=0xFF`` event body. + + This event is shared across vendors with incompatible parameter + layouts, so a contrib cannot simply bind its layer to it. Instead each + vendor handler declares a ``check`` that returns True only for its own + body (typically by testing a fixed leading subcode) and registers + itself here. The first registered check that accepts a body wins, so + checks should be as specific as possible. Re-registering the same + ``handler_cls`` replaces its previous entry, so reloading a contrib is + idempotent. + + :param Type[scapy.packet.Packet] handler_cls: + A reference to a Packet subclass to dissect the event body with. + It should subclass ``HCI_Event_Vendor``. + :param Callable[[bytes], bool] check: + (optional) callable used to decide whether a body should be + associated with this handler. If not supplied, + ``handler_cls.check`` is used instead. + :raises TypeError: If ``check`` is not specified, + and ``handler_cls.check`` is not implemented. + """ + if check is None: + if hasattr(handler_cls, "check"): + check = handler_cls.check + else: + raise TypeError("check not specified, and {} has no " + "attribute check".format(handler_cls)) + + cls.registered_handlers[handler_cls] = check + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt: + for handler_cls, check in cls.registered_handlers.items(): + if check(_pkt): + return handler_cls + return cls + class HCI_Event_LE_Meta(Packet): """ diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index e3c179b9655..40bd78b8a2c 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -623,6 +623,100 @@ assert parsed[HCI_Event_Hdr].len == 4 assert parsed[HCI_Event_Vendor].data == b"\xde\xad\xbe\xef" ++ HCI_Event_Vendor handler registration (register_handler / dispatch_hook) + += register_handler routes a recognised body to the vendor handler layer +# The 0xff event is shared across vendors, so a contrib registers a handler +# whose check() recognises its own body (here a leading 0xa5 subcode). The +# dispatch_hook then dissects a matching body with that handler instead of +# HCI_Event_Vendor. Save the registry first so it can be restored. +_saved_vendor_handlers = dict(HCI_Event_Vendor.registered_handlers) + +class _VendorHandlerA(HCI_Event_Vendor): + name = "Vendor Handler A" + match_subclass = True + fields_desc = [XByteField("subcode", 0xa5), ByteField("value", 0)] + @classmethod + def check(cls, body): + return len(body) >= 1 and body[0] == 0xa5 + +HCI_Event_Vendor.register_handler(_VendorHandlerA) +assert _VendorHandlerA in HCI_Event_Vendor.registered_handlers + +evt = HCI_Hdr(hex_bytes("04" "ff" "02" "a542")) +assert _VendorHandlerA in evt +assert HCI_Event_Vendor in evt # match_subclass keeps this true +assert evt[HCI_Event_Vendor] is evt[_VendorHandlerA] +assert evt[_VendorHandlerA].subcode == 0xa5 +assert evt[_VendorHandlerA].value == 0x42 +assert raw(evt) == hex_bytes("04" "ff" "02" "a542") # round-trips unchanged + += A vendor handler can also be built and re-dissected +evt = HCI_Hdr() / HCI_Event_Hdr() / _VendorHandlerA(value=0x42) +assert raw(evt) == hex_bytes("04" "ff" "02" "a542") +assert HCI_Hdr(raw(evt))[_VendorHandlerA].value == 0x42 + += register_handler accepts an explicit check callable +# A handler need not define check() itself; the callable may be supplied. +class _VendorHandlerB(HCI_Event_Vendor): + name = "Vendor Handler B" + match_subclass = True + fields_desc = [StrField("body", b"")] + +HCI_Event_Vendor.register_handler(_VendorHandlerB, check=lambda body: body[:1] == b"\x5a") +evt = HCI_Hdr(hex_bytes("04" "ff" "03" "5abeef")) +assert _VendorHandlerB in evt +assert _VendorHandlerA not in evt # A's check does not claim it +assert evt[_VendorHandlerB].body == b"\x5a\xbe\xef" + += Two registered handlers dispatch independently by content +# With both handlers loaded, each body is routed to the handler that claims it. +evt_a = HCI_Hdr(hex_bytes("04" "ff" "02" "a542")) +assert _VendorHandlerA in evt_a and _VendorHandlerB not in evt_a +evt_b = HCI_Hdr(hex_bytes("04" "ff" "03" "5abeef")) +assert _VendorHandlerB in evt_b and _VendorHandlerA not in evt_b + += An unrecognised 0xff body still falls back to the raw data field +# No registered check matches, so the historical raw-``data`` behaviour holds. +evt = HCI_Hdr(hex_bytes("04" "ff" "02" "0102")) +assert type(evt[HCI_Event_Vendor]) is HCI_Event_Vendor +assert _VendorHandlerA not in evt +assert _VendorHandlerB not in evt +assert evt[HCI_Event_Vendor].data == b"\x01\x02" +assert raw(evt) == hex_bytes("04" "ff" "02" "0102") + += dispatch_hook only applies to dissection, not to explicit construction +# Without bytes to inspect there is nothing to check, so the generic class is +# kept and ``HCI_Event_Vendor(data=...)`` still builds a generic event. +assert type(HCI_Event_Vendor()) is HCI_Event_Vendor +assert type(HCI_Event_Vendor(data=b"\xa5\x42")) is HCI_Event_Vendor + += register_handler is idempotent when the same handler is re-registered +# Keyed by handler class, so reloading a contrib does not duplicate entries. +_n = len(HCI_Event_Vendor.registered_handlers) +HCI_Event_Vendor.register_handler(_VendorHandlerA) +assert len(HCI_Event_Vendor.registered_handlers) == _n + += register_handler raises TypeError when no check is available +class _VendorNoCheck(Packet): + name = "Vendor No Check" + fields_desc = [] + +try: + HCI_Event_Vendor.register_handler(_VendorNoCheck) + assert False, "expected TypeError for a handler without a check" +except TypeError: + pass + +assert _VendorNoCheck not in HCI_Event_Vendor.registered_handlers + += Restore the HCI_Event_Vendor handler registry +# Undo the test registrations so later tests see a clean registry. +HCI_Event_Vendor.registered_handlers.clear() +HCI_Event_Vendor.registered_handlers.update(_saved_vendor_handlers) +assert HCI_Event_Vendor.registered_handlers == _saved_vendor_handlers + + + Bluetooth LE Advertising / Scan Response Data Parsing = Parse EIR_IncompleteList32BitServiceUUIDs