You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
R1CounterParameter.counter is a UInt32Field where RFC 7401 §5.2.3 gives the R1 generation counter 8 bytes, so the parameter packs 12 octets where the RFC total is 16 — four short, at 4 (mod 8), for both code 128 and code 129.
Found while measuring #651 (fixed in #664), which is where every number below comes from. Not fixed there: it is a field-width change with a data-model consequence and wants its own review, exactly as the SOLUTION width (#608) did in #629.
5.2.3. R1_COUNTER
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved, 4 bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| R1 generation counter, 8 bytes |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Type 129
Length 12
R1 generation
counter The current generation of valid puzzles
The R1_COUNTER parameter contains a 64-bit unsigned integer in
network byte order, indicating the current generation of valid
puzzles.
Reserved, 4 bytes plus R1 generation counter, 8 bytes is the Length 12 the RFC states, and "a 64-bit unsigned integer" settles the width beyond the diagram.
The code
pcapkit/protocols/schema/internet/hip.py:506:
counter: 'int'=UInt32Field()
Four octets, not eight. reserved above it is a correct PaddingField(length=4), and _make_param_r1_counter correctly writes len=12 — so the parameter declares 12 contents octets and packs 8 of them.
Both HIP parameter codes reach this one class, and both are affected. pcapkit/const/hip/parameter.py:24,27 name them R1_Counter = 128 ("v1 only, [RFC5201], Length: 12") and R1_COUNTER = 129 ("[RFC7401], Length: 12"), and RFC 5201 §5.2.3 gives the same 4 + 8 layout, so there is no version under which four octets is right.
Measured
Both codes, through the public maker, on #664's tree (pcapkit.__file__ printed and asserted under the worktree first, CPython 3.14.7):
Twelve octets: four of type and length, four of reserved, four of counter. A conformant receiver reads Length = 12 and consumes 11 + 12 - (12 + 3) % 8 = 16, so it takes four octets of whatever follows as the tail of this parameter and then reads the rest of the parameter area at a wrong offset. Writing is the same in reverse: a real peer's 16-octet R1_COUNTER has its last four octets left for the next parameter's Type.
Why it has been invisible, in three separate ways
Each of these is worth recording, because together they are why a suite that round-trips 46 of 49 HIP codes never saw it.
HIP_COPIES = 2. The generator puts two copies of each parameter in a packet, and two identical four-octet shortfalls sum to eight, so the record area stays 8-aligned and the header len arithmetic comes out exact. See the note at examples/generators/options.py.
The round trip cannot see it at all, because pcapkit's reader consumes the same 12 octets its writer wrote. Only a comparison against the RFC's arithmetic — or a real peer — can.
It also makes the fixture walker unreliable, which is the part with the widest reach
This is the most useful thing here. #664's strongest piece of evidence is a standalone walker that parses examples/captures/options-internet.pcap using no pcapkit at all, taking its stride solely from Total Length = 11 + Length - (Length + 3) % 8. On #664's regenerated capture it reports 0 violations against 43 on the pre-fix one. That 0 is not trustworthy as a conformance statement, and this defect is why.
The walker hits the R1_COUNTER frame, reads Length = 12, advances 16, and lands four octets inside the second copy — where it reads a phantom Type = 0, Length = 0 record out of that copy's own zeroed contents, whose "padding" is four zero octets, so the zero-padding check passes and the walk ends tidily on the area boundary. It reports 92 records where 91 are real.
The reason it gets away with it is the test data: R1_COUNTER has no entry in _hip_overrides(), so the generator builds it with counter = 0, and every octet the walker mis-reads is zero. Reproduced by patching only the second copy's counter in a scratch copy of the file and re-walking:
as generated : 92 records, 0 violations
with a non-zero counter : 92 records, 1 violations
frame 101: type 0 padding not zeroed: aabbccdd
Two consequences for anyone measuring HIP conformance from that capture:
"0 violations" means "no padding violations among the records a conformant reader can find", not "the capture is conformant." Cite it that way.
Giving R1_COUNTER a non-zero counter in examples/generators/options.py's _hip_overrides() would stop the fixture hiding this, and is worth doing whether or not the width is fixed first — a zero-valued field cannot discriminate a width defect from a correct one, which is the same trap that made the SOLUTION width (HIP SOLUTION builder sizes with ceil(bits/4), emitting a parameter its own reader rejects #608) survive so long.
Suggested fix
Widen the field to eight octets and let the data model carry it, rather than deriving anything from the value — NumberField(length=8, signed=False) in place of UInt32Field(), matching what §5.2.3 states as a literal width for both HIP versions. Then len=12 and the packed contents agree, the record is the RFC's 16 octets, and parameter_total_len(12) == 16 needs no compensation from anywhere.
Two things to check while doing it, since both bit this parameter's neighbours:
Whether HIP_COPIES can then go to 1. Measured on fix(hip): pad HIP parameters to the record length RFC 7401 gives, not the contents (#651) #664, one copy round-trips 45 of 49 codes, and the four that do not are this defect (codes 128 and 129), HOST_ID (declares len=8, packs 18) and HIP_TRANSFORM (HIPv1-only, built by the generator at version 2). Fixing this one clears two of the four.
Note that this is a wire-output and data-model change, so it wants the breaking label alongside fix on whatever PR carries it, on the same reasoning as #664.
R1CounterParameter.counteris aUInt32Fieldwhere RFC 7401 §5.2.3 gives the R1 generation counter 8 bytes, so the parameter packs 12 octets where the RFC total is 16 — four short, at4 (mod 8), for both code 128 and code 129.Found while measuring #651 (fixed in #664), which is where every number below comes from. Not fixed there: it is a field-width change with a data-model consequence and wants its own review, exactly as the SOLUTION width (#608) did in #629.
The RFC text, fetched
https://www.rfc-editor.org/rfc/rfc7401.txt, 309,319 octets, sha25609366b9f83dc80593172304ffcf05f57afd186aacb468ac6170a8fe26944c4be. §5.2.3, verbatim:Reserved, 4 bytesplusR1 generation counter, 8 bytesis theLength 12the RFC states, and "a 64-bit unsigned integer" settles the width beyond the diagram.The code
pcapkit/protocols/schema/internet/hip.py:506:Four octets, not eight.
reservedabove it is a correctPaddingField(length=4), and_make_param_r1_countercorrectly writeslen=12— so the parameter declares 12 contents octets and packs 8 of them.Both HIP parameter codes reach this one class, and both are affected.
pcapkit/const/hip/parameter.py:24,27name themR1_Counter = 128("v1 only, [RFC5201], Length: 12") andR1_COUNTER = 129("[RFC7401], Length: 12"), and RFC 5201 §5.2.3 gives the same 4 + 8 layout, so there is no version under which four octets is right.Measured
Both codes, through the public maker, on #664's tree (
pcapkit.__file__printed and asserted under the worktree first, CPython 3.14.7):Twelve octets: four of type and length, four of
reserved, four ofcounter. A conformant receiver readsLength = 12and consumes11 + 12 - (12 + 3) % 8 = 16, so it takes four octets of whatever follows as the tail of this parameter and then reads the rest of the parameter area at a wrong offset. Writing is the same in reverse: a real peer's 16-octet R1_COUNTER has its last four octets left for the next parameter'sType.Why it has been invisible, in three separate ways
Each of these is worth recording, because together they are why a suite that round-trips 46 of 49 HIP codes never saw it.
HIP_COPIES = 2. The generator puts two copies of each parameter in a packet, and two identical four-octet shortfalls sum to eight, so the record area stays 8-aligned and the headerlenarithmetic comes out exact. See the note atexamples/generators/options.py.Length = 12appended four surplus octets — precisely the four this parameter is missing, bringing the record to 16. That is whyR1_COUNTERround-tripped at one copy on0c7f2b7c9and does not on fix(hip): pad HIP parameters to the record length RFC 7401 gives, not the contents (#651) #664: measured, one copy gave4 OKof 49 codes before and45 OKafter, andR1_COUNTERis the single code that moved from OK to failing. Correcting the padding did not break it; it stopped hiding it.It also makes the fixture walker unreliable, which is the part with the widest reach
This is the most useful thing here. #664's strongest piece of evidence is a standalone walker that parses
examples/captures/options-internet.pcapusing no pcapkit at all, taking its stride solely fromTotal Length = 11 + Length - (Length + 3) % 8. On #664's regenerated capture it reports 0 violations against 43 on the pre-fix one. That 0 is not trustworthy as a conformance statement, and this defect is why.The walker hits the R1_COUNTER frame, reads
Length = 12, advances 16, and lands four octets inside the second copy — where it reads a phantomType = 0, Length = 0record out of that copy's own zeroed contents, whose "padding" is four zero octets, so the zero-padding check passes and the walk ends tidily on the area boundary. It reports 92 records where 91 are real.The reason it gets away with it is the test data:
R1_COUNTERhas no entry in_hip_overrides(), so the generator builds it withcounter = 0, and every octet the walker mis-reads is zero. Reproduced by patching only the second copy's counter in a scratch copy of the file and re-walking:Two consequences for anyone measuring HIP conformance from that capture:
R1_COUNTERa non-zerocounterinexamples/generators/options.py's_hip_overrides()would stop the fixture hiding this, and is worth doing whether or not the width is fixed first — a zero-valued field cannot discriminate a width defect from a correct one, which is the same trap that made the SOLUTION width (HIP SOLUTION builder sizes with ceil(bits/4), emitting a parameter its own reader rejects #608) survive so long.Suggested fix
Widen the field to eight octets and let the data model carry it, rather than deriving anything from the value —
NumberField(length=8, signed=False)in place ofUInt32Field(), matching what §5.2.3 states as a literal width for both HIP versions. Thenlen=12and the packed contents agree, the record is the RFC's 16 octets, andparameter_total_len(12) == 16needs no compensation from anywhere.Two things to check while doing it, since both bit this parameter's neighbours:
_read_param_r1_counterand_make_param_r1_counterneed anything beyond the field change. They passcounterstraight through, so probably not, but the SOLUTION work (HIP SOLUTION builder sizes with ceil(bits/4), emitting a parameter its own reader rejects #608, Re-serialising a parsed HIP PUZZLE or SOLUTION loses leading zero octets: a len=20 parameter rebuilds as len=6 #653) found width defects paired with maker-side arithmetic more than once.HIP_COPIEScan then go to 1. Measured on fix(hip): pad HIP parameters to the record length RFC 7401 gives, not the contents (#651) #664, one copy round-trips 45 of 49 codes, and the four that do not are this defect (codes 128 and 129),HOST_ID(declareslen=8, packs 18) andHIP_TRANSFORM(HIPv1-only, built by the generator at version 2). Fixing this one clears two of the four.Note that this is a wire-output and data-model change, so it wants the
breakinglabel alongsidefixon whatever PR carries it, on the same reasoning as #664.Related