Skip to content

AppType.get(port, proto=<combined flag>) mints an unknown member and displaces the real one #736

Closed

Description

@JarryShaw

AppType.get(port, proto=<combined flag>) misses the lookup, mints an unknown member, and permanently displaces the real member in the single-bit buckets. Reproduced deterministically in a clean process:

get(7, proto=tcp)         -> echo                  # correct
get(7, proto=tcp|udp)     -> unknown  (8182 -> 8183 members)
get(7, proto=tcp)         -> unknown                # the real member is gone

Cause

__members_proto__ (pcapkit/const/reg/apptype.py:81) is keyed on single TransportProtocol bits — tcp, udp, sctp, dccp, undefined. A combined flag is never a key, so the lookup misses, _missing_ mints an unknown member, and the registration path then files that member into each single-bit bucket the combined flag covers, overwriting whatever was there.

Impact

5,354 of 8,182 members (65%) are multi-transport, so the natural round-trip AppType.get(m.port, proto=m.proto) triggers this for most of the registry. The corruption is process-lifetime and silent.

Shipped parsing is not affected: every in-library call site passes a single bit — protocols/transport/tcp.py:615-616, udp.py:203-204, sctp.py:581-582. The hazard is the public API and anything that round-trips a member's own proto.

Notes

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
    on Sep 24, 2026
  2. JarryShaw commented on Sep 24, 2026

    @JarryShaw
    OwnerAuthor

    Deferred to #732 rather than patched — the owner's call, and it is the right one: the four-way split removes the combined-flag key space this defect lives in, so there is nothing left to guard.

    Marking blocked and adding it to #732's acceptance criteria alongside #717 and #734, so #732 cannot close without demonstrating this case is gone. Not closing it: the corruption is live in the public API today, and a closed issue is how it gets forgotten if #732 is descoped.

    Acceptance criterion for #732: in a clean process, get(7, proto=tcp|udp) must not displace the member get(7, proto=tcp) returns, and the round-trip get(m.port, proto=m.proto) must hold for a multi-transport member.

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

    @JarryShaw
    OwnerAuthor

    Closed by #754 (932cb48d1). Both halves of the acceptance criterion re-measured there, one fresh process each.

    Displacement — gone. The recorded sequence returns the same object three times, no member-count change in any registry:

    get(7, proto=tcp)     -> <TCP.echo: 7 [tcp|udp]>
    get(7, proto=tcp|udp) -> <TCP.echo: 7 [tcp|udp]>   # is r1 -> True
    get(7, proto=tcp)     -> <TCP.echo: 7 [tcp|udp]>   # is r1 -> True
    

    Each registry owns its own port-keyed MultiDict, so no shared single-bit bucket survives to be overwritten. get(80, proto=tcp|udp|sctp) twice returns <TCP.http: 80 [...]> both times — 'PORT_80_tcp|udp|sctp' is now unreachable, since the mint uses owner.__transport__.name (apptype.py:2417) and is always one transport.

    Round-trip — holds for 10,624 of the 10,625 multi-transport members, swept in one process with zero mints and zero exceptions. 5,337 hand back the TCP twin of a UDP member (LSB-first _dispatch; same port, svc and proto), and 45 are alias→canonical collapses, which is get's documented contract with get_all for the rest.

    The single exception is a different mechanism: UDP.accessbuilder (888, tcp|udp) round-trips to <TCP.cddbp: 888 [tcp]>, because __canonical__ diverges between registries on 7 ports. Filed separately — and note == cannot catch it, since AppType.__eq__ compares ports only.

  5. removed
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    on Sep 24, 2026
  6. 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