You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AppType.get(port, proto=<combined flag>) mints an unknown member and displaces the real one #736
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 singleTransportProtocol 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.
Related, same area, not filed separately: _missing_'s 766 range branches take no proto parameter, so get(6010, proto='udp') returns a tcp member and a second identical call raises TypeError.
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.
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.
Activity
JarryShaw commentedon Sep 24, 2026
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
blockedand 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 memberget(7, proto=tcp)returns, and the round-tripget(m.port, proto=m.proto)must hold for a multi-transport member.JarryShaw commentedon Sep 24, 2026
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:
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 usesowner.__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,svcandproto), and 45 are alias→canonical collapses, which isget's documented contract withget_allfor 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, sinceAppType.__eq__compares ports only.