diff --git a/docs/source/pcapkit/utilities/logging.rst b/docs/source/pcapkit/utilities/logging.rst index 4c69b8f434..59d6d3f783 100644 --- a/docs/source/pcapkit/utilities/logging.rst +++ b/docs/source/pcapkit/utilities/logging.rst @@ -55,9 +55,12 @@ flow tracing reports flows opening and closing. .. note:: - Nothing is logged from inside per-frame or per-field parsing loops, so + No record is logged for each frame parsed or each field read, so :data:`~logging.DEBUG` on a million-packet capture does not yield a million - records. Registration bookkeeping in :mod:`pcapkit.foundation.registry` is + records. What is logged per item is what gets dropped: the PyShark engine + logs a line for each tshark record it skips as not a packet + (``skipping tshark frame``), and reassembly logs a line for each buffer it + abandons on timeout (``abandoning buffer``). Registration bookkeeping in :mod:`pcapkit.foundation.registry` is at :data:`~logging.DEBUG` rather than :data:`~logging.INFO`, since a library announcing its own registry entries is not news to its consumer. diff --git a/pcapkit/protocols/misc/pcap/frame.py b/pcapkit/protocols/misc/pcap/frame.py index 04ffa35c6f..07a11a0bca 100644 --- a/pcapkit/protocols/misc/pcap/frame.py +++ b/pcapkit/protocols/misc/pcap/frame.py @@ -386,12 +386,14 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True, # snapshot length cut short the latter exceeds what the file holds, and # handing it over is the declared-length-exceeds-available-octets fault. # - # No test pins this line: reverting just this argument to ``frame.len`` - # leaves the whole suite green. ``big_endian.pcap``'s truncated frame - # dissects to ``Ethernet:IPv4:UDP:Raw``, and :meth:`Raw.read` ignores the - # ``length`` it is handed, so the over-long value never reaches anything - # that checks it. Catching a regression needs a fixture whose truncation - # lands in a length-checked field instead of bottoming out in ``Raw``. + # The schemas count their fields down from this length, so it is what + # lets a truncation be noticed: a frame cut inside its IPv4 header + # leaves IPv4 fewer octets than its header, and the count running below + # zero issues a ``SchemaWarning`` (``packet length < 0``). Handed the + # on-wire length, IPv4 would be told of octets the file does not hold + # and the truncation would pass silently. + # ``test_truncated_ipv4_header_warns_whatever_the_on_wire_length`` pins it + # (#2236). return self._decode_next_layer(frame, self._ghdr.network, frame.cap_len) def make(self, diff --git a/tests/protocols/misc/pcap/test_frame_read_cap_len_2236_unit.py b/tests/protocols/misc/pcap/test_frame_read_cap_len_2236_unit.py new file mode 100644 index 0000000000..f48b36ea18 --- /dev/null +++ b/tests/protocols/misc/pcap/test_frame_read_cap_len_2236_unit.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""A PCAP frame hands its dissector the captured length, not the on-wire one. + +This pins the length argument :meth:`Frame.read +` passes to ``_decode_next_layer`` +(#2236). The dissector is handed ``frame.cap_len``, the octets present, so a +frame the snapshot length cut inside its IPv4 header reports the truncation as a +:class:`~pcapkit.utilities.warnings.SchemaWarning` ``packet length < 0``. Handed +the on-wire ``frame.len`` instead, the schema believes the octets it lacks are +there and the truncation passes silently. + +Every case builds its own octets and reads no capture. Classes are imported +inside each test, after :func:`~tests._support.reimport_once_per_class`. + +""" + +import io +import struct +import unittest +import warnings + +from tests._support import reimport_once_per_class + +#: Little-endian global header: v2.4, snaplen 65535, Ethernet. +GLOBAL = bytes.fromhex('d4c3b2a1' '0200' '0400' '00000000' '00000000' 'ffff0000' '01000000') +#: An Ethernet header carrying IPv4, followed by the first 10 of the 20 octets of +#: an IPv4 header, i.e. a frame cut inside its IPv4 header. +PACKET = (bytes(6) + bytes(range(1, 7)) + b'\x08\x00' + + bytes.fromhex('4500008c000100004011')) +#: The on-wire length of the frame the snapshot length cut down to :data:`PACKET`. +ORIG_LEN = 154 + + +def _record(orig_len: 'int') -> 'bytes': + """A record holding :data:`PACKET` whose header claims ``orig_len`` on the wire.""" + return struct.pack(' None: + reimport_once_per_class(self) + + def schema_warnings(self, orig_len: 'int') -> 'list[str]': + """The :class:`SchemaWarning` messages from parsing the record for ``orig_len``.""" + from pcapkit.protocols.misc.pcap.frame import Frame + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.utilities.warnings import SchemaWarning + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + frame = Frame(io.BytesIO(_record(orig_len)), num=1, header=Header(GLOBAL).info) + self.assertEqual(frame.info.cap_len, len(PACKET)) + self.assertEqual(frame.info.len, orig_len) + return [str(item.message) for item in caught if issubclass(item.category, SchemaWarning)] + + def test_truncated_ipv4_header_warns_whatever_the_on_wire_length(self) -> None: + """The cut IPv4 header warns, the same as for an uncut frame of the same octets. + + IPv4 is handed the 10 octets Ethernet leaves of the captured 24, so its + header runs the remaining length to ``-10``. Were the on-wire 154 handed + over instead, IPv4 would be told of 140 octets and warn of nothing. + """ + truncated = self.schema_warnings(ORIG_LEN) + self.assertIn('packet length < 0: -10', truncated) + self.assertEqual(truncated, self.schema_warnings(len(PACKET))) + + +if __name__ == '__main__': + unittest.main()