Skip to content

Native UDP: hardening fixes (slot leak, transient errors, rotation, visitor unregister) - #2

Merged
ssharkkky merged 5 commits into
codex/native-udp-foundationfrom
codex/native-udp-hardening
Aug 21, 2026
Merged

Native UDP: hardening fixes (slot leak, transient errors, rotation, visitor unregister)#2
ssharkkky merged 5 commits into
codex/native-udp-foundationfrom
codex/native-udp-hardening

Conversation

@ssharkkky

Copy link
Copy Markdown
Owner

Summary

Hardening fixes for the native UDP path on top of codex/native-udp-foundation.
Four small, self-contained changes addressing correctness and availability
issues found while reviewing the SOCKS5 UDP ASSOCIATE → CONNECT-UDP over H3
datagram flow. Each is an independent green-to-green commit.

Caveat: these were not built locally — this is a Chromium tree and a
full build was not run in this environment. The changes are code-review-only;
please build + run the naive UDP tests before merging.

What's fixed

  1. Target-slot leak on first-datagram admission drop (naive_connect_udp_datagram_backend.cc)
    Send() inserted a new TargetEntry into targets_ before the
    association-level admission checks. When those checks rejected the datagram
    that created the target, Send() returned OK and left a permanent
    kConnecting entry with an empty queue that no timer reaps — leaking one of
    the 32 target slots for the association's lifetime. A client bursting many
    distinct destinations under queue pressure could exhaust the target table.
    Fix: erase the freshly created entry on the admission-drop path.

  2. Transient UDP relay error tears down the whole association (socks5_udp_association.cc)
    HandleRelayRead() / OnRelayWriteComplete() called Finish() on any
    negative result, killing the control TCP connection and every active tunnel.
    A single per-packet condition — most commonly Windows surfacing a prior ICMP
    "port unreachable" as ERR_CONNECTION_RESET on the next recv/send, also
    ERR_MSG_TOO_BIG / ERR_ADDRESS_UNREACHABLE — would take down traffic to
    all unrelated destinations sharing the association. Fix: classify these as
    recoverable, drop the affected datagram, keep pumping.

  3. UDP associations never rotate at tunnel_timeout_ (socks5_udp_association.{h,cc}, naive_proxy.cc)
    CleanUpIdleConnections() rotates TCP tunnels on both idle_timeout_ and a
    tunnel_timeout_ max-lifetime bound, but UDP associations were only aged by
    idle_timeout_. A steadily-used association never rotated. Fix: expose
    creation_time() and apply the same idle || age test used for TCP tunnels.

  4. Asymmetric H3 datagram-visitor unregister on stream close (quic_proxy_datagram_client_socket.cc)
    Close() guards UnregisterHttp3DatagramVisitor() behind
    datagram_visitor_registered_, but OnStreamClosed() cleared the flag
    without unregistering. It's a no-op today (the stream is already closed on
    that path), but the asymmetry is fragile under future refactors. Fix: make
    OnStreamClosed() perform the same guarded unregister. Defensive only.

Not addressed here (follow-ups)

Deliberately left out of this PR because they involve configuration surface or
authorization-semantics decisions the branch author should own:

  • UDP-ASSOCIATE capability/disable path in NaiveProxy::CanUseNativeUdp()
    no server-capability check or explicit disable flag, so a mismatch can silently
    black-hole UDP. Needs a config/CLI decision.
  • Relay socket attached to the global NetLog in BindUdpRelay() — latent
    under the shipped kDefault capture mode, but at kEverything the SOCKS5
    datagram destination + payload would be captured. A correct fix needs a
    non-capturing NetLog path and touches NetLog semantics.
  • IsAuthorizedClient() wildcard-port latching — when the client requested
    DST port 0, the first datagram from any source port on the control-peer IP
    latches client_endpoint_. Tightening this is an authorization-semantics
    change.

Testing

Not built or run locally. Review-only. Suggest building the naive target and
running the SOCKS5 UDP / CONNECT-UDP unit tests before merge.

ssharkkky and others added 5 commits July 26, 2026 18:15
NaiveConnectUdpDatagramBackend::Send() inserted a new TargetEntry into
targets_ before the association-level admission checks. When those checks
rejected the datagram that created the target, Send() returned OK and left
a permanent kConnecting entry with an empty outbound queue that no timer
ever reaps, leaking one of the 32 target slots for the association's
lifetime. A client that bursts many distinct destinations under queue
pressure could exhaust the target table and stall further targets.

Track whether the target was created for this datagram and erase it on the
association-level queue-capacity drop path. The cooldown/retiring branches
cannot fire for a freshly created kConnecting target, so only this path
needs the cleanup.

Not built locally (Chromium tree); change is code-review-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HandleRelayRead() and OnRelayWriteComplete() called Finish() on any negative
result, tearing down the control TCP connection and every active tunnel for
the association. A single per-packet condition would therefore kill traffic to
all unrelated destinations sharing the association. The most common case is
Windows surfacing a prior ICMP "port unreachable" as ERR_CONNECTION_RESET on
the next recv/send; ERR_MSG_TOO_BIG and ERR_ADDRESS_UNREACHABLE are similarly
per-datagram.

Classify these as recoverable: drop the affected datagram and continue pumping
instead of finishing the association. Non-recoverable errors still finish.

Not built locally (Chromium tree); change is code-review-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CleanUpIdleConnections() rotated TCP tunnels on both idle_timeout_ and a
tunnel_timeout_ max-lifetime bound, but UDP associations were only aged by
idle_timeout_. A long-lived association carrying steady traffic never rotated,
unlike every other tunnel, weakening the intended lifetime cap and connection
rotation behavior.

Expose Socks5UdpAssociation::creation_time() and apply the same
"idle > idle_timeout_ || age > tunnel_timeout_" test used for TCP tunnels.

Not built locally (Chromium tree); change is code-review-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close() unregisters the Http3DatagramVisitor when datagram_visitor_registered_
is set, but OnStreamClosed() cleared the flag without calling
UnregisterHttp3DatagramVisitor(). The stream is already closed on this path so
the unregister is effectively a no-op today, but the asymmetry is fragile: a
future change that lets the stream handle outlive the close notification would
leave a dangling visitor registration.

Make OnStreamClosed() perform the same guarded unregister as Close().

Not built locally (Chromium tree); change is code-review-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reject new CONNECT-UDP targets before factory/emplace when the
association queue is full, so admission pressure cannot churn throwaway
tunnels. Recoverable relay SendTo failures drop the datagram without
refreshing last_activity_. Add ShouldExpire() for the idle-or-age
rotation already used by CleanUpIdleConnections.

Cover the four hardening behaviors in naive_connect_udp_backend_test
and naive_socks5_udp_association_test. Built and ran the M2/M3 owner
scripts on the existing macOS arm64 Release tree:

M2_SOCKS5_UDP_INGRESS_OK
M3_NATIVE_UDP_CLIENT_OK
@ssharkkky
ssharkkky merged commit 474a1e4 into codex/native-udp-foundation Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant