From a1266d515e3ad04ee64662b80dabd0fab4948922 Mon Sep 17 00:00:00 2001 From: Muhtasim Munif Fahim Date: Tue, 27 Oct 2026 08:52:13 +0600 Subject: [PATCH] fix(pcapng): parse a raw bytes block given to make() (#2354) PCAPNG.unpack() overrides Protocol.unpack() without its handling of a made header that holds raw octets, so make(block=) reached read() with bytes where the block schema was expected and raised a bare AttributeError. Re-parse the packed octets as a block read from a capture would be, and raise ProtocolError when they describe no block. --- CHANGELOG.md | 1 + docs/source/changelog/1.5.0.rst | 5 ++ pcapkit/protocols/misc/pcapng.py | 17 +++++ .../test_pcapng_raw_bytes_block_2354_unit.py | 74 +++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 tests/protocols/misc/test_pcapng_raw_bytes_block_2354_unit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f58d8553c9..39c95de024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -350,6 +350,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - message text: the `epb_flags` and `pack_flags` length errors say `expected 4`, the length they check, rather than `expected 8` ([#2168](https://github.com/JarryShaw/PyPCAPKit/issues/2168), [#2179](https://github.com/JarryShaw/PyPCAPKit/pull/2179)), and the IPv4 TS option's unknown-flag warning and the Mobility Header's prefix error spell `timestamp` and `mobile` ([#2171](https://github.com/JarryShaw/PyPCAPKit/issues/2171), [#2189](https://github.com/JarryShaw/PyPCAPKit/pull/2189)). - IPX dissects its payload from a registry of its own, `IPX.__proto__`, keyed by IPX Packet Type, where it looked the Packet Type up as an IP protocol number, so a type 17 (NCP) payload parsed as `IPX:UDP` and type 4 as `IPX:IPv4`. No IPX-native protocol ships, so the payload is `Raw` unless one is registered ([#2169](https://github.com/JarryShaw/PyPCAPKit/issues/2169), [#2187](https://github.com/JarryShaw/PyPCAPKit/pull/2187)). - HIP's `ECHO_RESPONSE_UNSIGNED` maker builds an `EchoResponseUnsignedParameter` schema, where it built the request's; the octets were already right ([#2170](https://github.com/JarryShaw/PyPCAPKit/issues/2170), [#2188](https://github.com/JarryShaw/PyPCAPKit/pull/2188)). +- PCAP-NG's `make(block=)` parses the block out of the raw octets, as a block read from a capture is, where every block type raised a bare `AttributeError` (`'bytes' object has no attribute 'length'`) from `read()`; octets that hold no block raise `ProtocolError` ([#2354](https://github.com/JarryShaw/PyPCAPKit/issues/2354)). ### pcapkit.toolkit diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index f083f7c020..46bb01cf01 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -2755,6 +2755,11 @@ Fixed * HIP's ``ECHO_RESPONSE_UNSIGNED`` maker builds an ``EchoResponseUnsignedParameter`` schema, where it built the request's; the octets were already right (:issue:`2170`, :pr:`2188`). +* PCAP-NG's ``make(block=)`` parses the block out of the raw octets, as a + block read from a capture is, where every block type raised a bare + ``AttributeError`` (``'bytes' object has no attribute 'length'``) from + :meth:`~pcapkit.protocols.misc.pcapng.PCAPNG.read`; octets that hold no block raise + :exc:`.ProtocolError` (:issue:`2354`). pcapkit.toolkit --------------- diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 6f7e5f7b6b..9158f5429b 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -1261,6 +1261,23 @@ def unpack(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_PCAPN the packet data is not available in the current instance. """ + # NOTE: A block given to ``make()`` as raw :obj:`bytes` is packed + # verbatim, so the made header holds those octets where :meth:`read` + # expects the block schema. Parse the block back out of the packed + # octets instead, as a block read from a capture would be, so that + # ``make(block=)`` builds the block the octets describe, or + # fails with a :exc:`ProtocolError` when they describe none, as + # :meth:`Protocol.unpack ` + # does for a raw item it cannot parse (#2354). + header = cast('Optional[Schema_PCAPNG]', self.__header__) + block = cast('object', header.block) if header is not None else None + if isinstance(block, bytes): + self.__header__ = None # type: ignore[assignment] + try: + return self.unpack(length, **kwargs) + except (BaseError, struct.error) as error: # pylint: disable=no-member + raise ProtocolError(f'{type(self).__name__}: malformed raw block given to make(): {error}') from error + if cast('Optional[Schema_PCAPNG]', self.__header__) is None: packet = kwargs.get('__packet__', {}) # packet data diff --git a/tests/protocols/misc/test_pcapng_raw_bytes_block_2354_unit.py b/tests/protocols/misc/test_pcapng_raw_bytes_block_2354_unit.py new file mode 100644 index 0000000000..10a818c364 --- /dev/null +++ b/tests/protocols/misc/test_pcapng_raw_bytes_block_2354_unit.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2354: ``PCAPNG.make(block=)`` parses the raw block. + +``make()`` types ``block`` as accepting :obj:`bytes`, and packs a raw block +verbatim, but :meth:`~pcapkit.protocols.misc.pcapng.PCAPNG.read` then read the +made header, whose ``block`` was those :obj:`bytes` rather than a block schema, +and raised ``AttributeError: 'bytes' object has no attribute 'length'`` for +every block type. The raw block is now parsed out of the packed octets, as a +block read from a capture is, and octets that hold no block raise +:exc:`~pcapkit.utilities.exceptions.ProtocolError`. + +:mod:`pcapkit` is imported inside each test, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import struct +import unittest + +from tests.protocols.misc.test_pcapng_roundtrip2_unit import PCAPNGTestCase + +#: Secrets Type no registry names. +UNKNOWN_SECRETS = 0x12345678 + + +def _body(body: 'bytes') -> 'bytes': + """A little-endian block body around ``body``, after the Block Type.""" + body += bytes(-len(body) % 4) + total = 12 + len(body) + return struct.pack(' 'object': + from pcapkit.protocols.misc.pcapng import PCAPNG + + return PCAPNG(num=2, sct=1, ctx=self._context(), type=btype, block=block) + + def test_dsb_from_raw_bytes(self) -> None: + raw = _body(struct.pack(' None: + cases = { + 1: _body(struct.pack(' None: + from pcapkit.utilities.exceptions import ProtocolError + + for raw in (b'', bytes(8)): + with self.subTest(raw=raw): + with self.assertRaises(ProtocolError) as context: + self._make(10, raw) + self.assertIn('malformed raw block given to make()', str(context.exception)) + + +if __name__ == '__main__': + unittest.main()