Skip to content

Every EOF-truncated PCAP-NG file raises an uncaught ValueError: pcapng_block_selector passes a negative __length__ to SchemaField #678

Description

@JarryShaw

Found while measuring #594's amplification band, and deliberately left out of #676 because it is a
block-level defect rather than an option-level one, and fixing it changes frame counts on truncated
captures.

Every EOF-truncated PCAP-NG file raises an uncaught ValueError

Not "loses a frame" — raises out of Extractor, so the whole extraction is lost. Measured on
0c7f2b7c9 against the committed examples/captures/dhcp.pcapng, truncated at every 4-octet
boundary from -4 to -400. All 101 levels fail, identically:

dhcp.pcapng/whole  input=1508  frames=4   ok
dhcp.pcapng/-4     input=1504  ValueError: read length must be non-negative or -1
dhcp.pcapng/-8     input=1500  ValueError: read length must be non-negative or -1
...
dhcp.pcapng/-400   input=1108  ValueError: read length must be non-negative or -1

ValueError is not one of pcapkit.utilities.exceptions, so a caller cannot distinguish it from a
bug in its own code, and it defeats the #431/#571 standing constraint that a legitimately
truncated capture must still parse — for PCAP-NG that constraint is currently vacuous at the file
level
.

Where it comes from

  File "pcapkit/foundation/extraction.py", line 700, in record_frames
    self._exeng.read_frame()
  File "pcapkit/foundation/engines/pcapng.py", line 189, in read_frame
    block = P_PCAPNG(ext._ifile, num=ext._frnum+1, ...)
  File "pcapkit/protocols/protocol.py", line 644, in __init__
    self.__post_init__(file, length, **kwargs)
  File "pcapkit/protocols/misc/pcapng.py", line 1088, in __post_init__
    self._info = self.unpack(length, _read=_read, _seek_set=_seek_set, **kwargs)
  File "pcapkit/protocols/misc/pcapng.py", line 900, in unpack
    self.__header__ = cast('Schema_PCAPNG', self.__schema__.unpack(self._file, length, packet))
  File "pcapkit/utilities/decorators.py", line 280, in unpack
    schema = func(cls, data, length, packet)
  File "pcapkit/protocols/schema/schema.py", line 857, in unpack
    byte = data.read(length)
ValueError: read length must be non-negative or -1

The negative length originates at pcapkit/protocols/schema/misc/pcapng.py:228:

def pcapng_block_selector(packet: 'dict[str, Any]') -> 'Field':
    ...
    return SchemaField(length=packet['__length__'], schema=schema)

packet['__length__'] is seeded by @prepare from what is left in the file
(pcapkit/utilities/decorators.py:274) and then decremented by four for PCAPNG.type. A previous
block whose declared Block Total Length ran past the real end of the file leaves the reader
positioned such that fewer than four octets remain, so __length__ goes negative and
data.read(negative) raises. @prepare only turns a remainder of exactly zero into the quiet
StreamEOFError that the frame loop is built to catch (decorators.py:264-270); one, two or three
octets fall through.

The fix that is probably right

max(packet['__length__'], 0), which is the idiom pcapkit/protocols/schema/transport/sctp.py
already uses for the same hazard, and which Schema.unpack itself already anticipates —
schema.py:898-900 warns rather than raises when __length__ has gone negative, so the negative
value is expected to be survivable at that layer and only this one call site treats it as fatal.

Worth deciding alongside it: whether a short tail should surface as the quiet StreamEOFError the
frame loop already handles (giving the frames before the cut, like the legacy PCAP reader) or as a
ProtocolError naming the truncation. The first matches the #431 bar; the second is louder. Either
way it should be an in-library exception rather than a bare ValueError.

Why it is not in #676

#676 bounds an option's payload to the option area its block declares. This is the block's own
span against the file, one layer up. Measured byte-identical before and after #676 at all 101
truncation levels, including the failures — #676 neither causes nor cures it. Fixing it changes frame
counts on truncated captures, which wants its own review and its own breaking argument.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions