Skip to content

Commit ca4a3ce

Browse files
authored
fix(link): a GRE frame cut short by the capture reads TRUNCATED (#2653) (#2668)
- pcapkit.utilities.contextvars: frame_length holds the captured and the original length of the frame being dissected; Frame and PCAPNG set it, Internet clears it and GRE reads it, so it is shared under #2616 - Frame.read, and PCAPNG._read_block_epb, _read_block_spb and _read_block_packet, set it around the parse of their payload - Internet._decode_next_layer clears it around its payload's parse, as the length an IP layer hands on bounds that payload - GRE.read hands verify_message its length plus what the capture cut off the frame, since behind LinuxSLL the message runs to the frame's end - ICMPv4 and IGMP keep the length their IP layer declares, which gives TRUNCATED for a cut message; the frame's would misread a cut in padding - Docs: the contextvars page lists frame_length Tests: test_gre_snaplen_cut_2653_unit fails without the fix, and its cases behind IP fail without the Internet reset; the guard, GRE, IP, frame and PCAP-NG modules pass.
1 parent 65a2b41 commit ca4a3ce

7 files changed

Lines changed: 423 additions & 15 deletions

File tree

‎docs/source/pcapkit/utilities/contextvars.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ a module at any layer may import it; see :ref:`utilities-or-corekit`.
1717
.. autodata:: pcapkit.utilities.contextvars.frame_depth
1818
:no-value:
1919

20+
.. autodata:: pcapkit.utilities.contextvars.frame_length
21+
:no-value:
22+
2023
.. autodata:: pcapkit.utilities.contextvars.rebuilding
2124
:no-value:

‎pcapkit/protocols/internet/internet.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
2525
from pcapkit.protocols.schema.schema import keep_short_read, replay_short_read
2626
from pcapkit.utilities import config
27-
from pcapkit.utilities.contextvars import rebuilding
27+
from pcapkit.utilities.contextvars import frame_length, rebuilding
2828
from pcapkit.utilities.exceptions import stacklevel
2929
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn
3030

@@ -471,10 +471,21 @@ def _decode_next_layer(self, dict_: '_PT', proto: 'Optional[int]' = None, # pyl
471471
:meth:`Info.to_dict <pcapkit.corekit.infoclass.Info.to_dict>`
472472
carries and the dumpers leave out.
473473
474+
The next layer is parsed with
475+
:data:`~pcapkit.utilities.contextvars.frame_length` cleared, since
476+
this layer, not the frame, bounds its payload (:issue:`2653`).
477+
474478
"""
475-
next_ = cast('ProtocolBase', # type: ignore[redundant-cast]
476-
self._import_next_layer(proto, length, packet=packet, version=version,
477-
payload=payload)) # type: ignore[misc,call-arg]
479+
# NOTE: The length this layer hands on bounds the payload, so octets the
480+
# capture cut off the frame past it are none of the next layer's (#2653).
481+
# An IPv4 Total Length of 0, read as the rest of the frame, loses the cut.
482+
token = frame_length.set(None)
483+
try:
484+
next_ = cast('ProtocolBase', # type: ignore[redundant-cast]
485+
self._import_next_layer(proto, length, packet=packet, version=version,
486+
payload=payload)) # type: ignore[misc,call-arg]
487+
finally:
488+
frame_length.reset(token)
478489
info, chain = next_.info, next_.protochain
479490

480491
# make next layer protocol name

‎pcapkit/protocols/link/gre.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from pcapkit.protocols.link.link import Link
7979
from pcapkit.protocols.schema.link.gre import GRE as Schema_GRE
8080
from pcapkit.utilities import config
81+
from pcapkit.utilities.contextvars import frame_length
8182
from pcapkit.utilities.exceptions import ProtocolError
8283

8384
if TYPE_CHECKING:
@@ -108,9 +109,11 @@ class GRE(Link[Data_GRE, Schema_GRE],
108109
:data:`~pcapkit.utilities.config.CHECKSUM_VERIFY`, and computed by
109110
:meth:`make` when ``checksum_present`` asks for one without giving it.
110111
Neither this header nor the Linux cooked-capture header before it states
111-
a length, so the octets verified are the ones captured: a frame the
112-
capture cut short reads as
113-
:attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.INVALID`.
112+
a length, so the message runs to the end of its frame. A frame the
113+
capture cut short, holding fewer octets than its original length, reads
114+
as :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.TRUNCATED`,
115+
as :data:`~pcapkit.utilities.contextvars.frame_length` tells it
116+
(:issue:`2653`).
114117
115118
"""
116119

@@ -221,7 +224,13 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_GRE':
221224
if _checksum is None:
222225
status = ChecksumStatus.NOT_COMPUTED
223226
else:
224-
status = verify_message(self.alias, self._data, _checksum.to_bytes(2, 'big'), length=length)
227+
# NOTE: Behind LinuxSLL the message runs to the end of its frame, so
228+
# what the capture cut off is covered too. Behind IP the value is None:
229+
# Internet clears it, as the length IP declares bounds GRE (#2653).
230+
lengths = frame_length.get()
231+
missing = 0 if lengths is None else max(lengths[1] - lengths[0], 0)
232+
status = verify_message(self.alias, self._data, _checksum.to_bytes(2, 'big'),
233+
length=length + missing)
225234
gre.__update__([
226235
('checksum_status', status),
227236
])

‎pcapkit/protocols/misc/pcap/frame.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from pcapkit.protocols.schema.misc.pcap.frame import Frame as Schema_Frame
4949
from pcapkit.protocols.schema.misc.pcap.header import RESOLUTION
5050
from pcapkit.utilities.compat import localcontext
51+
from pcapkit.utilities.contextvars import frame_length
5152
from pcapkit.utilities.datetime import fromtimestamp, totimestamp
5253
from pcapkit.utilities.exceptions import StreamEOFError, UnsupportedCall, stacklevel
5354
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn
@@ -419,7 +420,14 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True,
419420
# (#2308). As the dissection does not tell the two apart,
420421
# ``test_truncated_ipv4_header_is_raw_with_its_error`` pins the argument
421422
# where it is passed (#2236).
422-
return self._decode_next_layer(frame, self._ghdr.network, frame.cap_len)
423+
#
424+
# NOTE: A layer that states no length cannot see the cut, so the layers
425+
# below are told both lengths (#2653).
426+
token = frame_length.set((_ilen, _olen))
427+
try:
428+
return self._decode_next_layer(frame, self._ghdr.network, frame.cap_len)
429+
finally:
430+
frame_length.reset(token)
423431

424432
def make(self,
425433
timestamp: 'Optional[float | Decimal | int | dt_type]' = None,

‎pcapkit/protocols/misc/pcapng.py‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
from pcapkit.protocols.schema.misc.pcapng import zigbee_key_held
212212
from pcapkit.protocols.schema.schema import Schema
213213
from pcapkit.utilities.compat import StrEnum, localcontext
214+
from pcapkit.utilities.contextvars import frame_length
214215
from pcapkit.utilities.datetime import fromtimestamp, totimestamp
215216
from pcapkit.utilities.exceptions import (BaseError, EnumValueError, FormatError, ProtocolError,
216217
StreamEOFError, UnsupportedCall, stacklevel)
@@ -2666,8 +2667,13 @@ def _read_block_epb(self, schema: 'Schema_EnhancedPacketBlock', *,
26662667
warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_len} runs past '
26672668
f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured',
26682669
ProtocolWarning, stacklevel=stacklevel())
2669-
return self._decode_next_layer(data, self._get_linktype(schema.interface_id),
2670-
schema.captured_len) # type: ignore[return-value]
2670+
# NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653).
2671+
token = frame_length.set((data.captured_len, data.original_len))
2672+
try:
2673+
return self._decode_next_layer(data, self._get_linktype(schema.interface_id),
2674+
schema.captured_len) # type: ignore[return-value]
2675+
finally:
2676+
frame_length.reset(token)
26712677

26722678
def _read_block_spb(self, schema: 'Schema_SimplePacketBlock', *,
26732679
header: 'Schema_PCAPNG') -> 'Data_SimplePacketBlock':
@@ -2728,7 +2734,12 @@ def _read_block_spb(self, schema: 'Schema_SimplePacketBlock', *,
27282734
warn(f'PCAP-NG: [Block {header.type}] packet data of {data.captured_len} octet(s) '
27292735
f'exceeds the original length of {data.original_len}; kept as captured',
27302736
ProtocolWarning, stacklevel=stacklevel())
2731-
return self._decode_next_layer(data, self._get_linktype(0), data.captured_len) # type: ignore[return-value]
2737+
# NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653).
2738+
token = frame_length.set((data.captured_len, data.original_len))
2739+
try:
2740+
return self._decode_next_layer(data, self._get_linktype(0), data.captured_len) # type: ignore[return-value]
2741+
finally:
2742+
frame_length.reset(token)
27322743

27332744
def _read_block_nrb(self, schema: 'Schema_NameResolutionBlock', *,
27342745
header: 'Schema_PCAPNG') -> 'Data_NameResolutionBlock':
@@ -3065,8 +3076,13 @@ def _read_block_packet(self, schema: 'Schema_PacketBlock', *,
30653076
warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_length} runs past '
30663077
f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured',
30673078
ProtocolWarning, stacklevel=stacklevel())
3068-
return self._decode_next_layer(data, self._get_linktype(schema.interface_id),
3069-
schema.captured_length) # type: ignore[return-value]
3079+
# NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653).
3080+
token = frame_length.set((data.captured_len, data.original_len))
3081+
try:
3082+
return self._decode_next_layer(data, self._get_linktype(schema.interface_id),
3083+
schema.captured_length) # type: ignore[return-value]
3084+
finally:
3085+
frame_length.reset(token)
30703086

30713087
def _read_pcapng_options(self, options_schema: 'list[Schema_Option]') -> 'Option':
30723088
"""Read PCAP-NG options.

‎pcapkit/utilities/contextvars.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
1515
"""
1616
import contextvars
17+
from typing import TYPE_CHECKING
1718

18-
__all__ = ['beholding', 'frame_depth', 'rebuilding']
19+
if TYPE_CHECKING:
20+
from typing import Optional
21+
22+
__all__ = ['beholding', 'frame_depth', 'frame_length', 'rebuilding']
1923

2024
#: contextvars.ContextVar[bool]: Whether a layer that raises is discarded by its
2125
#: caller, as :func:`~pcapkit.corekit.decorators.beholder` replaces it and
@@ -31,6 +35,18 @@
3135
#: :data:`~pcapkit.utilities.config.FRAME_LAYER_LIMIT`.
3236
frame_depth = contextvars.ContextVar('pcapkit.utilities.contextvars.frame_depth', default=0)
3337

38+
#: contextvars.ContextVar[tuple[int, int] | None]: The captured and the original
39+
#: length of the frame being dissected, in octets, as its PCAP record or PCAP-NG
40+
#: packet block states them; :data:`None` outside a frame, and inside the payload
41+
#: of an :class:`~pcapkit.protocols.internet.internet.Internet` layer, which
42+
#: bounds it. A frame the capture cut short holds fewer octets than were on the
43+
#: wire, which a layer stating no length of its own, as
44+
#: :class:`~pcapkit.protocols.link.gre.GRE`, cannot tell from the octets it is
45+
#: handed (:issue:`2653`).
46+
frame_length = contextvars.ContextVar(
47+
'pcapkit.utilities.contextvars.frame_length', default=None,
48+
) # type: contextvars.ContextVar[Optional[tuple[int, int]]]
49+
3450
#: contextvars.ContextVar[bool]: Whether a payload is being rebuilt by
3551
#: :meth:`Protocol._make_payload
3652
#: <pcapkit.protocols.protocol.Protocol._make_payload>`, so that the layer

0 commit comments

Comments
 (0)