Skip to content

Commit b14b04e

Browse files
committed
fix(pcapng)!: default the IDB snaplen to 0 and reject values the field cannot hold (#2278)
_make_block_idb defaulted snaplen to 2**64 - 1, which the 32-bit SnapLen field truncates to 0xFFFFFFFF, so the made info disagreed with its octets; any explicit value outside 32 bits was truncated the same way. Default to 0 (no limit, per the PCAP-NG spec and the schema's own default) and raise ProtocolError for a snaplen outside 0..0xFFFFFFFF. Breaking: a make() without snaplen writes 0. Changelog entry added, CHANGELOG.md regenerated. Test: the default writes and reports 0, made info equals the parsed info for in-range values, and out-of-range values raise.
1 parent 7b57bb0 commit b14b04e

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r
350350
- 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)).
351351
- 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)).
352352
- 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)).
353+
- **a breaking change to** PCAP-NG's Interface Description Block maker: `snaplen` defaults to 0, which the PCAP-NG specification reads as no limit, where its default of `2**64 - 1` was truncated to `0xFFFFFFFF` by the 32-bit SnapLen field and the made `info` disagreed with the octets. A block made without `snaplen` writes 0 there, and a `snaplen` outside `0` to `0xFFFFFFFF` raises `ProtocolError` where it was truncated silently ([#2278](https://github.com/JarryShaw/PyPCAPKit/issues/2278)).
353354

354355
### pcapkit.toolkit
355356

‎docs/source/changelog/1.5.0.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,12 @@ Fixed
27552755
* HIP's ``ECHO_RESPONSE_UNSIGNED`` maker builds an ``EchoResponseUnsignedParameter``
27562756
schema, where it built the request's; the octets were already right (:issue:`2170`,
27572757
:pr:`2188`).
2758+
* **a breaking change to** PCAP-NG's Interface Description Block maker: ``snaplen``
2759+
defaults to 0, which the PCAP-NG specification reads as no limit, where its default of
2760+
``2**64 - 1`` was truncated to ``0xFFFFFFFF`` by the 32-bit SnapLen field and the made
2761+
``info`` disagreed with the octets. A block made without ``snaplen`` writes 0 there,
2762+
and a ``snaplen`` outside ``0`` to ``0xFFFFFFFF`` raises :exc:`.ProtocolError` where it
2763+
was truncated silently (:issue:`2278`).
27582764

27592765
pcapkit.toolkit
27602766
---------------

‎pcapkit/protocols/misc/pcapng.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4478,7 +4478,7 @@ def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = No
44784478
linktype_default: 'Optional[int]' = None,
44794479
linktype_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
44804480
linktype_reversed: 'bool' = False,
4481-
snaplen: 'int' = 0xFFFF_FFFF_FFFF_FFFF,
4481+
snaplen: 'int' = 0,
44824482
options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType, dict[str, Any]] | bytes]]' = None,
44834483
**kwargs: 'Any') -> 'Schema_InterfaceDescriptionBlock':
44844484
"""Make PCAP-NG interface description block (IDB).
@@ -4489,13 +4489,18 @@ def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = No
44894489
linktype_default: Default value of link layer protocol type.
44904490
linktype_namespace: Namespace of link layer protocol type.
44914491
linktype_reversed: Reversed flag for link layer protocol type namespace.
4492-
snaplen: Snap length.
4492+
snaplen: Snap length, an unsigned 32-bit value; 0, the default,
4493+
means no limit.
44934494
options: Block options.
44944495
**kwargs: Arbitrary keyword arguments.
44954496
44964497
Returns:
44974498
Constructed block schema.
44984499
4500+
Raises:
4501+
ProtocolError: If ``snaplen`` is outside ``0`` to ``0xFFFFFFFF``,
4502+
which the 32-bit SnapLen field would truncate (:issue:`2278`).
4503+
44994504
"""
45004505
if block is not None and hasattr(block, 'block_raw'):
45014506
return self._make_block_short(block) # type: ignore[return-value]
@@ -4506,6 +4511,8 @@ def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = No
45064511
else:
45074512
linktype_val = self._make_index(linktype, linktype_default, namespace=linktype_namespace, # type: ignore[assignment]
45084513
reversed=linktype_reversed, pack=False)
4514+
if not 0 <= snaplen <= 0xFFFF_FFFF:
4515+
raise ProtocolError(f'PCAP-NG: [IDB] snaplen must be in range 0 to 0xFFFFFFFF: {snaplen!r}')
45094516

