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
No issue in that range mentions IPv4-mapped IPv6 canonicalization or the driver-level identity split below.
Findings
F1 — IPv4-mapped IPv6 advertise addresses pass validation but are not consistently reachable
Severity: Medium Confidence: High on the audited macOS socket stack; Medium for identical behavior on every supported OS.
Classification: Proven correctness bug. The related CIDR consequence is an architecture/security risk whose exact exploitability depends on policy and socket-family configuration.
The advertised address must be a concrete, canonical endpoint reachable by both gossip and reliable transports.
Equivalent IPv4 and IPv4-mapped representations must not key the same physical endpoint differently.
CIDR policy must not change solely because an IPv4 endpoint is written as IPv4-mapped IPv6.
A validated advertise address must not let TCP/TLS connectivity mask an unreachable UDP identity.
Concrete execution trace
Configure a standard TCP/TLS node with:
[::ffff:127.0.0.1]:0
either directly as MaybeResolved::Resolved or as a resolver candidate.
The TCP listener bind succeeds on the audited host. local_addr() retains the mapped address and replaces port zero with a concrete port.
Both validators accept it:
::ffff:127.0.0.1 is not Ipv6Addr::UNSPECIFIED;
it is not IPv6 multicast;
the IPv4 broadcast check runs only for SocketAddr::V4;
the read-back port is nonzero.
The endpoint, snapshot, and Alive state therefore use the mapped SocketAddr::V6. Proto encoding emits the V6 tag and the original 16 address octets; it does not canonicalize the value to 127.0.0.1.
A normal peer bound to native IPv4 owns an IPv4 gossip socket. In the focused experiment:
an IPv6 TCP client connecting to ::ffff:127.0.0.1 reached a native IPv4 listener;
an IPv4 UDP socket could not address the mapped-V6 destination;
targeting 127.0.0.1 instead reached the same mapped-bound UDP server.
Consequently, TCP/TLS reliable exchanges can reach the node while UDP probes and gossip from a native-IPv4 peer cannot use the gossiped address. The peer can falsely suspect and reap a node whose reliable listener remains reachable.
QUIC has no TCP escape hatch: its reliable and gossip traffic share the UDP socket, so the same representation mismatch can make the advertised node wholly unreachable from the native-IPv4 peer.
CIDR consequence
CidrPolicy passes the raw IpAddr to ipnet. Locked ipnet deliberately returns false for cross-family comparisons:
IpNet::V4 versus IpAddr::V6 => false
IpNet::V6 versus IpAddr::V4 => false
Therefore:
10.0.0.0/8 allows 10.1.2.3 but rejects ::ffff:10.1.2.3, although the latter routes through IPv4.
An IPv6 policy broad enough to include the mapped prefix—most obviously ::/0—admits a mapped destination that the socket stack routes to IPv4, despite native IPv4 being excluded.
The drivers apply CIDR checks directly to raw seed, datagram-source, accepted-peer, and self-advertised addresses, so no later layer repairs this distinction. This is a policy-representation risk rather than proof of a remotely exploitable bypass under every bind configuration.
Impact
Accepted configuration can create an Alive identity that is not reachable on the mandatory UDP plane.
TCP/TLS success and UDP failure disagree about whether the same member is reachable.
Membership can oscillate through false suspicion/death despite working reliable connections.
QUIC nodes may be completely unreachable.
CIDR admission and outbound filtering can false-reject or unintendedly admit the same underlying IPv4 route depending on textual/address-family representation.
Member keys, resolved seeds, and reported addresses can distinguish native and mapped forms of one physical endpoint.
The unusual mapped-address input limits severity, but construction currently declares it valid and publishes it as a usable contact.
Remediation
Choose and enforce one policy across both drivers:
Preferably canonicalize every IPv4-mapped IPv6 SocketAddr to SocketAddr::V4 before it enters driver identity:
advertise candidates;
post-bind local_addr;
resolved seeds;
accepted peer and datagram source addresses;
CIDR lookup;
endpoint and wire identity.
Alternatively, reject mapped addresses with a typed advertise/seed error before binding or dispatch. This is simpler but removes otherwise usable input.
Define handling for mapped IPv6 CIDRs. Either normalize exactly representable mapped networks to IPv4 or reject/document them so policy meaning cannot depend on representation.
Preserve the original unresolved/user input separately if diagnostic visibility is desired; do not use it as membership identity.
Acceptance tests
For Compio TCP, TLS, and QUIC, construct with [::ffff:127.0.0.1]:0. Require either:
typed rejection before the driver starts, or
successful construction advertising native 127.0.0.1:<actual-port>.
Repeat for Reactor TCP, TLS, and QUIC.
With canonicalization, run two real nodes:
one configured with native IPv4;
one given the mapped equivalent.
Require join, UDP probing, gossip, and reliable exchange to succeed, and require snapshots to contain only the canonical address.
Resolve a seed to both native and mapped forms and prove CIDR decisions are identical while preserving the documented per-exchange duplicate counting policy.
Assert that 10.0.0.0/8 treats 10.1.2.3 and ::ffff:10.1.2.3 according to the chosen canonical policy.
Assert that an IPv6-only policy cannot reach IPv4 through the mapped prefix unless that behavior is explicitly configured and documented.
Run the socket-family tests on each supported OS because bind/readback normalization differs across kernels.
Cross-driver architecture assessment
The defect has the same root in both standard drivers: address resolution yields a SocketAddr, bind readback makes the port concrete, and validation classifies the address using surface-level IpAddr predicates without first canonicalizing mapped IPv4.
Relevant parity strengths:
TCP/TLS bind the reliable listener and gossip UDP socket to the same actual port.
QUIC reads back the actual UDP port.
Port-zero identities are not published with port zero.
Unspecified, multicast, and IPv4 broadcast advertise addresses are rejected.
LocalAddrResolver preserves the requested IPv4/IPv6 family and excludes loopback, unspecified, and link-local addresses in its All scope.
Both drivers gate inbound datagrams before machine ingress and inbound streams before bridge/membership allocation.
The same CIDR policy separately gates transport source and peer self-advertised membership address.
Empty seed lists, nonempty lists resolving to no addresses, and duplicate seed exchanges have explicit, matching semantics.
Notable non-finding differences and risks:
Compio exposes an explicit AdvertiseAddrResolver; Reactor always uses the resolver’s first candidate. No silent successful bind to an invalid address was established, but resolver ordering is a stronger API responsibility in Reactor.
Both drivers intentionally fail the entire join-resolution batch on the first resolver error. This sacrifices partial bootstrap progress but is consistent across drivers and is covered by explicit error-propagation behavior, so it was not classified as a defect.
Compio stringifies resolver errors into io::ErrorKind::Other at memberlist-compio/src/memberlist/mod.rs:1190-1194 and the transport constructor equivalents, while Reactor preserves the boxed original error at memberlist-reactor/src/memberlist.rs:361-364,1306-1312. This loses type/source diagnostics in Compio, but no control-flow or security consequence was proven in this scope.
Validation performed/results
All validation was read-only with build output under /tmp.
bind [::ffff:0.0.0.0]:0 succeeded but read back as [::]:port; the existing unspecified-address validator therefore rejects this narrower wildcard candidate.
bind [::ffff:127.0.0.1]:0 succeeded and retained the mapped address.
connecting through [::ffff:127.0.0.1] reached a native IPv4 listener.
Focused UDP experiment:
mapped-loopback UDP bind succeeded and retained the mapped address;
a native IPv4 UDP socket could not use the mapped-V6 destination;
the native IPv4 destination reached the same mapped-bound socket.
Locked ipnet 2.12.0 source was checked directly; IpNet::contains(&IpAddr) matches only equal enum families.
A nonempty seed list resolving to zero addresses returns JoinFailed, not false success.
Resolver errors do not collapse into successful joins.
CIDR-blocked inbound streams are dropped before bridge registration, and blocked datagrams are dropped before endpoint ingress.
Membership admission correctly checks the peer’s self-advertised address separately from transport-source filtering.
No distinct defect was found involving port sharing, self-seed accounting, join cancellation, persistent allocation after ordinary CIDR rejection, or TCP/TLS/QUIC peer attribution beyond the excluded existing issues.
No production patch in this issue
This was a read-only audit. No production code, tests, generated files, or repository metadata were changed.
IPv4-mapped IPv6 addresses split standard-driver identity and admission
Audit scope, pinned commit, duplicate exclusions
/Users/al/Developer/memberlistaeef956bc0bdf0ed21ba386b329dab6aae64b47eSocketAddrwire representation andCidrPolicyonly where required to interpret driver behavior.ipnetbehavior.GitHub issues #157–#169 were screened. This report does not restate:
No issue in that range mentions IPv4-mapped IPv6 canonicalization or the driver-level identity split below.
Findings
F1 — IPv4-mapped IPv6 advertise addresses pass validation but are not consistently reachable
Severity: Medium
Confidence: High on the audited macOS socket stack; Medium for identical behavior on every supported OS.
Classification: Proven correctness bug. The related CIDR consequence is an architecture/security risk whose exact exploitability depends on policy and socket-family configuration.
Location
Advertise resolution and binding:
memberlist-compio/src/tcp/mod.rs:168-182,197-235memberlist-compio/src/tls/mod.rs:232-246,261-301memberlist-compio/src/quic/mod.rs:188-218memberlist-reactor/src/memberlist.rs:757-763,1098-1135,1296-1313Validators that omit mapped-address handling:
memberlist-compio/src/options/mod.rs:788-817memberlist-reactor/src/memberlist.rs:1568-1578Mapped IPv6 remains IPv6 on the wire:
memberlist-proto/src/data/primitives.rs:192-245,248-305memberlist-proto/src/convert/mod.rs:178-205Representation-sensitive CIDR paths:
memberlist-proto/src/cidr/mod.rs:135-145,210-220,241-248memberlist-reactor/src/cidr.rs:22-40memberlist-reactor/src/driver/stream/mod.rs:1154-1170,1721-1726,1744-1761memberlist-reactor/src/driver/quic/mod.rs:1090-1096memberlist-compio/src/driver/stream/mod.rs:2162-2192,3171-3194Locked family-matching dependency:
Cargo.lock:2308-2314ipnet-2.12.0/src/ipnet.rs:1419-1425Violated invariants
Concrete execution trace
Configure a standard TCP/TLS node with:
either directly as
MaybeResolved::Resolvedor as a resolver candidate.The TCP listener bind succeeds on the audited host.
local_addr()retains the mapped address and replaces port zero with a concrete port.Both validators accept it:
::ffff:127.0.0.1is notIpv6Addr::UNSPECIFIED;SocketAddr::V4;The endpoint, snapshot, and Alive state therefore use the mapped
SocketAddr::V6. Proto encoding emits the V6 tag and the original 16 address octets; it does not canonicalize the value to127.0.0.1.A normal peer bound to native IPv4 owns an IPv4 gossip socket. In the focused experiment:
::ffff:127.0.0.1reached a native IPv4 listener;127.0.0.1instead reached the same mapped-bound UDP server.Consequently, TCP/TLS reliable exchanges can reach the node while UDP probes and gossip from a native-IPv4 peer cannot use the gossiped address. The peer can falsely suspect and reap a node whose reliable listener remains reachable.
QUIC has no TCP escape hatch: its reliable and gossip traffic share the UDP socket, so the same representation mismatch can make the advertised node wholly unreachable from the native-IPv4 peer.
CIDR consequence
CidrPolicypasses the rawIpAddrtoipnet. Lockedipnetdeliberately returnsfalsefor cross-family comparisons:Therefore:
10.0.0.0/8allows10.1.2.3but rejects::ffff:10.1.2.3, although the latter routes through IPv4.::/0—admits a mapped destination that the socket stack routes to IPv4, despite native IPv4 being excluded.The drivers apply CIDR checks directly to raw seed, datagram-source, accepted-peer, and self-advertised addresses, so no later layer repairs this distinction. This is a policy-representation risk rather than proof of a remotely exploitable bypass under every bind configuration.
Impact
The unusual mapped-address input limits severity, but construction currently declares it valid and publishes it as a usable contact.
Remediation
Choose and enforce one policy across both drivers:
Preferably canonicalize every IPv4-mapped IPv6
SocketAddrtoSocketAddr::V4before it enters driver identity:local_addr;Alternatively, reject mapped addresses with a typed advertise/seed error before binding or dispatch. This is simpler but removes otherwise usable input.
Define handling for mapped IPv6 CIDRs. Either normalize exactly representable mapped networks to IPv4 or reject/document them so policy meaning cannot depend on representation.
Preserve the original unresolved/user input separately if diagnostic visibility is desired; do not use it as membership identity.
Acceptance tests
For Compio TCP, TLS, and QUIC, construct with
[::ffff:127.0.0.1]:0. Require either:127.0.0.1:<actual-port>.Repeat for Reactor TCP, TLS, and QUIC.
With canonicalization, run two real nodes:
Require join, UDP probing, gossip, and reliable exchange to succeed, and require snapshots to contain only the canonical address.
Resolve a seed to both native and mapped forms and prove CIDR decisions are identical while preserving the documented per-exchange duplicate counting policy.
Assert that
10.0.0.0/8treats10.1.2.3and::ffff:10.1.2.3according to the chosen canonical policy.Assert that an IPv6-only policy cannot reach IPv4 through the mapped prefix unless that behavior is explicitly configured and documented.
Run the socket-family tests on each supported OS because bind/readback normalization differs across kernels.
Cross-driver architecture assessment
The defect has the same root in both standard drivers: address resolution yields a
SocketAddr, bind readback makes the port concrete, and validation classifies the address using surface-levelIpAddrpredicates without first canonicalizing mapped IPv4.Relevant parity strengths:
LocalAddrResolverpreserves the requested IPv4/IPv6 family and excludes loopback, unspecified, and link-local addresses in itsAllscope.Notable non-finding differences and risks:
AdvertiseAddrResolver; Reactor always uses the resolver’s first candidate. No silent successful bind to an invalid address was established, but resolver ordering is a stronger API responsibility in Reactor.io::ErrorKind::Otheratmemberlist-compio/src/memberlist/mod.rs:1190-1194and the transport constructor equivalents, while Reactor preserves the boxed original error atmemberlist-reactor/src/memberlist.rs:361-364,1306-1312. This loses type/source diagnostics in Compio, but no control-flow or security consequence was proven in this scope.Validation performed/results
All validation was read-only with build output under
/tmp.git rev-parse HEAD:git status --short: empty before and after.Duplicate keyword screening across [Adversarial review] Correctness, lifecycle, and architecture findings across proto/compio/reactor #157–Vacuous simulation and fuzz oracles, plus missing regression gates, weaken test soundness #169 found no existing IPv4-mapped/CIDR-family finding.
Focused TCP socket experiment:
[::ffff:0.0.0.0]:0succeeded but read back as[::]:port; the existing unspecified-address validator therefore rejects this narrower wildcard candidate.[::ffff:127.0.0.1]:0succeeded and retained the mapped address.[::ffff:127.0.0.1]reached a native IPv4 listener.Focused UDP experiment:
Locked
ipnet 2.12.0source was checked directly;IpNet::contains(&IpAddr)matches only equal enum families.Focused command issued with isolated target:
Result: 6 CIDR unit tests passed, 0 failed; the other test binaries selected 0 tests under the filter.
Non-findings and positive controls
NoAddresses.JoinFailed, not false success.No production patch in this issue
This was a read-only audit. No production code, tests, generated files, or repository metadata were changed.