Skip to content

fix(link,internet): reject a bool at the four remaining maker call sites (#540) - #568

Merged
JarryShaw merged 2 commits into
mainfrom
fix/540-bool-laundering-four-sites
Sep 21, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/540-bool-laundering-four-sites

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #540. Follow-up to #508/#539/#552: bool is an int subclass, so a bare ipaddress.IPv4Address/IPv6Address/ip_address call inside a _make_* helper silently laundered True/False into an address (0.0.0.1, ::1) instead of raising. #539 fixed seven such sites but deliberately left out four whose files were owned by other work at the time; this PR fixes those four, all through the existing parse_ip_address helper (pcapkit/corekit/fields/ipaddress.py), the same pattern #539 and #552 used -- fixing the pattern in one pass rather than one site at a time, per the issue's own warning about how #481's single-site fix survived to become #491 and then #508.

All four sites confirmed against origin/main at 8cfd6ab01 before fixing:

  • ARP._make_proto_resolve -- pcapkit/protocols/link/arp.py:399,401 (post-fix). Before: _make_proto_resolve(True, IPv4) packed 00000001 (0.0.0.1), _make_proto_resolve(True, IPv6) packed ::1 -- no exception either way.
  • IPv6_Route._make_data_type_rpl -- pcapkit/protocols/internet/ipv6_route.py, five call sites (originally lines 737/745/753/764/768). Worse than a plain packed-address defect: cmpr_i/cmpr_e are derived from the laundered value, so a bool corrupts the compression metadata alongside the address list. Before: _make_data_type_rpl(ip=[True]) -> cmpr_i=0 cmpr_e=0 addresses=['00000001']; _make_data_type_rpl(dst=2001:db8::1, ip=[True]) -> cmpr_i=16 cmpr_e=0 addresses=['00000001'].
  • IPv6_Route.make's dst parameter -- originally line 314, the most reachable of the four since it's on the public make() entry point. Before: dst=True converted to ::1 with no exception.
  • OSPF._make_id_numbers -- originally line 328 (now 329 post-fix). Latent: nothing in the codebase calls it except a unit test (tests/protocols/link/test_link_unit.py:856 pre-fix). Fixed anyway so it doesn't resurface the defect the moment a future caller reaches it -- the same kind of omission is how mh: reject a wrong-type MN-ID identifier in-library, not as a stdlib leak #481's fix survived to become IP address fields silently accept bool and corrupt the packet — unfixed sibling of #469/#481 at the shared root #491 and then Address-typed SwitchField attributes bypass #491's bool guard: 3 mh.py makers still corrupt #508.

A second, smaller behaviour change beyond bool rejection

Passing version=6 to parse_ip_address on the two IPv6_Route sites (_make_data_type_rpl and make's dst) does more than reject a bool -- it pins the address family for a plain integer too. Both previously converted through the bare, family-inferring ipaddress.ip_address, under which ip=[258] packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only header. Pinned to version=6, the same input now packs the 16-octet IPv6 form (::102) instead:

ipaddress.ip_address(258)   ->  0.0.1.2   (an IPv4 address)
ipaddress.IPv6Address(258)  ->  ::102

This is a deliberate improvement -- an IPv6-only header should not be able to hold a v4 address -- but it is a real change in packed output for non-bool integer input, called out here since it is not implied by "rejects a bool". ARP._make_proto_resolve and OSPF._make_id_numbers are unaffected: ARP already dispatched to the explicit IPv4Address/IPv6Address constructor per ptype before this fix, and OSPF's small-integer router/area IDs already resolved to IPv4 under the old family-inferring call, so pinning version=4 there reproduces the prior behaviour for every value that reached it (it is latent, so nothing did).

Why self.__class__.__name__ instead of self.alias in ARP/OSPF

IPv6_Route.alias is a static string ('IPv6-Route'), so it's safe to use in the FieldValueError message exactly like the existing ProtocolError raises elsewhere in that file. ARP.alias and OSPF.alias, however, read self._acnm/self._version, which read() only assigns -- unavailable on a construction-only instance that never went through read() (confirmed: object.__new__(ARP).make(...) and a full ARP(spa=..., tpa=...) construction both call make()/pack() before unpack()/read() ever runs). Using self.alias there would turn every call into an AttributeError, bool or not, and would have broken the pre-existing test test_arp_make_builds_schema_with_resolved_addresses. Used self.__class__.__name__ instead, which needs no instance state.

Overlap with #561

#561 is open and unmerged, and also touches pcapkit/protocols/internet/ipv6_route.py (in _read_data_type_rpl, the read-path length guard) and adds tests to the same file this PR does, tests/protocols/internet/test_ipv6_extension_unit.py. This PR's edits to ipv6_route.py stay entirely inside make() and _make_data_type_rpl() (the construction path), a different method from _read_data_type_rpl. Verified rather than asserted: git merge-tree --write-tree <this-head> <561-head> exits 0 with a written tree and no conflict markers, so the two merge cleanly regardless of which lands first -- no manual resolution needed either way.

Coverage

Four new tests, each proven to fail without its fix. Reverted the three production files only, using a uniquely-tagged git stash push -u -m "<tag>" -- <the three files>, captured the stash entry's SHA immediately, restored with git stash apply <sha> (never pop), and dropped the entry once restored -- the shared stash stack this repo's other work may also be using was never touched by a bare stash/pop. Confirmed AssertionError: FieldValueError not raised at every site before restoring:

  • tests/protocols/link/test_link_unit.py::test_arp_proto_resolve_rejects_a_bool
  • tests/protocols/link/test_link_unit.py::test_ospf_id_numbers_rejects_a_bool
  • tests/protocols/internet/test_ipv6_extension_unit.py::test_ipv6_route_make_dst_rejects_a_bool
  • tests/protocols/internet/test_ipv6_extension_unit.py::test_ipv6_route_rpl_source_addresses_reject_a_bool (covers all three _make_data_type_rpl shapes: dst=None, dst given with the bool as the sole/cmpr_e address, and dst given with the bool as a cmpr_i-prefix address)

tests/protocols/test_option_roundtrip_unit.py's EXPECTED_FAILURES is unaffected -- the one IPv6-Route entry there (ipv6-route-type/RPL_Source_Route_Header) names a schema-layer defect in RPL.post_process (addressed by #561, not this PR's protocol-layer sites); confirmed unchanged by importing EXPECTED_FAILURES directly and by the roundtrip test still passing all 322 subtests.

mypy --ignore-missing-imports on the three changed production files: 7 pre-existing errors, same 7 after (one relocated from a bare ipaddress.ip_address call to parse_ip_address, itself pre-existing on main for the same bytearray-vs-stub mismatch -- not a regression, not touched, out of this PR's narrowed scope).

Test plan

  • pytest -q tests/protocols/link/test_link_unit.py tests/protocols/internet/test_ipv6_extension_unit.py -- 81 passed, 99 subtests passed
  • Each new test shown to fail (not just pass) without its fix
  • pytest -q tests/protocols/test_option_roundtrip_unit.py -k round_trip_is_identity -- 322/322 subtests, unchanged
  • pytest -q tests/project/test_changelog_md.py and python util/changelog_md.py --check -- CHANGELOG.md in step with docs/source/changelog/1.5.0.rst
  • CI's reduced unit-tier selection, pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py' -- 1120 passed, 8 skipped, 2673 subtests passed
  • Full local test run (all tiers), pytest tests -- 1264 passed, 17 skipped, 2855 subtests passed, exit 0 (samples regenerated first via python examples/generators/make_samples.py, since a fresh worktree's *_runtime.py/regression fixtures are not committed)
  • mypy --ignore-missing-imports on the three changed modules -- no new errors vs. main

@JarryShaw

Copy link
Copy Markdown
Owner Author

❌ NEEDS CHANGES

Cross-model review (Opus 5; the PR was authored on Sonnet), head fb3b8ec95. One commit on current main 8cfd6ab01.

The fix itself is correct, complete, and correctly scoped. I verified the scope personally: 7 files, +238/−11, touching arp.py, ospf.py and ipv6_route.py plus the two test files and both changelog files — and git diff 8cfd6ab01...HEAD -- esp.py hip.py schema/internet/ipv4.py is empty, so all four struck sites are untouched, exactly as triage required. All four in-scope sites route through the existing parse_ip_address rather than growing bespoke guards, which is the #539 pattern the issue insisted on and the thing that stops this becoming a sixth instance.

One change needed, plus one process matter that I want on the record.

1. An undocumented behaviour change beyond the bool fix. The four sites now pass version= to parse_ip_address, which is more than a bool guard — it changes what a plain integer produces. Measured, stdlib only:

ipaddress.ip_address(258)   ->  0.0.1.2   (an IPv4 address)
ipaddress.IPv6Address(258)  ->  ::102

So inside the IPv6-only RPL header, ip=[258] previously packed an IPv4 address and now packs ::102. That is a genuine improvement — an IPv6-only header should not be able to hold a v4 address — but it changes packed output for non-bool input, and neither the body nor the changelog entry mentions it. The entry covers the bool laundering and the cmpr_i/cmpr_e corruption and stops there. One sentence on the version= pinning closes it; the mechanics are already fine (util/changelog_md.py --check exits 0, so the generated CHANGELOG.md follows once the .rst is edited).

2. The fails-without proof was obtained with git stash, which is not safe here. The body states it "temporarily reverted the three production files via a scoped git stash". The stash stack is shared across every worktree on this machine and several agents hold uncommitted work — "scoped" is not a property git stash has, and a stray pop destroys someone else's tree. git stash list is empty now so no damage is visible, and the result is sound (I have no doubt the reverts happened), but please use a byte-identical file copy plus an md5sum/git status restoration proof instead. This is about method, not about the finding.

Two smaller notes. The body labels pytest -q --ignore=tests/integration --ignore-glob='*_runtime.py' --ignore-glob='*_regression.py' (1120 passed, 8 skipped) the "Full CI unit tier" — it is a reduced selection, and the flags are stated so it is transparent rather than misleading, but the label overstates it. Plain pytest tests is clean too: 1264 passed, 17 skipped, 2855 subtests, exit 0 — which is self-consistent against the main baseline I measured independently elsewhere in this batch (1260 passed / 2850 subtests, plus this PR's 4 new tests and 5 new subtests). And the body repeats the stale reference schema/internet/ipv6_route.py:156 for the RPL EXPECTED_FAILURES entry; that line was already wrong on main (the real cast is at 208, post_process at 198) and #561 has since corrected it, so this is only a quotation to drop.

On the #561 overlap: it is better than you claim. The body says the conflict "should be trivial to resolve". I ran the trial merge myself — git merge-tree --write-tree fb3b8ec95 d42940414 exits 0 with tree cb436e07fa16e846109acf9a7a24b6f01f91fb4e and no conflict markers at all. Worth correcting, since "trivial to resolve" invites a manual merge nobody needs to do. Note both PRs also add tests to the same file (tests/protocols/internet/test_ipv6_extension_unit.py), which the body does not mention — clean regardless.

Detail, including a weak assertion worth strengthening, follows.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Detailed cross-review — #568 @ fb3b8ec95

Reviewer: Opus 5, per the standing rule that an agent-raised PR gets a cross-review from a different model than the one that wrote it. This PR arrived unreported, so I treated nothing as established.

Provenance of the evidence below, stated plainly. I re-derived the scope, the struck-site exclusions, the #561 trial merge, the version= behaviour change, and the body's own claims about its method personally, with the commands quoted. The per-site revert runs, the bool-rejection sweep across all four sites, and the tier/mypy numbers came from a parallel Opus 5 run I dispatched; I cross-checked those against figures I had measured independently (see §5) rather than accepting them, and I flag below which is which. Where a number is not mine I say so.


1. Scope — verified personally

$ git diff --stat 8cfd6ab01...fb3b8ec95
 CHANGELOG.md                                       |  1 +
 docs/source/changelog/1.5.0.rst                    | 11 +++
 pcapkit/protocols/internet/ipv6_route.py           | 38 ++++++++--
 pcapkit/protocols/link/arp.py                      | 31 ++++++++-
 pcapkit/protocols/link/ospf.py                     | 24 ++++++-
 tests/protocols/internet/test_ipv6_extension_unit.py | 80 +++++++++++++++
 tests/protocols/link/test_link_unit.py             | 64 +++++++++++++
 7 files changed, 238 insertions(+), 11 deletions(-)

$ git diff --stat 8cfd6ab01...fb3b8ec95 -- \
      pcapkit/protocols/internet/esp.py \
      pcapkit/protocols/internet/hip.py \
      pcapkit/protocols/schema/internet/ipv4.py
(empty)

Three production files for four sites — ipv6_route.py carries two (make's dst and _make_data_type_rpl). All four struck sites (esp.py:575, hip.py:3072, hip.py:3077, schema/internet/ipv4.py:308) are untouched. Correct on both counts.

The parallel run additionally established that main's ipv6_route.py had exactly six ipaddress. code uses (lines 314, 737, 745, 753, 764, 768) and that all six are converted, with import ipaddress dropped there because nothing else needs it, and correctly retained in arp.py/ospf.py where read-path uses survive. I did not re-enumerate those six myself.

2. It follows #539's pattern rather than inventing guards

All four sites call parse_ip_address, the helper #539 established, with the same idiom — helper call plus a # NOTE: naming the bytes previously packed and citing #508. No per-site isinstance(..., bool) checks were added. This is the specific thing the issue warned against, given this is the fifth instance after #481, #500, #539 (seven sites) and #552 (one), and it is honoured.

3. The version= behaviour change — the blocking item

The sites pass version= in addition to routing through the helper. The helper's docstring sanctions that ("Pass it where the wire format fixes the family"), so it is not a misuse. But it is not only a bool guard, and that is unreported. Measured with stdlib alone, no pcapkit involved:

>>> ipaddress.ip_address(258)    # what the bare call did
IPv4Address('0.0.1.2')
>>> ipaddress.IPv6Address(258)   # what version=6 does now
IPv6Address('::102')

_make_data_type_rpl builds an RPL Source Route Header, which is IPv6-only. So on main, ip=[258] packed four octets of IPv4 into an IPv6 address slot; now it packs ::102. This is the right behaviour — the point is only that a user's packed output changes for a perfectly ordinary non-bool input, and the changelog entry does not say so. It currently covers the bool laundering and the cmpr_i/cmpr_e metadata corruption and stops.

4. Bool rejection and the main laundering

From the parallel run, not measured by me: all four sites now raise FieldValueError for both True and False; on main, three of the four laundered silently — ARP IPv4 Trueb'\x00\x00\x00\x01', ARP IPv6 True::1, OSPF Trueb'\x00\x00\x00\x01', and RPL ip=[True]addresses=[b'\x00\x00\x00\x01'] (a four-octet address in an IPv6-only header) with cmpr_i=16, cmpr_e=0, pad_len=8 once dst is set — which is the compression-metadata corruption the changelog claims, so that claim checks out.

One nuance I want to highlight because it connects to work I did verify: through the RPL path, main raises ValueError: [b'…'] does not appear to be an IPv4 or IPv6 address — and that is #561's defect masking the bool, the exact ValueError I reproduced myself while reviewing #561. The PR's test sidesteps it by omitting type= so the default Source Route path is taken, and its own comment explains that. Sound, and the body's "FieldValueError not raised at every site" therefore holds.

Per-site fails-without, from the parallel run (rc values read from files): ARP rc 1 (2 SUBFAILED, both ptypes); OSPF rc 1 (1 FAILED); IPv6-Route rc 1 (4 failures, all AssertionError: FieldValueError not raised). Restoration was proven by md5sum and an empty git status. Sites 3 and 4 share a file, so that file was reverted whole and the sites isolated by test — each test targets one site, so isolation is per-site in effect rather than per-hunk. Worth knowing; not a defect.

5. Tier numbers — cross-checked for consistency

The parallel run reports plain pytest tests at 1264 passed, 17 skipped, 2855 subtests, exit 0 (fixtures generated first). I did not re-run it, but it is consistent with baselines I did measure myself in this batch: main is 1260 passed / 2850 subtests (I measured 1261/2850 on #562's branch, which is main plus its own one new test, and 1279/2850 on #570's branch, which is main plus 19). 1260 + this PR's 4 new tests = 1264, and 2850 + 5 new subtests = 2855. Both line up, which is meaningful corroboration rather than mere agreement.

Also from that run: targeted tests on the two changed test files rc 0, 81 passed / 99 subtests (matching the body); the roundtrip table -k round_trip_is_identity rc 0, 322 subtests — matching both the body and the main baseline of 322 that I measured myself while reviewing #561, which independently confirms EXPECTED_FAILURES is genuinely unaffected; and mypy at 7 errors before and 7 after on the three files, the same set with one relocating from ospf.py:329 to ospf.py:351.

6. The git stash method

Verified from the body personally — it reads:

Four new tests, each proven to fail without its fix (temporarily reverted the three production files via a scoped git stash, re-ran the new tests, confirmed AssertionError: FieldValueError not raised at every site, then restored)

git stash is unsafe in this environment: the stack is shared across every worktree and other agents hold uncommitted work, so any pop can restore or destroy the wrong tree. There is no "scoped" stash. Nothing appears to have gone wrong — the stack is empty now — and I am not disputing the result. Use a file copy plus md5sum and git status/git diff HEAD restoration proof instead; that is what I did for every revert in this batch and it is auditable in a way a stash is not.

7. The #561 overlap is cleaner than claimed — verified personally

$ git merge-tree --write-tree fb3b8ec95 d42940414
cb436e07fa16e846109acf9a7a24b6f01f91fb4e
exit: 0        conflict markers: 0

No conflict at all, not merely a trivial one. The parallel run went further and reported that the merged tree has no import ipaddress and no remaining ipaddress. code use in that file (only comment text), so there is no NameError risk from the dropped import, that both PRs' tests survive, and that the merged tree runs both test sets plus the roundtrip table at rc 0 / 88 passed / 457 subtests. I independently reproduced the tree sha, which is the load-bearing part; the merged-tree test run is theirs.

8. The latent OSPF._make_id_numbers site

Justified, and the justification holds. From the parallel run: the method has no production caller (only test_link_unit.py:892 pre-existing, plus the two new tests), and OSPF.make passes router_id/area_id straight to the schema at ospf.py:252-253. Chasing the public path, OSPF.make(router_id=True) returns a schema holding raw True with no exception, but bytes(schema) raises FieldValueError: invalid IP address: must not be a bool#500's field-level guard. So this is defence-in-depth rather than a live hole, which is the right reason to fix a latent site while you are in the file. Minor: the public path only fires at pack time, and its message lacks the protocol context the new site-level message carries.

The self.__class__.__name__-instead-of-self.alias choice is also justified and was checked: object.__new__(ARP).alias raises AttributeError: 'ARP' object has no attribute '_acnm' and OSPF's raises on '_version', so alias genuinely is not available on a bare instance; IPv6_Route correctly keeps self.alias.

9. A weak assertion worth strengthening

test_ipv6_route_rpl_source_addresses_reject_a_bool's "still works" half asserts assertGreaterEqual(rpl.cmpr_i, 0) and assertGreaterEqual(rpl.cmpr_e, 0). Those are near-vacuous for values that cannot be negative — they assert only "no exception was raised". The ARP and OSPF tests assert exact packed bytes, and the changelog specifically calls out cmpr_i/cmpr_e corruption as the harm, so pinning the actual expected values would test the claim the changelog makes. Non-blocking.

10. Changelog

util/changelog_md.py --check → exit 0, in step. Entries present in both files, naming all four sites and the RPL metadata corruption. The gap is §3 only.

Could not verify

  • CI. Reached on local evidence only, per my brief; no claim about its tally.
  • Any interpreter other than CPython 3.14.7.
  • That the four struck sites are genuinely out of scope (read-path only, or already covered by fix: reject a bool IP address in-library instead of silently corrupting the packet #500's field guard). I confirmed only that fix(link,internet): reject a bool at the four remaining maker call sites (#540) #568 does not touch them, which is what triage asked.
  • The third RPL shape's pre-fix numbers (the cmpr_i-prefix case) by independent probe — covered only through the PR's own test.
  • The body's claim that full ARP(spa=…, tpa=…) construction calls make()/pack() before unpack()/read(). The consequences were verified; the call order itself was not.
  • Anything in §4, §5 (except the consistency arithmetic and the 322 roundtrip baseline), §7's merged-tree run, and §8 was measured by a delegated Opus 5 run rather than by me directly. I cross-checked what could be cross-checked and say so above rather than presenting it as first-hand.

…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
JarryShaw force-pushed the fix/540-bool-laundering-four-sites branch from fb3b8ec to 6d2ceed Compare September 21, 2026 05:33
@JarryShaw

Copy link
Copy Markdown
Owner Author

Correction from me on the git stash point

I need to walk back part of item 2 in my verdict above. I wrote that git stash "is not safe here" in a way that reads as though this PR had broken a house rule. It had not, and I was wrong to imply it.

The rule in force permits git stash push -u -m "<unique-tag>" with the entry's sha captured immediately and restored via git stash apply <sha> — never a bare git stash and never pop. That tagged, apply-by-sha form is what this PR actually used, so it followed the rule rather than violating it. My comment conflated "a stash was involved" with "the dangerous form of stash was involved", which is precisely the distinction the rule exists to draw.

What stands, as a preference rather than a finding: a byte-identical file copy with an md5sum and a git status/git diff HEAD restoration proof is still what I would reach for, because it leaves an auditable trail a reviewer can re-check afterwards and it cannot interact with a shared stack at all. That is the method I used for every revert in this batch. But it is better practice, not a correction to this PR, and it will not come back from me as a requested change.

Everything else in my verdict is unchanged. The one blocking item remains the undocumented version= behaviour change from §3 — that ipaddress.ip_address(258) yielded 0.0.1.2 where version=6 now yields ::102, so packed output changes for ordinary non-bool integer input and the changelog does not say so. The three body corrections (the "Full CI unit tier" label, the stale schema/internet/ipv6_route.py:156 quotation, and "trivial to resolve" where the trial merge is in fact conflict-free at tree cb436e07f) also stand as written.

Apologies for the noise; a reviewer telling a worker it violated a rule it had followed is worse than saying nothing, so it seemed better to correct it on the record than to leave it.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE

Re-review after the force-push, head 6d2ceed0b (previously reviewed at fb3b8ec95). Cross-model: Opus 5; the PR was authored on Sonnet.

My blocking item is addressed, and addressed accurately. The delta is the two changelog files only — git diff fb3b8ec95 6d2ceed0b -- pcapkit/ tests/ is empty — so everything I verified about the fix at the previous head carries over unchanged.

The new sentences say exactly what needed saying, with the real numbers:

Pinning version=6 on the two IPv6_Route sites is a second, smaller behaviour change beyond bool rejection: both previously converted a plain integer through the bare, family-inferring ipaddress.ip_address, which let ip=[258] pack a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only header; it now packs the 16-octet IPv6 form (::102) instead

And I checked the scoping rather than taking it, because "the two IPv6_Route sites" is a narrower claim than "the four sites got version=" — which they did. It holds:

  • ipv6_route.pymain used the family-inferring ipaddress.ip_address(...) at all six converted sites (314, 737, 745, 753, 764, 768), each cast to IPv6Address, which for an integer argument was simply untrue. So the behaviour genuinely changes here, exactly as the entry describes. ✓
  • arp.pymain already used explicit ipaddress.IPv4Address(addr) / ipaddress.IPv6Address(addr), selected by ptype (lines 400 and 402). Pinning version=4/version=6 there is behaviour-preserving, so correctly omitted. ✓
  • ospf.pymain used the inferring form (ipaddress.ip_address(id).packed, line 329) and this pins version=4. For an integer the result is unchanged (ip_address(258) and version=4 both give 0.0.1.2). There is a narrow change — an IPv6-shaped string, which previously packed 16 octets into an IPv4-only 4-octet field and now raises — but that site has no production caller, so nothing user-visible turns on it. Omitting it from a user-facing changelog is the right editorial call, not a gap.

So the entry documents the change that a user can actually observe and stays quiet about the two that they cannot. That is better than enumerating all four would have been.

util/changelog_md.py --check → exit 0, in step, and the CHANGELOG.md delta is confined to the one entry line carrying the same prose in the generator's markdown rendering — regenerated rather than hand-matched, which mattered given how many changelog entries were in flight.

The three body corrections from my first verdict are recommendations rather than blockers and I am not holding the PR for them: the "Full CI unit tier" label on what is a reduced selection, the stale schema/internet/ipv6_route.py:156 quotation (#561 has since corrected that entry), and "trivial to resolve" for a #561 overlap that is in fact conflict-free — git merge-tree --write-tree fb3b8ec95 d42940414 exits 0 at tree cb436e07f with no markers, which I reproduced myself. Worth tidying whenever the description is next touched.

And my git stash item is withdrawn — see my correction above; the tagged, apply-by-sha form this PR used is permitted, and I was wrong to imply otherwise.

The one remaining suggestion, non-blocking and for whenever that file is next open: test_ipv6_route_rpl_source_addresses_reject_a_bool's "still works" half asserts assertGreaterEqual(rpl.cmpr_i, 0) / assertGreaterEqual(rpl.cmpr_e, 0), which cannot fail for values that are never negative — it asserts only "no exception". Since the changelog names cmpr_i/cmpr_e corruption as the harm, pinning their expected values would test that claim the way the ARP and OSPF tests pin exact bytes.

Nothing further from me.

@JarryShaw
JarryShaw merged commit bdead72 into main Sep 21, 2026
23 checks passed
@JarryShaw
JarryShaw deleted the fix/540-bool-laundering-four-sites branch September 21, 2026 15:24
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IPv4/IPv6 make paths still launder a bool into an address in four more places (follow-up to #508)

1 participant