Skip to content

fix(corekit): reject a bool where a maker converts an address itself (#508) - #539

Merged
JarryShaw merged 2 commits into
mainfrom
fix/508-address-switch-bool-guard
Sep 20, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/508-address-switch-bool-guard

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Closes #508.

The issue's stated mechanism is wrong, and correcting it is most of this change

#508 reads the defect as a dispatch problem: because SwitchField picks the concrete field at runtime, a bool supposedly never reaches _IPAddressField.pre_process, so #500's guard cannot fire — which would put the fix in SwitchField. Measured, that is not what happens.

SwitchField.pre_process (pcapkit/corekit/fields/misc.py:432-445, a range I re-verified is still accurate) inspects only self._field and delegates straight to the resolved field:

if self._field is None:
    return NoValue  # type: ignore[unreachable]
return self._field.pre_process(value, packet)

So a bool that genuinely arrives at an address-selecting switch is already rejected, on every address-typed branch of every such switch — #500 covers it. A guard in SwitchField would be dead code twice over, because SwitchField.pack delegates to self._field.pack(...), which calls the resolved field's pre_process directly; SwitchField.pre_process is not even invoked on the pack path.

The real cause is upstream of the schema entirely. Seven _make_* methods have to know the address family before they can build the schema — to size an option whose length is the only thing on the wire carrying that family — so they convert the argument themselves with bare ipaddress.ip_address(). That conversion happens before the schema, so it launders True into a perfectly ordinary IPv4Address('0.0.0.1') that #500's guard can then only see as a legitimate address. Worse, the option length and family flag are then derived from the laundered address, so the selector picks the matching IPv4AddressField and no mismatch is left for anything to detect.

What changed

  • parse_ip_address(value, description, version=None) added to pcapkit/corekit/fields/ipaddress.py. It calls the existing _reject_bool as its first statement, then converts; version pins the family where the wire format fixes it, so an int widens correctly (258 is ::102 for version=6, but 0.0.1.2 for family-agnostic ip_address). It raises FieldValueError — the same class _IPAddressField.pre_process raises for the identical value, so a caller sees one exception whether the bool arrived through the schema or through a maker. (BoolError would be wrong here: it means "must be a bool", and sits with IntError/BytesError in the TypeError block.)
  • misc.py is untouched, for the reason above.
  • The seven corrupting makers routed through it.
  • The now-unused import ipaddress dropped from tcp.py (verified no remaining runtime use; the TYPE_CHECKING import is separate).
  • parse_ip_address documented in docs/source/pcapkit/corekit/fields/ipaddress.rst, naming the defining module rather than a re-export.

Re-derived counts

Both of the issue's counts are wrong, in opposite directions.

Address-typed SwitchField attributes: 10, not 7. Derived twice independently — statically, and at runtime from Schema.__fields__:

module class.attribute
internet.hip Locator.value
internet.hopopt SMFIdentificationBasedDPDOption.tid
internet.ipv6_opts SMFIdentificationBasedDPDOption.tid
internet.mh BindingIdentifierOption.address
internet.mh DelegatedMNPOption.prefix
internet.mh LMAAddressOption.address
internet.mh LMAUserPlaneAddressOption.address
internet.mh MNIDOption.identifier
internet.mh TargetCareofAddressSuboption.address
transport.tcp MPTCPAddAddress.address

The three the issue's table misses are hopopt/ipv6_opts SMFIdentificationBasedDPDOption.tid and mh BindingIdentifierOption.address — all ConditionalField-wrapped, e.g.

# pcapkit/protocols/schema/internet/mh.py:1294-1297
address: 'Optional[IPv4Address | IPv6Address | bytes]' = ConditionalField(
    SwitchField(selector=bid_address_selector),
    lambda pkt: pkt['length'] > 4,
)

so a grep anchored on = SwitchField( cannot see them. (The issue does mention BindingIdentifierOption in prose while omitting it from the table.) test_the_address_typed_switch_table_is_complete now pins this at 10 by rediscovering them from Schema.__fields__, so the next one added cannot go unnoticed — that "nobody listed it" failure mode is what #491 and #508 share.

Corrupting call sites: 7 makers, not 3. The issue lists 3, all in mh.py. The four it misses are MH._make_fid_suboption, MH._make_opt_dmnp, HIP._make_param_locator_set and TCP._make_mptcp_addaddr. Observed wire output before the fix:

maker address=True produced
MH._make_opt_bid 23080001000000000001
MH._make_opt_lmaa 2906010000000001 (vs 2906010001020304 for '1.2.3.4')
MH._make_fid_suboption 0506000000000001
MH._make_opt_dmnp 3706801800000001
MH._make_opt_lma_up 3b06000000000001
HIP._make_param_locator_set byte-identical to ip='::1'
TCP._make_mptcp_addaddr schema with address=IPv4Address('0.0.0.1'), version=4

_make_opt_dmnp needs prefix_length <= 32 to show it; the default prefix_length=64 hides it behind the range check, which is why the issue's own sweep read that site as already guarded. HIP._make_param_locator_set is the most damning: ip=True and ip='::1' were literally indistinguishable on the wire. TCP._make_mptcp_addaddr cannot be constructed end to end at all (#541), so its corruption was only ever visible on the schema the maker returns — #508 was right that it needed its own test, and it has one.

Fails-without evidence

pytest-subtests is not installed and pytest 9.1.1's native subtests print a failing subtest's parent as PASSED, so the exit code is the only honest signal. Read from a file, never through a pipe.

All three runs below were measured on this branch after it was rebased onto 691f12ab5, rather than carried over from the pre-rebase measurement.

Baseline, fix in place: exit code 0 — 80 passed, 584 subtests passed.

Proof 1 — revert all seven call sites, keep the helper: exit code 1. 18 failing subtests; only one parent produced a FAILED line. The other two tests (test_switch_backed_address_makers_reject_a_bool, test_both_bool_guards_stay_catchable_as_value_error_and_as_base_error) had failing subtests and were counted in "79 passed".

SUBFAILED(option='bid'|'lmaa'|'lma_up'|'tcoa'|'dmnp', value=True|False)   ... test_mh_unit.py
SUBFAILED(site='hip.Locator.value', value=True|False)                    ... test_fields_ipaddress.py
SUBFAILED(site='tcp.MPTCPAddAddress.address', value=True|False)          ... test_fields_ipaddress.py
18 failed, 79 passed, 567 subtests passed

Those seven distinct labels are the independent confirmation of the 7-maker count.

Proof 2 — remove only _reject_bool(value, description) from inside parse_ip_address, leave all routing in place: exit code 1, 21 failing subtests, and zero FAILED parent lines — every parent reported as passing. This is the starkest form of the pytest hazard, and it shows the helper's guard is load-bearing rather than the routing alone.

After each revert the tree was restored and verified byte-identical to the committed one.

Every measurement was taken with PYTHONSAFEPATH=1 and PYTHONPATH set to this worktree, with pcapkit.__file__ printed and asserted to start with the worktree root first. That matters here: an editable install (__editable___pypcapkit_1_4_1_post2_finder) is present in the venv, and I confirmed it appends to sys.meta_path (:76), so PathFinder still wins and the measurements are of this tree rather than the main checkout.

Coverage

coverage run -m pytest (no pytest-cov) over the three test files:

  • pcapkit/corekit/fields/ipaddress.py93%, missing lines 67, 294, 401, 439, 489, 534. The new parse_ip_address spans 115–216, so none of its lines are missing.
  • pcapkit/protocols/internet/mh.py — 92%; the five changed makers all covered.
  • The changed lines in hip.py (3081, 3086, 3091) and tcp.py (2907) are all covered.

EXPECTED_FAILURES in tests/protocols/test_option_roundtrip_unit.py is still 55 (imported, not grepped — it uses ** unpacking), i.e. this change neither fixes nor breaks an option round-trip.

Local suite

Rebased onto 691f12ab5. The CI-equivalent unit selection (--ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py') is green: exit code 0 — 1058 passed, 8 skipped, 2602 subtests passed, zero failures, with nothing to disclaim.

An earlier revision of this description reported one pre-existing failure on maintests/test_docstring_contract.py::DocstringParameterTests::test_known_defects_are_still_defects, from a stale KNOWN_DEFECTS entry for pcapkit/vendor/ipx/packet.py::process. #538 has since removed that entry, so it is gone. Re-measured on this rebased branch: that file alone runs at exit code 0, 7 passed, 13 subtests passed.

GitHub Actions checks on this PR have not started — the queue is currently wedged rather than merely slow, so the run being absent says nothing about the change.

Found but deliberately NOT fixed

Everything in this section is a real, verified defect that is out of scope for this PR. Each was reproduced on this branch's tree, i.e. these are not fixed by the change above. All are now tracked:

  • IPv4/IPv6 make paths still launder a bool into an address in four more places (follow-up to #508) #540 — the four remaining bool-laundering make paths, filed as one issue: ARP._make_proto_resolve (pcapkit/protocols/link/arp.py:400,402, True00000001 for IPv4 and …0001 for IPv6, against 01020304 for a real address); IPv6_Route._make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py:737,745,753,764,768, worse than a wrong address — the bool becomes a 4-byte IPv4Address which then corrupts the derived compression metadata: dst=IPv6Address('::1'), ip=[True] gave cmpr_i=16 cmpr_e=3 addresses=['01']); IPv6_Route.make's dst (ipv6_route.py:314, same shape in make rather than a _make_*); and the latent OSPF._make_id_numbers (pcapkit/protocols/link/ospf.py:318 — same conversion, but grep -rn '_make_id_numbers' pcapkit/ returns only the def, so no live caller).
  • TCP._make_mptcp_addaddr cannot pack: kind/length rejected as fields, then the port predicate KeyErrors on length #541TCP._make_mptcp_addaddr cannot be constructed end to end (KeyError: 'length' from pcapkit/protocols/schema/transport/tcp.py:790). Independent of this fix and left alone; noted in a code comment at the call site.

They were left out of this PR because its file ownership does not extend to arp.py, ipv6_route.py or ospf.py, and because #481 fixing exactly one call site is how the root cause survived to become #491 — piecemeal is the failure mode, so the remaining sites deserve their own review rather than being tacked on here. parse_ip_address is now the sanctioned way to do this conversion, so #540 has somewhere to route them.

…508)

- add `parse_ip_address()` to `pcapkit.corekit.fields.ipaddress`, which calls
  the existing `_reject_bool` before converting, and takes an optional
  `version` so a caller that pins the address family widens an `int` to the
  right one
- route the seven `_make_*` sites that convert a caller-supplied address
  *before* the schema is built through it: `MH._make_opt_bid`,
  `MH._make_opt_lmaa`, `MH._make_fid_suboption`, `MH._make_opt_dmnp`,
  `MH._make_opt_lma_up`, `HIP._make_param_locator_set` and
  `TCP._make_mptcp_addaddr`. Each derives its option length or family flag
  from the converted address, so a bare `ipaddress.ip_address(True)` became
  `0.0.0.1` and #500's field-level guard could no longer tell it from a real
  address
- leave `SwitchField` untouched: its `pre_process` delegates to the resolved
  field and is not reached on the pack path at all, so the guard #508
  proposes putting there would be dead code
- drop the now-unused `import ipaddress` from `tcp.py`, and document
  `parse_ip_address` on the ipaddress fields page

Adds tests over three files, including one that re-derives the ten
address-typed `SwitchField` declarations from `Schema.__fields__` so a new
one cannot be added unnoticed. CI-equivalent unit suite: 1056 passed, with
only the pre-existing `test_docstring_contract` failure that main already has.
@JarryShaw
JarryShaw force-pushed the fix/508-address-switch-bool-guard branch from 9aac76c to 6e2387b Compare September 20, 2026 06:26
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — independently confirmed the issue's own proposed mechanism is wrong: on main, a bool reaching an address-typed SwitchField via either .pack() or .pre_process() is already rejected with FieldValueError (empirically reproduced), so the real defect is the seven _make_* call sites that convert addresses themselves before the schema — independently re-derived at exactly 10 address-typed switches and exactly 7 corrupting call sites, matching the PR's counts precisely.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Detailed review (independent verification, falsify-not-bless)

Head sha reviewed: 6e2387ba896f4f7cf624031793ff0ce035e89699 (rebased from the original 9aac76c7c3874cb12298c3398f66b53d32a28dff onto main at 691f12ab5 after #538 merged). Confirmed the rebase carries the identical patch content via git patch-id --stable on both diffs against their respective bases — both produce a56f3a87252e341cee0d0a79359a4d156f781772 — so everything below, derived against the pre-rebase sha, applies unchanged to this head, and I re-ran the targeted verification directly against 6e2387ba8 as well (see "Final confirmation" below).

The refuted mechanism — independently reproduced, not taken on the PR's word

The PR's central claim is that #508 misdiagnosed the defect: it says a SwitchField guard is unnecessary because SwitchField.pre_process already delegates to the resolved field's own pre_process, and SwitchField.pack calls self._field.pack(...) directly (bypassing SwitchField.pre_process, but not the resolved field's own guard, since FieldBase.pack calls self.pre_process internally). I read pcapkit/corekit/fields/misc.py:432-460 and confirmed this delegation chain by inspection, then verified it empirically on main (122d32795), before this PR, with no code changes:

sf = SwitchField(selector=lambda pkt: IPv4AddressField())
resolved = sf({})
resolved.pack(True, {})         # -> FieldValueError: invalid IP address: must not be a bool...
resolved.pre_process(True, {})  # -> FieldValueError: invalid IP address: must not be a bool...

Both raise on main, unmodified. This directly confirms the PR's refutation: a SwitchField guard proposed by #508 would be dead code, because #500's guard (inside _IPAddressField.pre_process) already fires on every address-typed switch. The real defect is upstream — _make_* methods that convert the address themselves before the schema ever sees it.

Re-derived count: 10 address-typed SwitchField attributes, not 7

I did not accept the PR's count from its table. I grepped SwitchField( across pcapkit/protocols/schema/ (25 hits total) and read the type annotation and any wrapping ConditionalField around each one by hand, rather than filtering by regex on the annotation (which is exactly what undercounts the three ConditionalField-wrapped sites). Independently arrived at exactly these 10:

hip.Locator.value, hopopt.SMFIdentificationBasedDPDOption.tid, ipv6_opts.SMFIdentificationBasedDPDOption.tid, mh.BindingIdentifierOption.address, mh.DelegatedMNPOption.prefix, mh.LMAAddressOption.address, mh.LMAUserPlaneAddressOption.address, mh.MNIDOption.identifier, mh.TargetCareofAddressSuboption.address, tcp.MPTCPAddAddress.address.

This matches the PR's table exactly, including the three it says a bare annotation-grep would miss (hopopt/ipv6_opts SMFIdentificationBasedDPDOption.tid and mh.BindingIdentifierOption.address, all three genuinely ConditionalField-wrapped — confirmed by reading pcapkit/protocols/schema/internet/{hopopt,ipv6_opts}.py:504-508 and mh.py:1293-1296 directly).

Re-derived count: 7 corrupting call sites, not 3

Read the actual diff rather than trusting the PR's list: pcapkit/protocols/internet/mh.py changes exactly 5 makers (_make_opt_bid, _make_opt_lmaa, _make_fid_suboption, _make_opt_dmnp, _make_opt_lma_up), hip.py changes 1 (_make_param_locator_set), tcp.py changes 1 (_make_mptcp_addaddr) — 7 total, matching. I also checked the one address-typed switch site the PR does not list as corrupting, mh.MNIDOption.identifier — its maker _make_opt_mn_id (mh.py:7769) already has an explicit, pre-existing bool guard from #481/#469 (Raises: ProtocolError: If identifier is a bool...), so its exclusion from the 7 is correct, not an oversight.

Exception type — confirmed correct, and confirmed why the PR's alternative would be wrong

parse_ip_address raises FieldValueError and calls _reject_bool as its first statement (ipaddress.py:196, verified by reading the source). I independently pulled up pcapkit/utilities/exceptions.py: BoolError(BaseError, TypeError) — docstring literally "The argument(s) must be bool type" — confirming the PR's claim that BoolError would be semantically backwards here (it means "should be a bool", not "must not be one"). FieldValueError(BaseError, ValueError) is the class actually used, matching _IPAddressField.pre_process's exception for the identical value.

NonceIndicesOption.home — confirmed untouched, not merely claimed so

Checked MH._make_opt_ni (mh.py:7658): home: 'int' = 0, not address-typed, not in the diff at all. This maker is completely outside the PR's changes, so home=True still coerces to 1 exactly as before — the PR does not touch this path, confirmed by the diff rather than assumed from the description.

Falsification / fails-without proofs — both independently reproduced with exit codes read from a file

Proof 1 (mh.py half): reverted pcapkit/protocols/internet/mh.py to main, kept the new helper and all three test files. PYTHONSAFEPATH=1 <repo>/.venv/bin/python -m pytest tests/protocols/internet/test_mh_unit.py -q: exit code 1, 11 failed, 49 passed, 458 subtests passed, with exactly the 10 SUBFAILED(option=..., value=True|False) combinations across the 5 reverted makers plus 1 parent FAILED line — a clean, reproducible fails-without result for the largest half of the fix.

Proof 2 (the guard itself, not just the routing): on the full PR head, removed only the _reject_bool(value, description) call from inside parse_ip_address, leaving every call site routed through it. pytest tests/corekit/test_fields_ipaddress.py tests/corekit/test_fields_misc.py tests/protocols/internet/test_mh_unit.py: exit code 1, 21 failed, 80 passed, 563 subtests passed, and zero ^FAILED parent lines (grep -c "^FAILED" → 0; grep -c "SUBFAILED" → 21). This is a stark, independently-reproduced demonstration of the pytest-subtests hazard this whole review programme is built around: every parent test's summary line would read as passing while the guard is completely gone. Both edits were reverted afterward; git diff <head> --stat empty before moving on each time.

Coverage — independently measured, not copied from the PR

coverage run -m pytest (no pytest-cov) over the three test files: pcapkit/corekit/fields/ipaddress.py93%, missing lines 67, 294, 401, 439, 489, 534 — matches the PR's numbers exactly, and confirmed none of those misses fall inside parse_ip_address's own span (115-216).

EXPECTED_FAILURES — confirmed unchanged

Imported (not grepped) tests/protocols/test_option_roundtrip_unit.py::EXPECTED_FAILURES: 55 entries, same as the programme's known baseline. This PR neither fixes nor breaks an option round-trip.

KNOWN_DEFECTS rot check

Coordinator flagged that this PR touches mh.py and tcp.py, both of which have entries in KNOWN_DEFECTS (four vendor/mh/* entries are unrelated generated-code entries; the one live concern was whether #539 accidentally fixed any docstring defect it carries). Ran tests/test_docstring_contract.py on the (pre-rebase) PR head: the only rotted entry was the pre-existing, already-known pcapkit/vendor/ipx/packet.py::process one (fixed by #538, unrelated to this PR's files). No new rot from #539's own changes.

ARP/IPv6_Route/OSPF "found but not fixed" — spot-checked, not accepted on faith

Read pcapkit/protocols/link/arp.py:397-401 directly: ipaddress.IPv4Address(addr) / IPv6Address(addr) called bare, with no bool guard at all — confirms item 1 of the PR's "deliberately not fixed" list is a real, currently-unfixed defect and not a padding claim. Did not individually re-verify the ipv6_route.py and ospf.py items with the same depth, given time budget, but the ARP spot-check gives confidence the pattern-recognition behind the rest of that list is sound.

Final confirmation on the rebased head

Checked out 6e2387ba896f4f7cf624031793ff0ce035e89699 directly (not inferred from patch-id alone) and re-ran tests/test_docstring_contract.py tests/corekit/test_fields_ipaddress.py tests/corekit/test_fields_misc.py tests/protocols/internet/test_mh_unit.py: exit code 0, 87 passed, 597 subtests passed. The previously-inherited test_known_defects_are_still_defects failure is gone on this head, since it now sits on top of main post-#538-merge.

CI status

Not run. GitHub Actions is backed up (35 queued, 3 pending, 0 in-progress at last check) with no declared incident; verdict is on local evidence only, per standing instruction not to wait for CI.

What remains unverified

  • The ipv6_route.py and ospf.py items in the "deliberately NOT fixed" list were not independently re-derived with the same rigor as the ARP spot-check.
  • CI has not run on this head at all.

@JarryShaw
JarryShaw merged commit 9486d5f into main Sep 20, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the fix/508-address-switch-bool-guard branch September 20, 2026 14:48
JarryShaw added a commit that referenced this pull request Sep 20, 2026
- `TSOption.post_process` converted `ts_data` entries to addresses with a bare
  `ipaddress.ip_address`, which takes a `bool` as the `int` it subclasses, so
  `ts_data=[True, 5]` packed and reported `IPv4Address('0.0.0.1')` with no
  exception. It runs on the packing path too, and `IPv4.make` accepts a
  caller-built option schema, so this was reachable from the public API. All
  three conversions now go through `parse_ip_address`, the fifth site of the
  defect #481, #500, #539 and #540 fixed before it.
- `quick_start_data_selector` sized the nested Quick-Start suboption with a
  hardcoded `SchemaField(length=5)` -- the width of a Request's `ttl` and
  `nonce` alone. A well-formed 8-octet option decoded its nonce as 55 rather
  than 933982136 and left three octets to be read as a fabricated option, so
  the datagram failed with `ProtocolError`. The length now comes from
  `quick_start_option_length`, computed from the resolved suboption, and
  `QuickStartReportOption` gains the RFC 4782 section 3.1 `Not Used` octet it
  was missing, which had made it seven octets wide against the `length=8` both
  `_make_opt_qs` and `_read_opt_qs` use.
- `_make_opt_ts` passed `data=` where the schema field is `ts_data`, so every
  timestamp was dropped with an `UnknownFieldWarning` and the Timestamp option
  was unbuildable through `make`. The `TYPE_CHECKING` `__init__` stub that
  advertised `data` is corrected too.

Three new tests, each shown to fail without its fix; `ipv4-option/TS` deleted
from `EXPECTED_FAILURES` now that it round-trips. Full unit tier green, 1107
passed with 2666 subtests; both changed modules at 100% statement and branch
coverage.
JarryShaw added a commit that referenced this pull request Sep 20, 2026
- `TSOption.post_process` converted `ts_data` entries to addresses with a bare
  `ipaddress.ip_address`, which takes a `bool` as the `int` it subclasses, so
  `ts_data=[True, 5]` packed and reported `IPv4Address('0.0.0.1')` with no
  exception. It runs on the packing path too, and `IPv4.make` accepts a
  caller-built option schema, so this was reachable from the public API. All
  three conversions now go through `parse_ip_address`, the fifth site of the
  defect #481, #500, #539 and #540 fixed before it.
- `quick_start_data_selector` sized the nested Quick-Start suboption with a
  hardcoded `SchemaField(length=5)` -- the width of a Request's `ttl` and
  `nonce` alone. A well-formed 8-octet option decoded its nonce as 55 rather
  than 933982136 and left three octets to be read as a fabricated option, so
  the datagram failed with `ProtocolError`. The length now comes from
  `quick_start_option_length`, computed from the resolved suboption, and
  `QuickStartReportOption` gains the RFC 4782 section 3.1 `Not Used` octet it
  was missing, which had made it seven octets wide against the `length=8` both
  `_make_opt_qs` and `_read_opt_qs` use.
- `_make_opt_ts` passed `data=` where the schema field is `ts_data`, so every
  timestamp was dropped with an `UnknownFieldWarning` and the Timestamp option
  was unbuildable through `make`. The `TYPE_CHECKING` `__init__` stub that
  advertised `data` is corrected too.

Three new tests, each shown to fail without its fix; `ipv4-option/TS` deleted
from `EXPECTED_FAILURES` now that it round-trips. Full unit tier green, 1107
passed with 2666 subtests; both changed modules at 100% statement and branch
coverage.
JarryShaw added a commit that referenced this pull request Sep 21, 2026
…tes (#540)

Follow-up to #508/#539/#552: bool is an int subclass, so a bare
ipaddress.IPv4Address/IPv6Address/ip_address call in a _make_* helper
silently laundered True/False into 0.0.0.1/::1 instead of raising. Four
sites were deliberately left out of #539 because their files were owned
by other work at the time; all four are now routed through the existing
parse_ip_address helper, the same pattern #539 and #552 used.

- ARP._make_proto_resolve (pcapkit/protocols/link/arp.py): addr=True
  packed as 00000001 (IPv4) or ::1 (IPv6) with no exception.
- IPv6_Route._make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py):
  worse than a packed-address defect, since cmpr_i/cmpr_e are derived from
  the laundered value -- ip=[True] packed with cmpr_e=0 and an address of
  00000001 instead of raising.
- IPv6_Route.make's dst parameter: the most reachable of the four, on the
  public make() entry point; dst=True converted to ::1 silently.
- OSPF._make_id_numbers: latent, no production caller today, fixed anyway
  so it does not resurface the defect the moment one is added.

ARP and OSPF use self.__class__.__name__ rather than self.alias in the
FieldValueError message, because their alias properties read state
(_acnm, _version) that read() only assigns -- unavailable to a
construction-only instance that never went through read().

Beyond bool rejection, pinning version=6 on the two IPv6_Route sites is a
second, smaller behaviour change: both previously converted a plain
integer through the bare, family-inferring ipaddress.ip_address, so
ip=[258] packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only
header; it now packs the 16-octet IPv6 form (::102) instead, which is
what an IPv6-only header should hold regardless of what an int happens
to fit as an IPv4 address.

Overlaps PR #561 (open, unmerged) in ipv6_route.py, which touches
_read_data_type_rpl; this change stays inside make()/_make_data_type_rpl
so the conflict on merge should be trivial.

Four new tests, each shown to fail without its fix (FieldValueError not
raised). Full unit tier green: 1120 passed, 8 skipped, 2673 subtests
passed. CHANGELOG.md regenerated via util/changelog_md.py from
docs/source/changelog/1.5.0.rst.
JarryShaw added a commit that referenced this pull request Sep 21, 2026
…tes (#540) (#568)

Follow-up to #508/#539/#552: bool is an int subclass, so a bare
ipaddress.IPv4Address/IPv6Address/ip_address call in a _make_* helper
silently laundered True/False into 0.0.0.1/::1 instead of raising. Four
sites were deliberately left out of #539 because their files were owned
by other work at the time; all four are now routed through the existing
parse_ip_address helper, the same pattern #539 and #552 used.

- ARP._make_proto_resolve (pcapkit/protocols/link/arp.py): addr=True
  packed as 00000001 (IPv4) or ::1 (IPv6) with no exception.
- IPv6_Route._make_data_type_rpl (pcapkit/protocols/internet/ipv6_route.py):
  worse than a packed-address defect, since cmpr_i/cmpr_e are derived from
  the laundered value -- ip=[True] packed with cmpr_e=0 and an address of
  00000001 instead of raising.
- IPv6_Route.make's dst parameter: the most reachable of the four, on the
  public make() entry point; dst=True converted to ::1 silently.
- OSPF._make_id_numbers: latent, no production caller today, fixed anyway
  so it does not resurface the defect the moment one is added.

ARP and OSPF use self.__class__.__name__ rather than self.alias in the
FieldValueError message, because their alias properties read state
(_acnm, _version) that read() only assigns -- unavailable to a
construction-only instance that never went through read().

Beyond bool rejection, pinning version=6 on the two IPv6_Route sites is a
second, smaller behaviour change: both previously converted a plain
integer through the bare, family-inferring ipaddress.ip_address, so
ip=[258] packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only
header; it now packs the 16-octet IPv6 form (::102) instead, which is
what an IPv6-only header should hold regardless of what an int happens
to fit as an IPv4 address.

Overlaps PR #561 (open, unmerged) in ipv6_route.py, which touches
_read_data_type_rpl; this change stays inside make()/_make_data_type_rpl
so the conflict on merge should be trivial.

Four new tests, each shown to fail without its fix (FieldValueError not
raised). Full unit tier green: 1120 passed, 8 skipped, 2673 subtests
passed. CHANGELOG.md regenerated via util/changelog_md.py from
docs/source/changelog/1.5.0.rst.
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Address-typed SwitchField attributes bypass #491's bool guard: 3 mh.py makers still corrupt

1 participant