fix(fields): raise ProtocolError, not struct.error, on a negative resolved field length - #811
Conversation
…olved field length (#805) - FieldBase.length (pcapkit/corekit/fields/field.py) now catches struct.error from struct.calcsize and re-raises ProtocolError. A length=lambda pkt: pkt['__length__']-style callback can resolve negative once Schema.unpack's running counter has been overdrawn by a preceding field, building a template such as '-5s' that struct.calcsize cannot size -- pre-fix this escaped as a bare struct.error, uncatchable by ordinary caller code. This is the one choke point every __length__-keyed field in the ten affected schema modules shares (application/ftp.py, httpv1.py, httpv2.py, ngap.py; internet/hip.py, ipv6_route.py, mh.py; link/ethernet.py; misc/pcapng.py; transport/sctp.py), so no other module needs a change, and success is untouched: the property still returns the same value for every non-negative length. - Left schema.py's running-counter warning alone: measured that converting it would reject a SETTINGS frame with a short trailing entry, which parses successfully today while only warning. - Updated test_http_unit.py's existing pinning test, which documented the direct-construction leak as expected pre-#805 behaviour, to assert ProtocolError instead; the guess-path's struct.error suppression stays as defence in depth. New: tests/corekit/test_fields_field.py::FieldBaseLengthNegativeResolvedLengthTests and tests/protocols/application/test_httpv2_negative_length_unit.py, each shown failing with a bare struct.error on stock code and passing after this change. Build/test: coverage run -m unittest over the touched files and their existing suites, 117 tests green; field.py's new lines fully covered, http.py and httpv2.py 100%.
|
Verified by me on a Placing the guard in Two process problems to fix before this is ready, neither about the code:
One claim I could not reproduce and am not holding against the PR: the issue's exhaustive 1136 escape count. My own sweep covers 28 representative cases; the author reports the same and says so honestly rather than asserting the figure.
Still |
|
Cross-review verdict: GOOD TO GO (haiku — the same reviewer that identified this root cause on #802; author was sonnet). One non-blocking finding, verified by me, plus a correction to the reviewer. It reproduced the 1136 figure the author could not, and it goes to zero:
The finding, confirmed by me independently —
Correcting the reviewer, not the PR. It reported that it could not locate So the #805 worker's original blast-radius claims stand, and the reviewer's correction of them does not. Its conclusion — that the fix is inert on those nine modules — is unaffected, since it rests on the no-new-success-path argument plus a catcher audit plus two green suites, all of which I accept. Its sharper check on the nine modules is worth keeping: rather than re-running nine suites, it looked for anything that depended on catching Still outstanding, and it is the author's to fix: the checklist's |
`main` moved from 4530424 to 3cbdf89 while this PR sat open. Four are the new PRs merged in that window (#811-#814); the other four are older defects (#704, #723, #739, #743/#746) whose fixes had merged earlier but were never cited. Eight new bullets cover them, appended in merge order: - #704 -- `SystemdJournalExportBlock.post_process` skipped a binary field's trailing newline by reading to EOF, discarding every field behind it. - #723 -- the same block split entries on a bare `b'\n\n'`, shredding binary data that contains that byte pair; fixed alongside an independent trailing-separator/EOF ambiguity. - #739 -- five more registrars (`register_engine`/`_reassembly`/`_traceflow`, `register_dumper` x2) sat outside #718/#726's identity guard. - #743, #746 -- `pypcapfile`'s `IP.src`/`.dst` are dotted-decimal text, not packed bytes, and its frames need un-hexlifying before decoding; the two fixes are cross-dependent and landed together. - #805 -- `FieldBase.length`'s `struct.calcsize` on a negative resolved length raised a bare `struct.error`; now `ProtocolError`. Closes the follow-on #802's own entry filed as out of scope. - #796 -- thirteen `re.sub` sites under `pcapkit/vendor/` passed `re.MULTILINE` positionally as `count`, not as `flags=`. - #800 -- `httpv2._guess_version` now identifies a connection preface before parsing it, rather than by trial and error. Derived the gap by diffing `git log 73f09ae..origin/main` against `gh pr view --json state,mergedAt` for every candidate number, not from commit-subject text alone. `CHANGELOG.md` regenerated with `util/changelog_md.py`; `--check` exit 0 and `test_changelog_md.py`'s 47 tests pass. Sphinx's full-site build did not finish inside budget -- `pcapkit.const.reg`'s autodoc page is slow regardless of this change -- so verified instead with `docutils --report=1`, which parses the updated file with zero messages.
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
…eneric wrap - test_guess_version_reports_a_preface_with_no_frame_as_such's GOAWAY case asserted str(exception) == 'HTTP/2: invalid format', which #814 produced when the sixteen-octet GOAWAY's oversized declared length drove the ``debug`` field negative and struct.calcsize raised a bare struct.error. - #811 (fix(fields): raise ProtocolError, not struct.error, on a negative resolved field length) landed after #814's branch point and fixed that exact case at its root: FieldBase.length now raises ProtocolError itself, so http.py's ``except ProtocolError: raise`` passes it through unchanged instead of reaching the ``except (ValueError, struct.error)`` wrap that produced #814's message. The assertion was stale, not the behaviour. - Update the assertion to the field-level message ("Field debug resolved to a negative length; template='-1s'") while keeping every other check #814 cared about: still a catchable BaseError, still not a bare struct.error, still chained to the original struct.error via __cause__. Verified the failure on stock 3cbdf89 (CPython 3.14) and the fix passing on both 3.14 and 3.10; tests/protocols/application/ and tests/corekit/ otherwise pass (338 passed, 16 skipped -- 5 unrelated pre-existing failures in test_http_runtime.py were just missing generated sample captures, fixed by running examples/generators/make_samples.py). No production code changed, so coverage of pcapkit/protocols/application/http.py (98%) and pcapkit/corekit/fields/field.py (75%) is unchanged. Closes #822
…k comment (#824) The fallback-arm comment in `_guess_version` (roughly :358-395) documented measurements from before #799 and #811 landed, so three sentences described behaviour the code no longer has: - `HTTP(io.BytesIO(b'\x00' * 8), 8)` was cited as raising a bare `struct.error`; #799's nine-octet guard now answers `ProtocolError: unknown HTTP version` instead, re-measured on this tree. - The 16-octet `GOAWAY` example was cited as still raising a bare `struct.error` through `httpv2.HTTP` directly, with the fix left as future work "tracked as #805". #811 closed that at `FieldBase.length`; the same input now raises `ProtocolError: Field debug resolved to a negative length; template='-1s'`. - Both sentences are rewritten in the past tense with the current, re-measured outcome, keeping the surrounding causal explanation (why the last arm still suppresses `struct.error`, as defence in depth) intact. Comment-only change, no behaviour touched. Verified `tests/protocols/application/` still passes (122 passed, 1 pre-existing failure: #822's `test_guess_version_reports_a_preface_with_no_frame_as_such`, confirmed failing identically on stock `origin/main`, fixed by open PR #823 rather than here). Closes #824
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
…ent (#824) The `_guess_version` comments (roughly :310-405) documented measurements from before #799 and #811 landed, so several sentences described behaviour the code no longer has: - `HTTP(io.BytesIO(b'\x00' * 8), 8)` was cited as raising a bare `struct.error`; #799's nine-octet guard now answers `ProtocolError: unknown HTTP version` instead, re-measured on this tree. - A 16-octet `GOAWAY` was cited as still raising a bare `struct.error` through `httpv2.HTTP` directly, present tense, in two places (the fallback arm and the preface arm's #805-residual note) — one with the fix left as future work "tracked as #805". #811 closed that at `FieldBase.length`; the same input now raises `ProtocolError: Field debug resolved to a negative length; template='-1s'`. - Restored the dropped note that #811 deliberately left `Schema.unpack`'s own running-counter warning alone, since converting it would reject a `SETTINGS` frame with a short trailing entry that parses successfully today while only warning — that `SchemaWarning` still fires. - Reworded the last arm's "stays regardless" permanence claim to attribute the decision to #811 and point at #825 as the open question, rather than asserting the suppression can never be removed. All rewritten past tense with the re-measured, current outcome, keeping the causal explanation for each suppression/conversion intact. Comment-only, no behaviour touched — confined to `pcapkit/protocols/application/http.py` (21 code objects, 0 structurally differing in bytecode, non-code constants, names, or def line numbers). Verified `tests/protocols/application/` passes (123 passed, 0 failed, 432 subtests). isort/mypy/pylint clean on this file (pylint's 5 pre-existing findings are all outside the changed lines). Closes #824
…in FieldBase.length (#825) struct.calcsize raises the identical bare struct.error for a malformed template as for a negative resolved count (measured on 3.14.7: calcsize('-1s') and calcsize('Xs') both raise "bad char in struct format"). #811's guard caught that blanket and always reported "resolved to a negative length", misdiagnosing a typo'd template. - pcapkit/corekit/fields/field.py: add _RE_NEGATIVE_LENGTH_TEMPLATE, matching a negative resolved count's leading '-', optionally preceded by one of NumberField's byte-order prefixes ('@=<>!'). Every template built from a resolved length is f'{length}s' (strings.py, misc.py, collections.py, and numbers.py's build_template `else` arm before it gets byte-order-prefixed as f'{endian}{struct_fmt}') -- so the minus sign is always there, but a prefix in front of it defeats a plain '^-\d+' anchor (caught by #827's cross-review: NumberField(length=-1) produced '>-1s', misdiagnosed as malformed). FieldBase.length checks the pattern once struct.calcsize has failed, and raises a distinct, template-naming ProtocolError for anything else calcsize cannot size. Both branches still chain from the real struct.error via `from error`, so no bare struct.error escapes either way, and the negative-length message and __cause__ chain are byte-for-byte unchanged for the existing (unprefixed) case -- so the pinned message assertion at tests/protocols/application/test_http_unit.py:662 needs no change. - tests/corekit/test_fields_field.py: three new tests on FieldBaseLengthNegativeResolvedLengthTests -- a malformed template raises ProtocolError naming the template and not claiming a negative length; neither category ever leaks a bare struct.error; and a real NumberField(length=-1), whose template comes out byte-order-prefixed ('>-1s'), is still reported as negative, not malformed. All three fail on stock/pre-fix code with the wrong diagnosis and pass after. #825 also asks whether the negative length should be prevented, not just correctly reported, by bounding pkt['__length__'] at the schema layer. Measured directly (both a global clamp in schema.py's decrement and a narrow max(pkt['__length__'], 0) on GoawayFrame.debug / ContinuationFrame.fragment, matching the existing sctp.py/pcapng.py convention): the GOAWAY repro still raises ProtocolError either way, just a different one ("HTTP/2: [Type 7] invalid format" from httpv2.py's own schema.length > length guard, __cause__ None) that breaks the pinned assertion in three ways, not just its message -- so the one relaxation this change is allowed would not save it. Left for its own change with the schema.py owner's input; not attempted here. Build: mypy and isort (-l100 -ppcapkit) clean on the changed source file. tests/corekit/test_fields_field.py's affected test class (5/5) and the application-side pinned scenario verified directly; full tests/corekit and tests/protocols/application suites re-run.
…ent (#824) The `_guess_version` comments (roughly :310-405) documented measurements from before #799 and #811 landed, so several sentences described behaviour the code no longer has: - `HTTP(io.BytesIO(b'\x00' * 8), 8)` was cited as raising a bare `struct.error`; #799's nine-octet guard now answers `ProtocolError: unknown HTTP version` instead, re-measured on this tree. - A 16-octet `GOAWAY` was cited as still raising a bare `struct.error` through `httpv2.HTTP` directly, present tense, in two places (the fallback arm and the preface arm's #805-residual note) — one with the fix left as future work "tracked as #805". #811 closed that at `FieldBase.length`; the same input now raises `ProtocolError: Field debug resolved to a negative length; template='-1s'`. - Restored the dropped note that #811 deliberately left `Schema.unpack`'s own running-counter warning alone, since converting it would reject a `SETTINGS` frame with a short trailing entry that parses successfully today while only warning — that `SchemaWarning` still fires. - Reworded the last arm's "stays regardless" permanence claim to attribute the decision to #811 and point at #825 as the open question, rather than asserting the suppression can never be removed. All rewritten past tense with the re-measured, current outcome, keeping the causal explanation for each suppression/conversion intact. Comment-only, no behaviour touched — confined to `pcapkit/protocols/application/http.py` (21 code objects, 0 structurally differing in bytecode, non-code constants, names, or def line numbers). Verified `tests/protocols/application/` passes (123 passed, 0 failed, 432 subtests). isort/mypy/pylint clean on this file (pylint's 5 pre-existing findings are all outside the changed lines). Closes #824
…in FieldBase.length (#825) struct.calcsize raises the identical bare struct.error for a malformed template as for a negative resolved count (measured on 3.14.7: calcsize('-1s') and calcsize('Xs') both raise "bad char in struct format"). #811's guard caught that blanket and always reported "resolved to a negative length", misdiagnosing a typo'd template. - pcapkit/corekit/fields/field.py: add _RE_NEGATIVE_LENGTH_TEMPLATE, matching a negative resolved count's leading '-', optionally preceded by one of NumberField's byte-order prefixes ('@=<>!'). Every template built from a resolved length is f'{length}s' (strings.py, misc.py, collections.py, and numbers.py's build_template `else` arm before it gets byte-order-prefixed as f'{endian}{struct_fmt}') -- so the minus sign is always there, but a prefix in front of it defeats a plain '^-\d+' anchor (caught by #827's cross-review: NumberField(length=-1) produced '>-1s', misdiagnosed as malformed). FieldBase.length checks the pattern once struct.calcsize has failed, and raises a distinct, template-naming ProtocolError for anything else calcsize cannot size. Both branches still chain from the real struct.error via `from error`, so no bare struct.error escapes either way, and the negative-length message and __cause__ chain are byte-for-byte unchanged for the existing (unprefixed) case -- so the pinned message assertion at tests/protocols/application/test_http_unit.py:662 needs no change. - tests/corekit/test_fields_field.py: three new tests on FieldBaseLengthNegativeResolvedLengthTests -- a malformed template raises ProtocolError naming the template and not claiming a negative length; neither category ever leaks a bare struct.error; and a real NumberField(length=-1), whose template comes out byte-order-prefixed ('>-1s'), is still reported as negative, not malformed. All three fail on stock/pre-fix code with the wrong diagnosis and pass after. #825 also asks whether the negative length should be prevented, not just correctly reported, by bounding pkt['__length__'] at the schema layer. Measured directly (both a global clamp in schema.py's decrement and a narrow max(pkt['__length__'], 0) on GoawayFrame.debug / ContinuationFrame.fragment, matching the existing sctp.py/pcapng.py convention): the GOAWAY repro still raises ProtocolError either way, just a different one ("HTTP/2: [Type 7] invalid format" from httpv2.py's own schema.length > length guard, __cause__ None) that breaks the pinned assertion in three ways, not just its message -- so the one relaxation this change is allowed would not save it. Left for its own change with the schema.py owner's input; not attempted here. Build: mypy and isort (-l100 -ppcapkit) clean on the changed source file. tests/corekit/test_fields_field.py's affected test class (5/5) and the application-side pinned scenario verified directly; full tests/corekit and tests/protocols/application suites re-run.
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
…s in #817 and #821 Two bullets, both non-breaking, appended after the #800 entry in merge order. Bullet count 128 to 130 (`grep -cE '^\* \*\*'`). - #804 (PR #817) -- the three `__repr__` methods #798 left `%`-formatted are f-strings now, dropping `consider-using-f-string` from both const modules and both vendor templates; the other bespoke templates in `{const,vendor}/{ftp,http}/` still carry the disable, so #804's claim holds for this pair only, not for those directories. - #682 (PR #821) -- `TCP.__proto__` no longer binds `httpv1.HTTP` directly for ports 80/8080; both repoint to the generic HTTP proxy `_guess_version` identifies through, which only became reliable once #800/#814 landed. `udp.py` already pointed there, so that side of the PR is prose-only (its port rows and docstring), not a code change, and the entry says so. Protochain over the 23 sample captures is *not* byte-identical: 9 frames in `options-transport.pcap` go `Raw` to `HTTP/2`, all 231 HTTP/1.1 frames are unaffected, and `_guess_version`'s entry count goes 0 to 252. Not marked `**a breaking change to**`: PR #821's own labels are `bug,fix,docs,test`, no `breaking`, unlike #759/#783 and #805/#811 last round, whose crediting PRs did carry it. The entry does say what a `breaking`-blind reader would still want to know -- TCP:80/8080 traffic that is neither valid HTTP/1 nor preface-carrying now reaches `_guess_version`'s fall-through arm instead of the direct `httpv1` bind's unconditional `Raw`, which is where the 12 (of 252) fall-throughs the PR measured come from. `util/changelog_md.py` regenerated `CHANGELOG.md`, first pass, no line-spanning literal this round; `--check` exit 0. `test_changelog_md.py` 47 passed.
make pylint,make mypy,make isort)make testpasses, and a test case covers the changeWhat is the purpose of your pull request?
fix— corrects a defectDescription
FieldBase.lengthcalledstruct.calcsizeon a template built from anegative resolved field length (e.g.
'-5s', from alength=lambda pkt: pkt['__length__']callback going negative once the running counter isoverdrawn), which raised a bare
struct.error— uncatchable by ordinarycaller code. It now catches that and raises
ProtocolError.This is the one choke point every affected schema module shares
(
application/{ftp,httpv1,httpv2,ngap}.py,internet/{hip,ipv6_route,mh}.py,link/ethernet.py,misc/pcapng.py,transport/sctp.py), so no other fileneeds a change, and every non-negative length still returns the same value —
well-formed input is unaffected.
schema.py's separate running-counterwarning is deliberately untouched: converting it would reject a
SETTINGSframe with a short trailing entry that parses fine today.
Closes #805