fix(pcapng): bound the skip after a journal binary field to one octet - #722
Conversation
|
❌ NEEDS CHANGES — cross-review on Opus of Sonnet-authored work at |
Cross-review detail (Opus, independent of the Sonnet author)Evidence obtained here, not taken from the PR body. The finding: the new check fires on valid captures
The message is also factually wrong there — the newline was in the capture; the splitter removed it before the check ran. Verified fix (not speculative — I ran it): re-append the newline the split took. segments = self.entry.split(b'\n\n')
for index, entry_buffer in enumerate(segments):
if index < len(segments) - 1:
entry_buffer += b'\n'Both false positives go to 0; the genuine warnings below are retained; all 6 journal tests / 23 subtests still pass unchanged. (Accepting Per-claim verdict1. Value read bounded, bug one line later — CONFIRMED. 2. The 3. 4. Neither modified test was weakened; both were strengthened. Both fail on 5. Coverage claim reproduced, and the author's argument is right. 543 stmts / 76 branches, 100%/100%. Deselecting only the new test leaves it at 543/76, 100%/100% — the new test adds zero coverage yet is the only thing that fails on main. Falsification against 6. Fixtures faithfully model the format. Against systemd.io
Adjacent, pre-existing, not this PR's job
Neither is introduced here, but both mean "every field behind a binary field survives" is still not true in general. Suggest a separate issue. Could not verify
|
Every systemd Journal Export field behind the first binary field was silently discarded. - Problem: `SystemdJournalExportBlock.post_process` skipped a binary field's trailing newline with a bare `entry_data.read()`, which reads to EOF rather than past one octet, so the entry-parsing loop's next `readline()` found nothing and ended the entry there. - Fix: bound the skip to `entry_data.read(1)`. A terminator that is missing or is not that newline ends the entry with a `SchemaWarning`, mirroring the short-length-prefix guard above it; skipped when the value was already clamped to the entry's own end. - Cross-review found a false positive: `self.entry.split(b'\n\n')` eats a real entry's last field's terminator along with the blank-line separator, so a binary last field followed by a trailing separator or a second entry warned over a newline that was actually present. Re-append `b'\n'` to every split segment but the last to restore it. - Two existing fixtures relied on the old unbounded read to end cleanly without a real terminator; updated their expectations. Added fixtures for the multi-binary-field discard, the trailing-separator false positive, and the two-entries false positive. `coverage run -m pytest tests/protocols/misc/test_pcapng_unit.py`: pcapng.py schema module 546 stmts / 78 branches, 100% both; 81 tests (1753 subtests) pass.
cb62a4f to
1f340cb
Compare
|
Reproduced on my own harness — the false positive is real. Prior GOOD-TO-GO-adjacent verdict is superseded. New head:
|
|
✅ GOOD TO MERGE — cross-review on Opus of Sonnet-authored work at |
|
✅ GOOD TO MERGE — cross-review on Opus of Sonnet-authored work at Supersedes the Evidence obtained here, not from the PR body. Boundary cases — no off-by-one found
8 shapes warned falsely at Per attack item
On the "+1 byte-count artifact" — benign, and in fact a correction. Non-blocking
✅ GOOD TO MERGE — cross-review on Opus of Sonnet-authored work at |
- SystemdJournalExportBlock.post_process split self.entry on b'\n\n' before reading a field, delimiting a length-prefixed format by content. A binary value containing b'\n\n' was cut mid-value into a bogus field in a spurious entry (#723 defect A); a 2,570-octet value is worse, since struct.pack('<Q', 2570) starts with b'\n\n', landing the split inside the length prefix and reading zero fields (defect B). - Rewrote the loop to walk self.entry once, length-prefix driven; folds in #722's terminator check so both fixes coexist, and retires its newline-restoration, which only undid what slicing had taken away. - A trailing separator landing on the block's last octet was indistinguishable from plain EOF, so it was swallowed instead of ending the entry, and a rebuild wrote a length one octet short. Now tracks whether the closing line was an actual blank line and keeps walking when it was. - Tests for both #723 defects, #722's fixtures, and the alignment gap. Tests: 84 passed, 1753 subtests; pcapng.py coverage stays 100% statements/branches (538/72 -> 554/78 lines/branches).
- SystemdJournalExportBlock.post_process split self.entry on b'\n\n' before reading a field, delimiting a length-prefixed format by content. A binary value containing b'\n\n' was cut mid-value into a bogus field in a spurious entry (#723 defect A); a 2,570-octet value is worse, since struct.pack('<Q', 2570) starts with b'\n\n', landing the split inside the length prefix and reading zero fields (defect B). - Rewrote the loop to walk self.entry once, length-prefix driven; folds in #722's terminator check so both fixes coexist, and retires its newline-restoration, which only undid what slicing had taken away. - A trailing separator landing on the block's last octet was indistinguishable from plain EOF, so it was swallowed instead of ending the entry, and a rebuild wrote a length one octet short. Now tracks whether the closing line was an actual blank line and keeps walking when it was. - Tests for both #723 defects, #722's fixtures, and the alignment gap. Tests: 84 passed, 1753 subtests; pcapng.py coverage stays 100% statements/branches (538/72 -> 554/78 lines/branches).
- SystemdJournalExportBlock.post_process split self.entry on b'\n\n' before reading a field, delimiting a length-prefixed format by content. A binary value containing b'\n\n' was cut mid-value into a bogus field in a spurious entry (#723 defect A); a 2,570-octet value is worse, since struct.pack('<Q', 2570) starts with b'\n\n', landing the split inside the length prefix and reading zero fields (defect B). - Rewrote the loop to walk self.entry once, length-prefix driven; folds in #722's terminator check so both fixes coexist, and retires its newline-restoration, which only undid what slicing had taken away. - A trailing separator landing on the block's last octet was indistinguishable from plain EOF and swallowed instead of ending the entry, writing a rebuilt length one octet short. Now tracked so a real blank line still starts the next entry. - Cross-review found the added `malformed` flag broke the *outer* per-entry loop, widening #722's per-entry terminator check into a per-block one and silently dropping well-formed entries behind a bad one. Removed it; the existing `break` already scopes the check to its own entry. Its `:1832` twin was already dead (EOF-only, where the walk ends regardless) -- confirmed and dropped too. - Tests for both #723 defects, #722's fixtures, the alignment gap, and a well-formed entry surviving a bad terminator ahead of it. Tests: 86 passed, 1753 subtests; pcapng.py coverage stays 100% statements/branches (551/78).
Fixes #704.
Mechanism, verified against source: not quite as the issue titled it -- the value read (
entry_data.read(length)) is already correctly bounded (fixed for #678/#699). The actual defect is the very next line, meant to skip the binary field's one trailing newline:entry_data.read()with no argument reads to EOF, not past that one octet. The entry loop's nextreadline()then returnsb''and ends the entry -- so every field behind the first binary field is discarded, silently, however well-formed.Fix:
pcapkit/protocols/schema/misc/pcapng.py,SystemdJournalExportBlock.post_process-- bound the skip toentry_data.read(1). If that octet is missing or isn't\n, end the entry with aSchemaWarning(mirrors the short-length-prefix guard just above) rather than resynchronising at the wrong offset. Skipped when the value was already clamped to the entry's own end, since that shortfall is reported there already and nothing real is left to check.Falsification: added
test_journal_fields_following_a_binary_field_are_not_discarded, a two-binary-field entry with a text field after the last one -- the shape that discriminates a bounded skip from an unbounded one (a single binary field has nothing behind it to lose). Against the unfixed code it fails:Passes after the fix;
BEFORE,FIRST,SECOND,AFTERall survive.Two pre-existing fixtures relied on the old unbounded read to end cleanly without a real terminator behind the declared value; updated their expectations for the newline-bounded behaviour.
Coverage (
coverage run -m pytest tests/protocols/misc/test_pcapng_unit.py, scoped to the schema module):pcapkit/protocols/schema/misc/pcapng.py-- 546 stmts / 78 branches, 100%/100% both before and after this test was added (the corrected pre-existing fixtures already reach the new lines; the new test is what actually proves the discard is gone, which line coverage alone can't show). Full file: 80 tests, 1753 subtests, all pass.Verified
pcapkit.__file__resolves inside the worktree under test before running.Not verified: no real-world systemd-journal-export
.pcapngcapture with a mid-entry binary field was available to test against; coverage is from synthetic fixtures.Second round — newline restoration
The first cross-review found the new terminator check warned falsely on spec-legal input:
self.entry.split(b'\n\n')atpcapkit/protocols/schema/misc/pcapng.py:1778eats the last field's terminating newline along with the separator, so a binary last field maderead(1)returnb''.Fixed by re-appending
b'\n'to every split segment but the last. Two fixtures added:test_a_binary_last_field_before_a_trailing_separator_is_not_warnedtest_a_binary_field_ending_the_first_of_two_entries_is_not_warnedBoth fail at
cb62a4f04and pass here. They guard the restoration, not #704 — the original multi-field fixture is what fails onmain.tests/protocols/misc/test_pcapng_unit.py: 80 passed, 1 skipped, 1753 subtests. Coverage546 stmts / 78 branches, 100%/100%.