Conversation
715b968 to
dbad35c
Compare
|
Two things measured after the PR description was written, both worth having on the record here. A bounded fuzz, run either side of the change4,000 rounds of word-aligned mutation over the committed
Both of #678's families are gone under fuzz as well as under the systematic sweep. The two foreign
Amended since the description
|
dbad35c to
08f5b8d
Compare
…ts sweep claim - The journal export block's bare struct.error, which #699 now fixes too: the block's own 32-bit NUL padding was read as a binary field's name, so every entry of unaligned length raised. Reachable from valid input. - The "no foreign exception" claim scoped to the truncation sweep, with the two families a fuzz still reaches named and measured unchanged either side: #701 (an unassigned block type raising from aenum) and #593's 32-bit band. - #704, the silent loss of every journal field after a binary one, noted as filed rather than fixed. - Where the two remaining ProtocolError levels actually cut: the Interface Description Block's if_tsresol option, not the Section Header Block. `python util/changelog_md.py --check` exits 0.
…pture An EOF-truncated PCAP-NG file raised a bare `ValueError` out of `Extractor`, losing the whole extraction rather than the one truncated block. - `PCAPNG.read` clamps the post-block seek to the octets the file actually held, warning when Block Total Length overran it. The declared length is cross-checked only against its own trailing copy, so it used to seek past the end -- legal and silent -- and every later block then measured a negative remainder. - `PCAPNG._check_block_floor` reports a tail under twelve octets as the quiet `StreamEOFError` the frame loop already catches, instead of padding a block out of nothing. - `nonnegative()` floors every computed length in the schema at zero, and `bounded_option`/`bounded_area` compose it. A negative reached a `struct` template as `'-8s'` or `read()` as a deficit; neither exception was one of `pcapkit.utilities.exceptions`. - `SystemdJournalExportBlock.post_process` no longer leaks three bare exceptions of the same family: a `struct.error` from unpacking a 64-bit length out of a short buffer, which its own NUL padding reached on any entry of unaligned length; an `OverflowError` from a length at or above `2**63` reaching `BytesIO.read`; and a `UnicodeDecodeError` from a field name, key or value that is not UTF-8. - `Option.register` reports a displaced option schema as a `RegistryWarning`, the last of five unguarded registrars. Measured over all 1,509 octet boundaries of `examples/captures/dhcp.pcapng`: 6 parsed before, 1,495 after, and no level raises from outside the library. Coverage on the two files is unchanged at 99.93%. Fixes #678
08f5b8d to
428a231
Compare
…fixed in #699 The cross-review's second pass found an OverflowError from a binary field's 64-bit length reaching BytesIO.read at 2**63 and above, and a UnicodeDecodeError from a field name, key or value that is not UTF-8. Both pre-existing, both in the function #699 had just fixed the struct.error in, both now clamped or replaced and reported. `python util/changelog_md.py --check` exits 0.
Cross-review verdict: GOOD TO GOModel: Sonnet, against this change authored on Opus — a different model by construction, as the It took three rounds, and the first two were NEEDS CHANGES. Both were right, and what it Round one — NEEDS CHANGES.
Round two — NEEDS CHANGES. It confirmed the above closed, reproducing both the defect and the repair Round three — GOOD TO GO. It re-ran its own repros unmodified, added the What it disputed that I did not change, both recorded rather than folded away:
What it could not verify: that 11 of the new tests fail on Two design judgements it was asked to second-guess and agreed with, with its own reasoning rather than AutoSDE has no equivalent here; CI is still queued at the time of writing and no claim is made about |
Fixes #678.
The defect
Every EOF-truncated PCAP-NG file raised a bare
ValueErrorout ofExtractor— the wholeextraction, not one frame. Measured over all 1,509 octet boundaries of the committed
examples/captures/dhcp.pcapngonf0999858e, with one harness before and after:f0999858eValueError: read length must be non-negative or -1struct.error: bad char in struct formatProtocolErrorStreamEOFErrorFormatErrorNo level of that sweep raises from outside
pcapkit.utilities.exceptionsany more, and the framecount degrades monotonically with the cut: 4 frames at 365 levels, then 3, 2, 1, and 0 at the 58
deepest.
None of the 14 levels that still raise costs a frame that was in the file. Twelve are cuts leaving a
file too short to hold a block at all — under twelve octets (
StreamEOFError), under four not evenidentifiable as PCAP-NG (
FormatError). The other two, at cuts 1462 and 1463, land inside theInterface Description Block's
if_tsresoloption (ProtocolError), which is before the firstpacket block either way.
That claim is about this sweep, not about every possible input — see What this does not fix
below, which names the two foreign-exception families a fuzz still reaches and why neither is in
scope.
Where it came from, and the four changes
#678's line numbers are stale after #676 and #683; these are the re-located sites.1.
PCAPNG.read,pcapkit/protocols/misc/pcapng.py:1060-1097— the root. Block Total Length iscross-checked against its own trailing copy and never against the file, so
seek_cur = _seek_set + block.lengthseeked past the real end on a truncated last block — legal and silent. The nextblock read then measured a negative remainder, since
preparederives it as the end of thestream less the current position. That is why nearly every level failed rather than only the one
holding the cut.
_read_filenghad already stopped at the end of the file, so the octets it returnedare the authority on where the block really finishes; the seek is clamped to them and a
ProtocolWarningnames the overrun.2.
PCAPNG._check_block_floor, new,pcapkit/protocols/misc/pcapng.py:1241. A block is twelveoctets at its smallest, which is what
__length_hint__already reports. A shorter tail is not ablock, so parsing one out of it can only invent fields from
FieldBase.unpack's zero padding — andonce
PCAPNG.type's four octets are padded out of nothing,__length__is negative. Reported as thequiet
StreamEOFErrorthatExtractor.record_framesalready catches.3.
nonnegative(), new,pcapkit/protocols/schema/misc/pcapng.py:210. Every span in the moduleis a subtraction of wire fields, and
_TextField.__call__builds its template asf'{length}s'unconditionally — so
-8becomes the format'-8s'andstruct.calcsizeraises. Applied to theeight unwrapped arithmetic sites (
length - 12/- 16/- 20/- 24/- 28, and the DSB's- 20 - secrets_length - padding), the eight__option_padding__-sized padding fields, and composedinto
bounded_optionandbounded_area. Measured: 28 of the module's 74 length callbacksreturned a negative on the parent commit, from
-1on anepb_hashdeclaring no payload to-16777248on an EPB option area; 0 do now.4.
SystemdJournalExportBlock.post_process,pcapkit/protocols/schema/misc/pcapng.py:1711—three more of the same family, all found by the cross-review.
struct.unpack('<Q', entry_data.read(8))refuses a short buffer with a barestruct.error, and itwas reachable from valid input, not only truncated input: the block body is padded to a 32-bit
boundary with NULs,
bytes.strip()takes only ASCII whitespace, so the padding survived it and wasread as the name of a binary field whose 64-bit length prefix then had nothing behind it.
Measured:
MESSAGE=hello\n— 14 octets, so two NULs of padding — raised. A NUL-only line now endsthe entry, and a genuinely short prefix ends it with a
SchemaWarning.entry: at
2**63and aboveBytesIO.readrefuses it with a bareOverflowError(cannot fit 'int' into an index-sized integer), and below that it silently returned whatever was there — the samemalformed prefix fatal or invisible by magnitude alone. Clamped to what the entry has left, and
reported.
UnicodeDecodeError— aValueError, soforeign on both counts, and fatal to the whole extraction over one octet in one field. Decoded with
errors='replace'and reported, which is the option this module's ownStringFieldalready takes.Worth stating plainly: the first of these is not a truncation defect at all. It made
SystemdJournalExportBlockraise on any journal entry whose length is not a multiple of four, whichis most of them.
5.
Option.register,pcapkit/protocols/schema/misc/pcapng.py:694— the registrar guard. Itwrote into its namespace dictionaries with no check at all, one of five in the package. Now warns
RegistryWarning, #681's pattern.Relation to #676's clamp-and-warn
Deliberately consistent with it, and the one departure is argued.
nonnegativeclamps and warns forexactly
bounded_area's reason: there is no catch point aboveFieldBase.unpack, so a refusalinside a block read aborts the whole extraction instead of one block, which is what the #431
accommodation exists to prevent.
The end of the file is the case that is not a clamp, because there no block is being read at all.
StreamEOFErroris anEOFError, which the frame loop catches by design, so raising it costs noframe that was actually in the file — the argument against refusing does not apply to it. Clamping
there instead would have fabricated a whole block out of zero padding, which is worse than reporting
the truncation.
#676's note that the five non-packet option areas keep the framing assumption is updated rather than
closed: they now go through
nonnegative, which is the part of #678 that stops a declared lengthreaching a read, but the per-block equality against
__length__is still open. #593's 32-bit band forblock-level payloads is untouched.
The
struct.errormanifestationFixed, both ways in. The issue's comment reports a huge
captured_lendrivingbounded_area'sspan negative; measured, that was because
nominal <= availableis true for a negative nominal, soit returned it unclamped. The 200-block / 8,048-octet vector now parses to 200 frames. The truncation
sweep reached the same
struct.errorby a second route —__option_padding__at-32onEnhancedPacketBlock.padding_opts, at cuts 372 and 373 — which the same floor covers.Behaviour change, and why
breakingLabelled
breakingalongsidefixandtest. Well-formed captures are unaffected — verified byregenerating
examples/captures/pcapng.txtand diffing: byte-identical to the same fileregenerated on
f0999858e. But for a whole class of inputs the output changes:that read "extraction raised" as "this file is unusable" now gets a partial result, and the last
frame may carry zero-padded octets;
ProtocolError: unknown byteorder magicnow raiseStreamEOFError, andPCAPNG(b'...')withunder twelve octets raises
StreamEOFErrorrather thanValueError.That the old behaviour was a defect makes the change justified, not invisible, which is the same call
#683 made for a change of the same shape.
Tests
26 new tests in
tests/protocols/misc/test_pcapng_unit.py, appended; all 26 pass here, over 1,552subtests. 11 of them fail on
f0999858ewith the sources reverted and the tests kept (1,519subtest failures). The cross-review checked the other side of that and named eight it judged not
load-bearing — the monotonicity guard, the twelve-octet boundary's accept side, the unseekable-stream
and construction-path pins, the
__length__-whole exemption, and three of the five registry-guardtests — which is a fair reading: they are guards against the fix's own footguns rather than
regression tests for the defect, and they are labelled as such in their docstrings.
The sweep tests walk every octet boundary rather than picking one — the levels that behave
differently are not ones anybody would have chosen: 372 and 373 held the
struct.error, and 376 iswhere the cut lands on a block boundary.
test_every_length_callback_in_the_module_is_floored_at_zerowalks every schema in the module and drives all 74 length callbacks with a hostile packet, so a newly
added unfloored subtraction is caught without anyone updating a list. The crafted
captured_lenvector runs in a subprocess under
RLIMIT_AS, since #594 is about amplification and anin-process regression would take the test host rather than fail.
EXPECTED_FAILURESwas imported rather than grepped: 44 entries, 35 PCAP-NG, none moved. Oneintermediate attempt did move two of them — flooring the three decryption-secrets payloads that read
__length__whole packs nothing, which emptied both payloads and turnedpcapng-secrets/TLS_Key_Logand.../WireGuard_Key_LogfromMISMATCHtoOKbecause an emptypayload compares equal to an empty payload. A regression that reads as a fix; reverted, and pinned by
test_a_field_sized_by_the_remaining_length_whole_is_left_alone.What this does not fix
A 4,000-round bounded mutation fuzz over
dhcp.pcapng, run with the same seed and harness either sideof the change under a 2 GiB
RLIMIT_AS, takes the parse rate from 2,118 to 3,438 and removes both of#678's families entirely (
ValueError: read length must be non-negative or -1, andstruct.error: bad char in struct formatat 131 → 0). Two foreign-exception families remain, both pre-existing andmeasured unchanged:
ValueError: N is not a valid BlockType. An unassigned Block Type raises fromaenuminsideEnumField.post_process, so theUnknownBlockdefault the schema registry declares is unreachableand the format's skip-unknown-blocks requirement is not met. Reachable from a single byte flip in the
committed capture. Filed as An unassigned PCAP-NG Block Type raises a bare ValueError from aenum, so UnknownBlock is unreachable and one unknown block costs the whole extraction #701; the site is
pcapkit/corekit/fields/numbers.py, shared byevery protocol, so fixing it here would move every enum-typed wire field in the package at once.
MemoryError. Exactly 30 of 4,000 in both trees under the same cap — fix(corekit): bound the total zero padding a parse may synthesise (#573) #593's 32-bit band forblock-level payload fields, which fix(pcapng): bound an option's payload to the area its block declares (#594) #676's note already records as a known gap.
Also out of scope, and noted rather than fixed:
SystemdJournalExportBlock.post_process'sentry_data.read()after a binary field reads to the end of the entry rather than past one newline, soany field after the first binary one is discarded. Filed as #704.
Coverage
Measured and reported inside each tree, over the same three test files:
f0999858epcapkit/protocols/misc/pcapng.pypcapkit/protocols/schema/misc/pcapng.py25 new statements, all covered; the single remaining miss is the pre-existing
return datetime.timezone.utc. An earlier draft did drop the total to 99.89% — an explicit_readguard in
unpackwhose false branch can never be taken, since the construction path sets__header__frommakebefore it gets there. Removed rather than pragma'd.Also
examples/captures/pcapng.txtdoes not move further under this change (#685 unaffected) — the116-line diff against the committed fixture is entirely the pre-existing #683 drift, byte-identical
before and after this branch.
Cross-review
Reviewed by a subagent on Sonnet (this change was authored on Opus), briefed to falsify rather than
to bless, running read-only. It came back NEEDS CHANGES twice, and both rounds were right:
struct.error— item 4 of the list above, which I hadnot looked at — and that the description's claim "nothing raises from outside
pcapkit.utilities.exceptions" was unscoped and therefore false. Both addressed: the exception isfixed, and the claim is now scoped to the sweep it was measured on with the two remaining families
named above. It also caught two miscounts (20 vs 21 tests, 13 vs 14 residual levels) and one wrong
attribution — I had written that all 14 residual levels cut into the Section Header Block, where two
of them cut into the Interface Description Block.
against a separately reconstructed pre-fix tree, and then found the
OverflowErrorandUnicodeDecodeErrorabove in the same function — pre-existing, but, as it put it, "silently leavingthem out after specifically fixing this function's other foreign-exception path is the same
unscoped-claim shape as the original NEEDS CHANGES". Fair, and fixed rather than deferred.
Two of its observations I accepted without changing the code, and both are recorded above rather than
folded away: that eight of the new tests are guards against the fix's own footguns rather than
regression tests for the defect, and that the
MemoryErrorfamily is scoped out on the strength of itsbeing measured identical either side rather than on argument.
It could not verify the "11 of the new tests fail on the parent commit" claim without reverting the
tree, which it correctly declined to do; it reasoned about each test instead.
CI has not run yet; no claim is made about it.