diff --git a/pcapkit/protocols/internet/hopopt.py b/pcapkit/protocols/internet/hopopt.py index 1ef8bae70a..fc3bdaef82 100644 --- a/pcapkit/protocols/internet/hopopt.py +++ b/pcapkit/protocols/internet/hopopt.py @@ -1611,8 +1611,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi Constructed option schema. Raises: - ProtocolError: If the DPD mode is invalid, or the Tagger ID does - not fit the TaggerID type. + ProtocolError: If the DPD mode is invalid, the Tagger ID does + not fit the TaggerID type, or the H-DPD Hash Assist Value is + empty. """ if opt is not None: @@ -1673,6 +1674,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi id=id, ) # type: Schema_SMFDPDOption elif dpd_type == Enum_SMFDPDMode.H_DPD: + # The first octet of the HAV carries the H-DPD mode bit. + if not hav: + raise ProtocolError(f'{self.alias}: [OptNo {code}] empty Hash Assist Value (HAV)') hav_ba = bytearray(hav) hav_ba[0] = hav[0] | 0x80 diff --git a/pcapkit/protocols/internet/ipv6_opts.py b/pcapkit/protocols/internet/ipv6_opts.py index 22c53e8961..a8e267bdbe 100644 --- a/pcapkit/protocols/internet/ipv6_opts.py +++ b/pcapkit/protocols/internet/ipv6_opts.py @@ -1600,8 +1600,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi Constructed option schema. Raises: - ProtocolError: If the DPD mode is invalid, or the Tagger ID does - not fit the TaggerID type. + ProtocolError: If the DPD mode is invalid, the Tagger ID does + not fit the TaggerID type, or the H-DPD Hash Assist Value is + empty. """ if opt is not None: @@ -1662,6 +1663,9 @@ def _make_opt_smf_dpd(self, code: 'Enum_Option', opt: 'Optional[Data_SMFIdentifi id=id, ) # type: Schema_SMFDPDOption elif dpd_type == Enum_SMFDPDMode.H_DPD: + # The first octet of the HAV carries the H-DPD mode bit. + if not hav: + raise ProtocolError(f'{self.alias}: [OptNo {code}] empty Hash Assist Value (HAV)') hav_ba = bytearray(hav) hav_ba[0] = hav[0] | 0x80 diff --git a/tests/protocols/internet/test_smf_dpd_empty_hav_unit.py b/tests/protocols/internet/test_smf_dpd_empty_hav_unit.py new file mode 100644 index 0000000000..5874bc78df --- /dev/null +++ b/tests/protocols/internet/test_smf_dpd_empty_hav_unit.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""HOPOPT and IPv6-Opts reject an H-DPD SMF_DPD option with an empty HAV. + +GitHub issue #1193: ``_make_opt_smf_dpd`` set the H-DPD mode bit with +``hav[0] | 0x80`` and no length check, so an empty Hash Assist Value raised a +bare :exc:`IndexError` rather than an in-library +:exc:`~pcapkit.utilities.exceptions.ProtocolError`. + +Every case builds its options in memory and reads no capture. The protocol +classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, as in +:mod:`tests.protocols.link.test_ethernet_mac_roundtrip_unit`. + +""" + +import unittest + +from tests._support import reimport_once_per_class + + +def _classes() -> 'dict[str, type]': + from pcapkit.protocols.internet.hopopt import HOPOPT + from pcapkit.protocols.internet.ipv6_opts import IPv6_Opts + return {'HOPOPT': HOPOPT, 'IPv6_Opts': IPv6_Opts} + + +class TestSMFDPDEmptyHAV(unittest.TestCase): + """Pin the error raised for an empty H-DPD Hash Assist Value.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_empty_hav_raises_protocol_error(self) -> None: + from pcapkit.const.ipv6.option import Option + from pcapkit.const.ipv6.smf_dpd_mode import SMFDPDMode + from pcapkit.const.reg.transtype import TransType + from pcapkit.utilities.exceptions import ProtocolError + + for name, cls in _classes().items(): + with self.subTest(proto=name): + with self.assertRaisesRegex(ProtocolError, r'\[OptNo 8\] empty Hash Assist Value'): + cls(options=[(Option.SMF_DPD, {'mode': SMFDPDMode.H_DPD, 'hav': b''})], + next=TransType.IPv6_NoNxt, payload=b'') + + def test_one_octet_hav_still_builds(self) -> None: + from pcapkit.const.ipv6.option import Option + from pcapkit.const.ipv6.smf_dpd_mode import SMFDPDMode + from pcapkit.const.reg.transtype import TransType + + for name, cls in _classes().items(): + with self.subTest(proto=name): + built = cls(options=[(Option.SMF_DPD, {'mode': SMFDPDMode.H_DPD, 'hav': b'\x01'})], + next=TransType.IPv6_NoNxt, payload=b'') + # SMF_DPD, length 1, HAV 0x01 with the H-DPD bit set, then PadN. + self.assertEqual(built.data, bytes.fromhex('3b00080181010100')) + + +if __name__ == '__main__': + unittest.main()