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
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
ValueErrorNot "loses a frame" — raises out of
Extractor, so the whole extraction is lost. Measured on0c7f2b7c9against the committedexamples/captures/dhcp.pcapng, truncated at every 4-octetboundary from -4 to -400. All 101 levels fail, identically:
ValueErroris not one ofpcapkit.utilities.exceptions, so a caller cannot distinguish it from abug in its own code, and it defeats the
#431/#571standing constraint that a legitimatelytruncated capture must still parse — for PCAP-NG that constraint is currently vacuous at the file
level.
Where it comes from
The negative length originates at
pcapkit/protocols/schema/misc/pcapng.py:228:packet['__length__']is seeded by@preparefrom what is left in the file(
pcapkit/utilities/decorators.py:274) and then decremented by four forPCAPNG.type. A previousblock 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 anddata.read(negative)raises.@prepareonly turns a remainder of exactly zero into the quietStreamEOFErrorthat the frame loop is built to catch (decorators.py:264-270); one, two or threeoctets fall through.
The fix that is probably right
max(packet['__length__'], 0), which is the idiompcapkit/protocols/schema/transport/sctp.pyalready uses for the same hazard, and which
Schema.unpackitself already anticipates —schema.py:898-900warns rather than raises when__length__has gone negative, so the negativevalue 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
StreamEOFErrortheframe loop already handles (giving the frames before the cut, like the legacy PCAP reader) or as a
ProtocolErrornaming the truncation. The first matches the#431bar; the second is louder. Eitherway 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
breakingargument.Related
packetis computed then overwritten with b'', so dumping through PCAPIO writes a record header claiming 314 octets followed by none #646 — also PCAP-NG and also about a length being the wrong length, but that one isProtocolBase.packettreating Block Total Length as a pre-payload header length. Different site,different symptom.