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
RFC 7401 §5.2.5 names the SOLUTION parameter's second contents octet Reserved, "zero when sent, ignored when received". SolutionParameter names that octet lifetime and both sides treat it as PUZZLE's Lifetime — a duration encoded as 2^(value - 32) seconds. So pcapkit writes 0x20, 0x21 or 0x25 into a field the RFC says MUST be zero, and it cannot write the zero at all: asking for it reaches math.log2(0) and escapes a bare ValueError.
Those two halves are one causal chain rather than two coincidences, which is why they are filed together. The RFC-mandated value for this octet is zero; zero is the one value the builder cannot encode; and a conformant peer's SOLUTION — whose Reserved is correctly 0x00 — parses to timedelta(0) and then cannot be re-serialised at all.
The RFC text, fetched
Fetched from https://www.rfc-editor.org/rfc/rfc7401.txt (sha256 09366b9f83dc80593172304ffcf05f57afd186aacb468ac6170a8fe26944c4be). SOLUTION is §5.2.5, not §5.2.4 — §5.2.4 is PUZZLE. Verbatim, RFC 7401 §5.2.5:
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| #K, 1 byte | Reserved | Opaque, 2 bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Random #I, n bytes |
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Puzzle solution #J, RHASH_len / 8 bytes |
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Type 321
Length 4 + RHASH_len / 4
#K #K is the number of verified bits
Reserved zero when sent, ignored when received
Opaque copied unmodified from the received PUZZLE
parameter
Random #I random number of size RHASH_len bits
Puzzle solution #J random number of size RHASH_len bits
and for contrast, PUZZLE at §5.2.4, where a lifetime genuinely lives:
The 2^(value - 32) encoding pcapkit applies to both is PUZZLE's, and §5.2.4 is the only place the RFC defines it.
HIPv1 agrees, so this is not a version-specific reading. RFC 5201 (fetched from https://www.rfc-editor.org/rfc/rfc5201.txt, sha256 8b42d181a8e239713eb8d608c11e8e75829561d994adbd3caa96c6db6e69cef6), §5.2.5:
| K, 1 byte | Reserved | Opaque, 2 bytes |
...
Reserved zero when sent, ignored when received
against its own §5.2.4, where PUZZLE has | K, 1 byte | Lifetime | and Lifetime puzzle lifetime 2^(value-32) seconds. Both HIP versions name SOLUTION's second octet Reserved and require it to be zero, and pcapkit uses one SolutionParameter schema for both, so there is no version under which the current field name is right.
The sites
pcapkit/protocols/schema/internet/hip.py:450, in the SolutionParameter declared at :444, gives the octet a lifetime field where the RFC has Reserved:
Two of those four are in _make_param_puzzle, where Lifetime is a real field, so the bare-exception half of this is not SOLUTION-specific — it is shared by both builders. Only the misnamed-field half is specific to SOLUTION.
Measured
On origin/main (a62aed134), CPython 3.14.7, importing from an immutable git archive snapshot with pcapkit.__file__ asserted against it:
MEASURED pcapkit.__file__ = /tmp/hipissues/tA62/pcapkit/__init__.py
CPython 3.14.7
SOLUTION: the octet written at the Reserved position (RFC 7401 5.2.5)
lifetime=0 -> ValueError: expected a positive input
lifetime=1 -> schema.lifetime=0x20 wire octet[5]=0x20 len=20
lifetime=2 -> schema.lifetime=0x21 wire octet[5]=0x21 len=20
lifetime=32 -> schema.lifetime=0x25 wire octet[5]=0x25 len=20
lifetime=3600 -> schema.lifetime=0x2b wire octet[5]=0x2b len=20
Octet 5 of the parameter is the Reserved position. 0x20, 0x21, 0x25 and 0x2b are all non-zero, and the RFC says the field is zero when sent. The default — lifetime: 'timedelta | int' = 0, at pcapkit/protocols/internet/hip.py:3170 for SOLUTION and :3128 for PUZZLE — is the one input that would produce the conformant 0x00, and it raises instead.
The exception, with the raise site (the traceback needs an explicit limit, because pcapkit sets sys.tracebacklimit = 0 when a BaseError is raised, which silently truncates an ordinary extract_tb to nothing):
_make_param_solution(lifetime=0) -> ValueError: expected a positive input
isinstance(exc, BaseError) = False
pcapkit/protocols/internet/hip.py:3198 in _make_param_solution | lifetime = math.floor(math.log2(
_make_param_puzzle(lifetime=0) -> ValueError: expected a positive input
isinstance(exc, BaseError) = False
pcapkit/protocols/internet/hip.py:3154 in _make_param_puzzle | lifetime = math.floor(math.log2(
ValueError: expected a positive input is CPython 3.14's math.log2(0) message — confirmed by calling math.log2(0) directly in the same interpreter.
A conformant SOLUTION cannot be re-serialised at all
This is the part that makes it more than a bad default. A parameter whose Reserved octet is 0x00 — the only value the RFC permits — round-trips into an unbuildable object:
conformant-Reserved SOLUTION on the wire (28 octets):
01 41 00 14 01 00 6f 70 80 00 00 00 00 00 00 00 80 00 00 00 00 00 00 00 00 00 00 00
type len=20 #K ^^ opaque Random #I solution #J padding
Reserved = 0x00, as RFC 7401 5.2.5 and RFC 5201 5.2.5 both require
parsed -> lifetime = datetime.timedelta(0)
rebuild with param=<that parsed object>:
ValueError: expected a positive input, got 0.0
isinstance(exc, pcapkit.utilities.exceptions.BaseError) = False
2 ** (0 - 32) seconds is 2.3283064365386963e-10, which datetime.timedelta rounds to timedelta(0), and math.log2(0.0) then raises. The same happens for a PUZZLE whose Lifetime octet is 0x00, which is a legal encoding meaning 2^-32 seconds:
PUZZLE with Lifetime=0x00 parses to datetime.timedelta(0); rebuild:
ValueError: expected a positive input, got 0.0
isinstance(exc, BaseError) = False
So the reachability is not hypothetical and does not need a crafted input: parse a conformant SOLUTION, re-emit it, get a bare ValueError.
The bare exception breaks the house convention
pcapkit/utilities/exceptions.py exists so that "only user error stack information" is shown when an exception reaches the user, and its module docstring describes the whole family as "refined built-in exceptions". The mechanism is BaseError: raising one logs once at logging.CRITICAL and, outside development mode, sets sys.tracebacklimit = 0 so the user sees the exception line rather than a walk through pcapkit's internals. ProtocolError — the type this code already raises for malformed parameters, itself a ValueError subclass — is in that family.
A bare ValueError from math.log2 gets none of it. It is not an instance of BaseError (measured above), so it is invisible to except BaseError, it is not logged, and it arrives with a full internal traceback and the message expected a positive input, which names neither HIP nor the parameter nor the field. The right shape here is the same ProtocolError the readers raise, naming the parameter and the offending value.
Why nothing caught it
examples/generators/options.py:975-977 dodges it explicitly, and records that it is doing so:
So the crash is already documented in the repository, as a generator override rather than as a bug. The override has a second consequence worth spelling out: {'lifetime': 1} sets onlylifetime, leaving random and solution at their 0 defaults. That means the round-trip generator exercised the SOLUTION length formula at zero bits — maximally non-discriminating, since every candidate formula agrees there — which is why no fixture ever caught #608. One override written to dodge this defect is the reason the neighbouring one stayed hidden.
Nothing else reaches lifetime=0: the round-trip suite is the only caller, and it overrides.
Re-verified here against a62aed134 before filing, with the RFC fetched rather than paraphrased. Also reproduces identically on 375e9d411, a62aed134's parent, where the four math.log2 sites are at :3146, :3150, :3189 and :3194 — fix(hip): size the SOLUTION parameter as two whole-octet fields (#608) #629 added four lines of comment above them and shifted each by four.
Two separable repairs, which is worth noting because one is riskier than the other. Raising a ProtocolError instead of letting ValueError escape is local to the four sites and changes no wire format. Renaming SOLUTION's octet to reserved and writing zero into it changes the octets of every SOLUTION fixture and the data model's public field name, so it belongs with a deliberate regeneration.
RFC 7401 §5.2.5 names the SOLUTION parameter's second contents octet
Reserved, "zero when sent, ignored when received".SolutionParameternames that octetlifetimeand both sides treat it as PUZZLE'sLifetime— a duration encoded as2^(value - 32)seconds. So pcapkit writes0x20,0x21or0x25into a field the RFC says MUST be zero, and it cannot write the zero at all: asking for it reachesmath.log2(0)and escapes a bareValueError.Those two halves are one causal chain rather than two coincidences, which is why they are filed together. The RFC-mandated value for this octet is zero; zero is the one value the builder cannot encode; and a conformant peer's SOLUTION — whose
Reservedis correctly0x00— parses totimedelta(0)and then cannot be re-serialised at all.The RFC text, fetched
Fetched from
https://www.rfc-editor.org/rfc/rfc7401.txt(sha25609366b9f83dc80593172304ffcf05f57afd186aacb468ac6170a8fe26944c4be). SOLUTION is §5.2.5, not §5.2.4 — §5.2.4 is PUZZLE. Verbatim, RFC 7401 §5.2.5:and for contrast, PUZZLE at §5.2.4, where a lifetime genuinely lives:
The
2^(value - 32)encoding pcapkit applies to both is PUZZLE's, and §5.2.4 is the only place the RFC defines it.HIPv1 agrees, so this is not a version-specific reading. RFC 5201 (fetched from
https://www.rfc-editor.org/rfc/rfc5201.txt, sha2568b42d181a8e239713eb8d608c11e8e75829561d994adbd3caa96c6db6e69cef6), §5.2.5:against its own §5.2.4, where PUZZLE has
| K, 1 byte | Lifetime |andLifetime puzzle lifetime 2^(value-32) seconds. Both HIP versions name SOLUTION's second octetReservedand require it to be zero, and pcapkit uses oneSolutionParameterschema for both, so there is no version under which the current field name is right.The sites
pcapkit/protocols/schema/internet/hip.py:450, in theSolutionParameterdeclared at:444, gives the octet alifetimefield where the RFC hasReserved:Both sides then convert it as a duration.
pcapkit/protocols/internet/hip.py:1101on the way in (and:1041for PUZZLE, where it is correct):and on the way out, four
math.log2call sites across the two builders —pcapkit/protocols/internet/hip.py:3150,:3154,:3193and:3198:Two of those four are in
_make_param_puzzle, whereLifetimeis a real field, so the bare-exception half of this is not SOLUTION-specific — it is shared by both builders. Only the misnamed-field half is specific to SOLUTION.Measured
On
origin/main(a62aed134), CPython 3.14.7, importing from an immutablegit archivesnapshot withpcapkit.__file__asserted against it:Octet 5 of the parameter is the
Reservedposition.0x20,0x21,0x25and0x2bare all non-zero, and the RFC says the field is zero when sent. The default —lifetime: 'timedelta | int' = 0, atpcapkit/protocols/internet/hip.py:3170for SOLUTION and:3128for PUZZLE — is the one input that would produce the conformant0x00, and it raises instead.The exception, with the raise site (the traceback needs an explicit limit, because pcapkit sets
sys.tracebacklimit = 0when aBaseErroris raised, which silently truncates an ordinaryextract_tbto nothing):ValueError: expected a positive inputis CPython 3.14'smath.log2(0)message — confirmed by callingmath.log2(0)directly in the same interpreter.A conformant SOLUTION cannot be re-serialised at all
This is the part that makes it more than a bad default. A parameter whose
Reservedoctet is0x00— the only value the RFC permits — round-trips into an unbuildable object:2 ** (0 - 32)seconds is2.3283064365386963e-10, whichdatetime.timedeltarounds totimedelta(0), andmath.log2(0.0)then raises. The same happens for a PUZZLE whoseLifetimeoctet is0x00, which is a legal encoding meaning2^-32seconds:So the reachability is not hypothetical and does not need a crafted input: parse a conformant SOLUTION, re-emit it, get a bare
ValueError.The bare exception breaks the house convention
pcapkit/utilities/exceptions.pyexists so that "only user error stack information" is shown when an exception reaches the user, and its module docstring describes the whole family as "refined built-in exceptions". The mechanism isBaseError: raising one logs once atlogging.CRITICALand, outside development mode, setssys.tracebacklimit = 0so the user sees the exception line rather than a walk through pcapkit's internals.ProtocolError— the type this code already raises for malformed parameters, itself aValueErrorsubclass — is in that family.A bare
ValueErrorfrommath.log2gets none of it. It is not an instance ofBaseError(measured above), so it is invisible toexcept BaseError, it is not logged, and it arrives with a full internal traceback and the messageexpected a positive input, which names neither HIP nor the parameter nor the field. The right shape here is the sameProtocolErrorthe readers raise, naming the parameter and the offending value.Why nothing caught it
examples/generators/options.py:975-977dodges it explicitly, and records that it is doing so:So the crash is already documented in the repository, as a generator override rather than as a bug. The override has a second consequence worth spelling out:
{'lifetime': 1}sets onlylifetime, leavingrandomandsolutionat their0defaults. That means the round-trip generator exercised the SOLUTION length formula at zero bits — maximally non-discriminating, since every candidate formula agrees there — which is why no fixture ever caught #608. One override written to dodge this defect is the reason the neighbouring one stayed hidden.Nothing else reaches
lifetime=0: the round-trip suite is the only caller, and it overrides.Notes
a62aed134before filing, with the RFC fetched rather than paraphrased. Also reproduces identically on375e9d411,a62aed134's parent, where the fourmath.log2sites are at:3146,:3150,:3189and:3194— fix(hip): size the SOLUTION parameter as two whole-octet fields (#608) #629 added four lines of comment above them and shifted each by four.ProtocolErrorinstead of lettingValueErrorescape is local to the four sites and changes no wire format. Renaming SOLUTION's octet toreservedand writing zero into it changes the octets of every SOLUTION fixture and the data model's public field name, so it belongs with a deliberate regeneration.version).0x20/0x21/0x25octets are a second, independent reason the HIP fixtures need regenerating, alongside HIP parameter padding aligns the contents, not the record, so every parameter pcapkit emits is 4 (mod 8) octets #651's padding. Whoever regenerates them should land both, since each one alone still leaves every SOLUTION fixture non-conformant.