Found while fixing #678 (PR #699), which fixes the exception leak two lines above this one and leaves
this alone deliberately: it is silent data loss on valid input rather than a length reaching a read, so
it changes parse output for well-formed captures and wants its own review.
Every journal field after the first binary field is discarded
pcapkit/protocols/schema/misc/pcapng.py, in SystemdJournalExportBlock.post_process:
length = struct.unpack('<Q', prefix)[0] # type: int
entry.add(line.decode('utf-8'), entry_data.read(length))
entry_data.read() # Skip trailing newline.
The comment says "skip trailing newline"; io.BytesIO.read() with no argument reads to EOF. So the
while True loop's next readline() returns b'', breaks, and everything after the binary field is
gone. Measured on 08f5b8df7 (PR #699's head; identical on f0999858e), against
pcapkit.__file__ = .../pcapkit/__init__.py printed on the run:
| entry data |
parsed |
b'BINARY\n' + <8-octet length 3> + b'abc\nAFTER=one\nMORE=two\n' |
[('BINARY', b'abc')] — AFTER and MORE lost |
b'BEFORE=zero\nBINARY\n' + <8-octet length 3> + b'abc\n' |
[('BEFORE', 'zero'), ('BINARY', b'abc')] — correct |
b'BEFORE=zero\nAFTER=one\nMORE=two\n' |
all three — correct |
No warning, no error. A field before the binary one survives; every field after it does not.
Why it matters
The systemd Journal Export Format puts no ordering constraint on binary fields — a binary field is a
name line, a 64-bit little-endian length, that many octets, and a newline, and it may appear anywhere in
an entry. MESSAGE is routinely binary (it is whenever the message contains a newline or a non-UTF-8
octet), and it is conventionally written early, so the common case is the broken one: every field
after MESSAGE disappears.
The fix, and the one thing it has to decide
entry_data.read() should consume exactly the one octet the format puts there:
entry_data.read(1) # the newline that terminates a binary field
What wants deciding is what to do when that octet is not a newline, or is absent because the entry
was cut short. The surrounding code now treats "the entry ran out" as end-of-entry with a
SchemaWarning (PR #699, for the 64-bit length prefix immediately above), and the same shape is
probably right here: a binary field whose terminator is missing or wrong is a malformed entry, and
reporting it beats both raising and silently resynchronising at the wrong offset.
Note this is not the same defect as the struct.error PR #699 fixes, and fixing that one does not
help here: it guards the length prefix being short, whereas this is the read after a
successfully-parsed value.
Scope
Not fixed in #699 for the same reason it is filed separately: recovering the discarded fields changes
the parse output of every journal export block that has a binary field followed by anything, so it is a
behaviour change on valid captures and needs a breaking argument of its own, plus a fixture that
actually exercises a multi-field journal entry — examples/captures/ has none, so #699's coverage of
this block comes entirely from hand-built buffers.
Related
Found while fixing #678 (PR #699), which fixes the exception leak two lines above this one and leaves
this alone deliberately: it is silent data loss on valid input rather than a length reaching a read, so
it changes parse output for well-formed captures and wants its own review.
Every journal field after the first binary field is discarded
pcapkit/protocols/schema/misc/pcapng.py, inSystemdJournalExportBlock.post_process:The comment says "skip trailing newline";
io.BytesIO.read()with no argument reads to EOF. So thewhile Trueloop's nextreadline()returnsb'', breaks, and everything after the binary field isgone. Measured on
08f5b8df7(PR #699's head; identical onf0999858e), againstpcapkit.__file__ = .../pcapkit/__init__.pyprinted on the run:b'BINARY\n' + <8-octet length 3> + b'abc\nAFTER=one\nMORE=two\n'[('BINARY', b'abc')]—AFTERandMORElostb'BEFORE=zero\nBINARY\n' + <8-octet length 3> + b'abc\n'[('BEFORE', 'zero'), ('BINARY', b'abc')]— correctb'BEFORE=zero\nAFTER=one\nMORE=two\n'No warning, no error. A field before the binary one survives; every field after it does not.
Why it matters
The systemd Journal Export Format puts no ordering constraint on binary fields — a binary field is a
name line, a 64-bit little-endian length, that many octets, and a newline, and it may appear anywhere in
an entry.
MESSAGEis routinely binary (it is whenever the message contains a newline or a non-UTF-8octet), and it is conventionally written early, so the common case is the broken one: every field
after
MESSAGEdisappears.The fix, and the one thing it has to decide
entry_data.read()should consume exactly the one octet the format puts there:What wants deciding is what to do when that octet is not a newline, or is absent because the entry
was cut short. The surrounding code now treats "the entry ran out" as end-of-entry with a
SchemaWarning(PR #699, for the 64-bit length prefix immediately above), and the same shape isprobably right here: a binary field whose terminator is missing or wrong is a malformed entry, and
reporting it beats both raising and silently resynchronising at the wrong offset.
Note this is not the same defect as the
struct.errorPR #699 fixes, and fixing that one does nothelp here: it guards the length prefix being short, whereas this is the read after a
successfully-parsed value.
Scope
Not fixed in #699 for the same reason it is filed separately: recovering the discarded fields changes
the parse output of every journal export block that has a binary field followed by anything, so it is a
behaviour change on valid captures and needs a
breakingargument of its own, plus a fixture thatactually exercises a multi-field journal entry —
examples/captures/has none, so #699's coverage ofthis block comes entirely from hand-built buffers.
Related
struct.errortwo lines above, from a short 64-bit length prefix, and the32-bit NUL padding being read as a field name. Both fixed there; this one is not.
packetis computed then overwritten with b'', so dumping through PCAPIO writes a record header claiming 314 octets followed by none #646 / fix(pcapng): keep the captured octets every packet block declares (#646) #683 — also PCAP-NG and also about octets going missing, but at the packet-block payloadrather than inside a journal entry.