45104517
if options is not None:
45114518
options_value, total_length = self._make_pcapng_options(options, namespace='if')
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
"""GitHub issue #2278: an Interface Description Block's snaplen fits its 32-bit field.
3+
4+
Without the fix, ``_make_block_idb`` defaults ``snaplen`` to ``2**64 - 1``, which
5+
the 32-bit SnapLen field truncates to ``0xFFFFFFFF``, so the made info disagrees
6+
with the octets it parses back to, and an explicit ``snaplen`` of ``2**32`` or
7+
more, or below zero, is truncated the same way. The default is 0, which the
8+
PCAP-NG specification reads as no limit, and a value the field cannot hold
9+
raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`.
10+
11+
:mod:`pcapkit` is imported inside each test, after
12+
:func:`~tests._support.reimport_once_per_class`.
13+
14+
"""
15+
16+
import unittest
17+
from typing import TYPE_CHECKING
18+
19+
from tests.protocols.misc.test_pcapng_roundtrip2_unit import PCAPNGTestCase
20+
21+
if TYPE_CHECKING:
22+
from typing import Any
23+
24+
H = bytes.fromhex
25+
26+
27+
class TestPCAPNGIDBSnaplen(PCAPNGTestCase):
28+
"""Pin the snaplen an Interface Description Block is made with."""
29+
30+
def _make(self, **block: 'Any') -> 'Any':
31+
"""Make an Ethernet Interface Description Block from ``block``."""
32+
from pcapkit.const.pcapng.block_type import BlockType
33+
from pcapkit.const.reg.linktype import LinkType
34+
from pcapkit.protocols.misc.pcapng import PCAPNG
35+
36+
return PCAPNG(num=1, sct=1, ctx=self._context(), type=BlockType.Interface_Description_Block,
37+
block={'linktype': LinkType.ETHERNET, **block})
38+
39+
def test_default_is_no_limit(self) -> None:
40+
"""The issue's case: the default writes 0, and the info says 0."""
41+
made = self._make()
42+
self.assertEqual(made.data.hex(), H('01000000 14000000 0100 0000 00000000 14000000').hex())
43+
self.assertEqual(made.info.snaplen, 0) # type: ignore[attr-defined]
44+
45+
def test_made_info_matches_the_octets(self) -> None:
46+
from pcapkit.protocols.misc.pcapng import PCAPNG
47+
48+
for snaplen in (None, 0, 1, 0x40000, 0xFFFF_FFFF):
49+
with self.subTest(snaplen=snaplen):
50+
made = self._make() if snaplen is None else self._make(snaplen=snaplen)
51+
parsed = PCAPNG(made.data, len(made.data), num=1, sct=1, ctx=self._context())
52+
self.assertEqual(made.info.snaplen, parsed.info.snaplen) # type: ignore[attr-defined]
53+
self.assertEqual(made.info, parsed.info)
54+
self.assertRebuilds(made.data)
55+
56+
def test_out_of_range_is_refused(self) -> None:
57+
from pcapkit.utilities.exceptions import ProtocolError
58+
59+
for snaplen in (-1, 0x1_0000_0000, 0xFFFF_FFFF_FFFF_FFFF):
60+
with self.subTest(snaplen=snaplen):
61+
with self.assertRaises(ProtocolError) as context:
62+
self._make(snaplen=snaplen)
63+
self.assertTrue(str(context.exception).endswith(f': {snaplen!r}'),
64+
str(context.exception))
65+
66+
67+
if __name__ == '__main__':
68+
unittest.main()

0 commit comments

Comments
 (0)