Skip to content

const(apptype): per-registry __canonical__ divergence makes get(port, proto=<multi-bit>) return a different service #759

Description

@JarryShaw

Describe the bug

__canonical__ is per-registry and diverges between registries on 7 ports, so AppType.get(port, proto=<multi-bit>) can return a different service than the member it was asked to round-trip. It is silent, and == cannot detect it.

Reproduction

On 932cb48d1, sweeping all 10,625 multi-transport members in one process — zero mints, zero exceptions, and exactly one service mismatch:

m = <UDP.accessbuilder: 888 [tcp|udp]>       UDP.get(888).svc = 'accessbuilder'
AppType.get(888, proto=m.proto) -> <TCP.cddbp: 888 [tcp]>
  result is m: False        result == m: True        result.svc == m.svc: False
  result.proto = 1 (tcp)  vs  m.proto = 3 (tcp|udp)

__canonical__ diverges on ports 113, 465, 512, 631, 750, 888, 999; 888 is the only one where the divergent member is multi-transport and is its own registry's answer.

Expected behavior

get(m.port, proto=m.proto) should return m's service, or refuse. It currently returns a different service and claims a narrower transport, and == reports True because AppType.__eq__ compares self.port == other — so a caller checking equality sees no problem.

System information

  • OS Version: Linux 5.10 (Amazon Linux 2 int)
  • Python Version: 3.14.7
  • Python Implementation: CPython
  • pcapkit Version: checkout at 932cb48d1

Additional context

Found while verifying #736's acceptance criterion after #754 (#736 is otherwise closed — 10,624 of 10,625 round-trip correctly). Distinct mechanism from #736's displacement.

This is the case #732 already predicted. Its recorded note says the fix for multi-transport lookup is to make _dispatch refuse a multi-bit proto rather than silently pick LSB-first — that would turn this from a wrong answer into an error. Worth deciding alongside #732's deferred TransportProtocol retype rather than patching __canonical__ per port.

Activity

  1. added
    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)
    constRegenerated IANA or vendor constant tables; members keep their numeric values
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    on Sep 24, 2026
  2. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Blocked on #769, which is open and regenerates all five files under pcapkit/const/reg/apptype/ plus the pcapkit/vendor/reg/apptype/apptype.py crawler. This issue's fix lives in the generated apptype.py, so working it now would collide with that regeneration and be overwritten by it.

    Unblocks when #769 merges. Re-measure before implementing: #769 changes the member-emitting template and the four range-row conditions #764 added, so the __canonical__ divergence may present differently — or already be resolved — on the regenerated tree.

  3. removed
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    on Sep 25, 2026
  4. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Unblocked — #769 merged as 83b58ebda, so pcapkit/const/reg/apptype/ and the crawler are free.

    Re-measure before implementing rather than working from this issue as filed. #769 regenerated all five files: the per-member annotation is gone and every generator-emitted TransportProtocol.get(...) is now attribute access, including the four range-row conditions #764 added. So the __canonical__ divergence may present differently, or already be resolved, on the regenerated tree. The fix was also expected to be #732's "_dispatch refuses a multi-bit proto", which is still deferred — check whether that is still the right shape before writing anything.

  5. added
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 25, 2026
  6. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Re-measured on 83b58ebda, and this issue is ~46× larger than its body claims — but not a regression. I ran the sweep independently of the investigating agent; both agree on the count.

    multi-transport members swept : 10625
    exceptions                    : 0
    mints (member delta)          : 0
    service mismatches            : 46
    distinct ports                : 20
    ports: 42, 63, 80, 105, 351, 352, 666, 888, 999, 1525, 1701, 1989, 1992, 2049,
           3000, 3002, 3478, 4444, 5349, 9100
    

    46, not "exactly one at port 888." The body's number comes from a narrower lens than its own text describes: the 7 ports where __canonical__ disagrees between registries, filtered further to members that are multi-transport and their own registry's answer — true only at 888. A literal full sweep, which is what the body says it did, finds 46 and always has.

    Structure: 20 distinct ports, three of which carry two service names each (3478 → turn/stun-behavior, 5349 → turns/stun-behaviors, and one more), giving 23 (port, name) pairs, each contributing one TCP-declared and one UDP-declared copy: 17×2 + 3×4 = 46. Every mismatch carries proto=tcp|udp; none involves sctp or dccp. AppType.__eq__ compares on .port alone, so result == m reads True in all 46 — the comparison cannot detect any of them.

    Not caused by #764 or #769. The identical sweep at 932cb48d1 — this issue's own cited commit — gives the same 46 / 0 / 0, and tcp.py, udp.py, sctp.py, dccp.py are byte-identical across 932cb48d1..57b2c1761. #764 changed _missing_'s out-of-range propagation and per-transport range-row claiming, neither of which is reached here since no mismatch involves a mint; #769 changed only the annotation and the TransportProtocol reference form.

    Mechanism, confirmed by reading rather than inferred. AppType._dispatch (pcapkit/const/reg/apptype/apptype.py:2332-2367) resolves a multi-bit proto via show_flag_values(proto), which iterates LSB-first (pcapkit/utilities/compat.py:184-197, _iter_bits_lsb). tcp = 1 is the lowest bit, so any proto containing it dispatches into TCP's registry regardless of which registry the member being round-tripped belongs to. get()'s __canonical__ lookup then returns that registry's single answer for the port.

    The fix is narrower and safer than expected. There is exactly one production call site of AppType.get(port, proto=…) outside the const module — pcapkit/protocols/transport/transport.py:187, _make_port — and it always passes the concrete single-bit transport of the packet being parsed, never a member's own possibly-multi-bit .proto. So making _dispatch raise on a multi-bit proto breaks no caller, converts 46 silent wrong answers into explicit errors, and stays the narrow half of #732 rather than the deferred Flag→Enum retype.

    Blocked on #777, which owns both apptype.py files. The fix belongs in the BASE template in pcapkit/vendor/reg/apptype/apptype.py followed by a regeneration, never a hand-edit of the generated file.

  7. added
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    and removed
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 25, 2026
  8. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Unblocked — #777 merged at 12:16:37Z as fe80b8525, so pcapkit/const/reg/apptype/apptype.py and pcapkit/vendor/reg/apptype/apptype.py are both free. Dispatching.

    Re-stating the fix so it is not re-derived: _dispatch must refuse a multi-bit proto rather than resolving it LSB-first. That is safe because exactly one production call site exists outside the const module — pcapkit/protocols/transport/transport.py:187 in _make_port — and it always passes a single bit. Re-measured at 83b58ebda: 46 mismatches at 20 ports, 0 exceptions, 0 mints across 10,625 members, identical to the count at 932cb48d1, so this is long-standing rather than a regression.

    #775 now waits on this issue, reversing the earlier order: #775 regenerates all 121 registries including these two files, so the targeted fix lands first and the regeneration picks it up.

  9. added
    wipWork in flight - a covering PR is open or an agent is actively on it
    and removed
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    on Sep 25, 2026
  10. 3 remaining items

  11. removed
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 25, 2026
  12. added this to the 1.5 milestone on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)constRegenerated IANA or vendor constant tables; members keep their numeric values

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions