Summary
Parsing any Mobility Header whose option area contains a Pad1 option (option type 0) raises struct.error: bad char in struct format. The cause is PadOption.data's length lambda receiving the NoValue sentinel that Schema.unpack stores for a ConditionalField-absent length, which then becomes part of a struct format string. The same pattern affects HOPOPT.
Location
pcapkit/protocols/schema/internet/mh.py:230:
data: 'bytes' = PaddingField(length=lambda pkt: pkt.get('length', 0))
with length declared at pcapkit/protocols/schema/internet/mh.py:192-195:
length: 'int' = ConditionalField(
UInt8Field(default=0),
lambda pkt: pkt['type'] != Enum_Option.Pad1,
)
Observed symptom
File "/pcapkit/protocols/protocol.py", line 280, in unpack
self.__header__ = cast('_ST', self.__schema__.unpack(self._file, length, packet))
File "/pcapkit/protocols/schema/schema.py", line 641, in unpack
value = field.unpack(byte, packet.copy())
File "/pcapkit/corekit/fields/collections.py", line 270, in unpack
data = schema.unpack(file, length, packet)
File "/pcapkit/protocols/schema/schema.py", line 621, in unpack
byte = data.read(field.length)
File "/pcapkit/corekit/fields/field.py", line 101, in length
return struct.calcsize(self.template)
struct.error: bad char in struct format
An option area holding only a PadN parses fine, which isolates it to Pad1:
=== option area with only a PadN (no Pad1): b'\x01\x06\x00\x00\x00\x00\x00\x00'
parsed OK -> BindingRefreshRequestMessage(next=59, length=16, type=0, chksum=b'\x00\x00',
options=OrderedMultiDict([(<Option.PadN: 1>, <PadOption type=<Option.PadN: 1>, length=8>)]), ...)
pcapkit also cannot read back the Pad1 octet pcapkit itself emits — _make_opt_pad packs a Pad1 to b'\x00' without complaint, and parsing that same octet crashes:
=== packing a Pad1
schema returned: PadOption(type=0, length=0) -> packed b'\x00' -- packing does NOT crash
=== re-reading pcapkit's own output
option area built from pcapkit output: b'\x00\x01\x05\x00\x00\x00\x00\x00' (8 octets)
re-parse raised struct.error: bad char in struct format
Minimal reproduction
import io
from pcapkit.protocols.internet.mh import MH
# MH Binding Refresh Request whose 8-octet option area is
# Pad1 (0x00) followed by PadN with Option Length = 5.
raw = bytes([59, 1, 0, 0, 0, 0, 0, 0]) + bytes([0x00]) + bytes([0x01, 0x05]) + bytes(5)
MH(io.BytesIO(raw), len(raw))
PadOption.unpack(io.BytesIO(b'\x00'), 1, {}) reproduces it at the schema level with the same final two frames.
Root cause
pkt.get('length', 0) never reaches its 0 default. When a ConditionalField test fails, Schema.unpack at pcapkit/protocols/schema/schema.py:634 stores the sentinel rather than omitting the key:
if not field.test(packet):
self.__buffer__[field.name] = b''
setattr(self, field.name, None)
packet[field.name] = NoValue
continue
So .get finds 'length' present and returns NoValue. That value flows into Field.__call__ (pcapkit/corekit/fields/field.py:267-268, new_self._length = new_self._length_callback(packet)) and then into _TextField.__call__ (pcapkit/corekit/fields/strings.py:64, new_self._template = f'{new_self._length}s'), producing a template built from the sentinel's repr. Field.length (field.py:99-101) calls struct.calcsize on it and raises. Measured directly against the field:
packet={'length': <...NoValueType object at 0x...>}
-> _length=<...NoValueType object at 0x...>
template='<pcapkit.corekit.fields.field.NoValueType object at 0x...>s'
length raised error: bad char in struct format
packet={} -> _length=0 template='0s' length=0
packet={'length': 5} -> _length=5 template='5s' length=5
PadOption.post_process (schema/internet/mh.py:207-210), which sets length = 0 for a Pad1, cannot help: it runs after unpack returns (pcapkit/utilities/decorators.py:194), and the crash is inside unpack.
Note that the packing direction works, because Schema.pack seeds the packet dict from self.__dict__ (schema/schema.py:488) where length is a real int. Only the parse direction is affected.
Also affected
pcapkit/protocols/schema/internet/hopopt.py:273 carries the identical pattern under a different field name:
pad: 'bytes' = PaddingField(length=lambda pkt: pkt.get('len', 0))
and a HOPOPT extension header whose option area contains a Pad1 (b';\x00\x00\x01\x03\x00\x00\x00') raises the same struct.error through the same field.py:101 frame. pcapkit/protocols/schema/internet/ipv6_opts.py:273 has the same line but does not reach it, for an unrelated reason not filed here.
What a fix would need to touch
Narrow fix: the length lambdas — pcapkit/protocols/schema/internet/mh.py:230, pcapkit/protocols/schema/internet/hopopt.py:273, and pcapkit/protocols/schema/internet/ipv6_opts.py:273 — so the NoValue sentinel is coerced to 0 rather than passed through.
Broader fix, covering every pkt.get(...)-style length lambda over a conditional field at once: Schema.unpack at pcapkit/protocols/schema/schema.py:630-635 (do not write the sentinel into the packet dict), or Field.__call__ at pcapkit/corekit/fields/field.py:267-268 (reject or normalise a non-integer callback result).
No existing test covers this: the MH unit tests construct schema.PadOption(type=Pad1, length=0) directly rather than unpacking a Pad1 from bytes, so the whole MH suite passes today (10 passed).
A correction to how this was first reported
It was initially described as "a PadN with length 0 normalises to Pad1 and hits the same crash". That is not what happens: _make_opt_pad(PadN, length=0) normalises to Pad1 and packs fine, returning b'\x00'. The crash is only on the way back in — pcapkit cannot re-parse the octet it just emitted. Recording the correction here so the mechanism is not mis-stated.
Observed vs inferred
- Observed by running code on the current tree: the
struct.error traceback above, both via MH(...) and via PadOption.unpack(b'\x00'); that a PadN-only option area parses fine; that the length lambda receives NoValue while an empty packet dict would yield 0; that packing a Pad1 succeeds and re-parsing pcapkit's own output fails; the identical HOPOPT crash; that the MH unit suite is green.
- Inferred by reading code: that
post_process runs too late to help; that a fix at schema.py:630-635 or field.py:267-268 would cover all similar lambdas. Neither was verified by patching the source.
Summary
Parsing any Mobility Header whose option area contains a Pad1 option (option type 0) raises
struct.error: bad char in struct format. The cause isPadOption.data's length lambda receiving theNoValuesentinel thatSchema.unpackstores for aConditionalField-absentlength, which then becomes part of astructformat string. The same pattern affects HOPOPT.Location
pcapkit/protocols/schema/internet/mh.py:230:with
lengthdeclared atpcapkit/protocols/schema/internet/mh.py:192-195:Observed symptom
An option area holding only a PadN parses fine, which isolates it to Pad1:
pcapkit also cannot read back the Pad1 octet pcapkit itself emits —
_make_opt_padpacks a Pad1 tob'\x00'without complaint, and parsing that same octet crashes:Minimal reproduction
PadOption.unpack(io.BytesIO(b'\x00'), 1, {})reproduces it at the schema level with the same final two frames.Root cause
pkt.get('length', 0)never reaches its0default. When aConditionalFieldtest fails,Schema.unpackatpcapkit/protocols/schema/schema.py:634stores the sentinel rather than omitting the key:So
.getfinds'length'present and returnsNoValue. That value flows intoField.__call__(pcapkit/corekit/fields/field.py:267-268,new_self._length = new_self._length_callback(packet)) and then into_TextField.__call__(pcapkit/corekit/fields/strings.py:64,new_self._template = f'{new_self._length}s'), producing a template built from the sentinel'srepr.Field.length(field.py:99-101) callsstruct.calcsizeon it and raises. Measured directly against the field:PadOption.post_process(schema/internet/mh.py:207-210), which setslength = 0for a Pad1, cannot help: it runs afterunpackreturns (pcapkit/utilities/decorators.py:194), and the crash is insideunpack.Note that the packing direction works, because
Schema.packseeds the packet dict fromself.__dict__(schema/schema.py:488) wherelengthis a realint. Only the parse direction is affected.Also affected
pcapkit/protocols/schema/internet/hopopt.py:273carries the identical pattern under a different field name:and a HOPOPT extension header whose option area contains a Pad1 (
b';\x00\x00\x01\x03\x00\x00\x00') raises the samestruct.errorthrough the samefield.py:101frame.pcapkit/protocols/schema/internet/ipv6_opts.py:273has the same line but does not reach it, for an unrelated reason not filed here.What a fix would need to touch
Narrow fix: the length lambdas —
pcapkit/protocols/schema/internet/mh.py:230,pcapkit/protocols/schema/internet/hopopt.py:273, andpcapkit/protocols/schema/internet/ipv6_opts.py:273— so theNoValuesentinel is coerced to0rather than passed through.Broader fix, covering every
pkt.get(...)-style length lambda over a conditional field at once:Schema.unpackatpcapkit/protocols/schema/schema.py:630-635(do not write the sentinel into the packet dict), orField.__call__atpcapkit/corekit/fields/field.py:267-268(reject or normalise a non-integer callback result).No existing test covers this: the MH unit tests construct
schema.PadOption(type=Pad1, length=0)directly rather than unpacking a Pad1 from bytes, so the whole MH suite passes today (10 passed).A correction to how this was first reported
It was initially described as "a
PadNwith length 0 normalises toPad1and hits the same crash". That is not what happens:_make_opt_pad(PadN, length=0)normalises to Pad1 and packs fine, returningb'\x00'. The crash is only on the way back in — pcapkit cannot re-parse the octet it just emitted. Recording the correction here so the mechanism is not mis-stated.Observed vs inferred
struct.errortraceback above, both viaMH(...)and viaPadOption.unpack(b'\x00'); that a PadN-only option area parses fine; that the length lambda receivesNoValuewhile an empty packet dict would yield0; that packing a Pad1 succeeds and re-parsing pcapkit's own output fails; the identical HOPOPT crash; that the MH unit suite is green.post_processruns too late to help; that a fix atschema.py:630-635orfield.py:267-268would cover all similar lambdas. Neither was verified by patching the source.