Skip to content

fix(mcp): close OAuth callback port races — closures, reuse_address, TOCTOU (salvage of #44872 + #22161) - #65622

Merged
teknium1 merged 5 commits into
mainfrom
fix/mcp-oauth-callback-cluster
Jul 16, 2026
Merged

fix(mcp): close OAuth callback port races — closures, reuse_address, TOCTOU (salvage of #44872 + #22161)#65622
teknium1 merged 5 commits into
mainfrom
fix/mcp-oauth-callback-cluster

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Infographic

mcp-oauth-callback-cluster

Summary

Closes three races in the MCP OAuth callback flow: cross-server port pollution on concurrent flows, TIME_WAIT lockout on sequential flows, and the select-to-bind TOCTOU on the callback port.

Salvage of #44872 by @Code-suphub (cherry-picked, authorship preserved) with the TOCTOU fix from #22161 by @amathxbt folded in. The same closure-factory direction was independently submitted in #5345 by @caseyg (earliest), #44607 (@LeonSGP43), and #44685 (@HrushiYadav); the allow_reuse_address half also in #44611 (@LeonSGP43). Credit to all six contributors.

Fixes #44588, fixes #44590, fixes #5344.

Changes

Validation (live E2E, real sockets)

Check Result
Two concurrent flows: each handler prints its own port, global overwritten pass
Steal attempt on reserved port rejected
Callback round-trip through the adopted reserved socket pass
Sequential reflow on the same pinned port immediately after pass
test_mcp_oauth.py + test_mcp_oauth_manager.py 98/98

Code-suphub and others added 4 commits July 16, 2026 05:03
…#44588, #44590)

Two related OAuth fixes:

1. Replace module-level _redirect_handler with _make_redirect_handler()
   closure factory that closes over the resolved port. This prevents
   cross-server state pollution when multiple MCP servers run OAuth
   concurrently (#44588).

2. Set server.allow_reuse_address = True on the ephemeral callback
   HTTPServer so the socket doesn't stay in TIME_WAIT after the flow
   completes. This prevents 'Address already in use' errors on the
   next OAuth flow for the same port (#44590).

Fixes #44588
Fixes #44590
_find_free_port() closed its probe socket before HTTPServer re-bound
the port minutes later, leaving a window where another process could
steal it (#22161 by @amathxbt). _reserve_callback_port() now keeps the
selected socket bound (bounded FIFO pool) until _wait_for_callback
adopts it via bind_and_activate=False. Also sets allow_reuse_address
BEFORE binding — the cherry-picked #44872 set it after the constructor
had already bound, where it is a no-op.

Also updates the three #57836 non-interactive-guard tests to the
closure-factory API from #44872.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/mcp MCP client and OAuth area/auth Authentication, OAuth, credential pools labels Jul 16, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: this salvages #44872 and additionally closes the callback-port reservation race from #22161; it is broader than the closure-only approach in #5345.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment

Scope

  • 4 files (tools/mcp_oauth.py, tools/mcp_oauth_manager.py, tests/tools/test_mcp_oauth.py, scripts/release.py), +376/-11 lines
  • Fixes OAuth callback port races: closures, reuse_address, TOCTOU.

Quality

  • Multiple concurrent callback port openings could race; this fixes the race conditions.
  • Based on prior PR context (#44872, #22161 salvage).

Looks Good

  • Technical fix with multi-file impact.

Reviewed by Hermes Agent

teknium1 added a commit that referenced this pull request Jul 16, 2026
…cross ports

_wait_for_callback still read the legacy module-level _oauth_port, so
with two concurrent OAuth flows, flow A's callback wait bound flow B's
port while A's redirect URI pointed at A's port — the callback-side
half of the cross-flow collision that #65622 fixed on the redirect
side. _make_callback_waiter(port) closes over each flow's resolved
port; both provider construction sites (build_oauth_auth and
MCPOAuthManager._build_provider) now wire per-flow waiters. The legacy
_wait_for_callback delegates for backwards compatibility.

Direction credit to @LeonSGP43 (#34280) and the #34260 analysis.
teknium1 added a commit that referenced this pull request Jul 16, 2026
…cross ports

_wait_for_callback still read the legacy module-level _oauth_port, so
with two concurrent OAuth flows, flow A's callback wait bound flow B's
port while A's redirect URI pointed at A's port — the callback-side
half of the cross-flow collision that #65622 fixed on the redirect
side. _make_callback_waiter(port) closes over each flow's resolved
port; both provider construction sites (build_oauth_auth and
MCPOAuthManager._build_provider) now wire per-flow waiters. The legacy
_wait_for_callback delegates for backwards compatibility.

Direction credit to @LeonSGP43 (#34280) and the #34260 analysis.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…cross ports

_wait_for_callback still read the legacy module-level _oauth_port, so
with two concurrent OAuth flows, flow A's callback wait bound flow B's
port while A's redirect URI pointed at A's port — the callback-side
half of the cross-flow collision that NousResearch#65622 fixed on the redirect
side. _make_callback_waiter(port) closes over each flow's resolved
port; both provider construction sites (build_oauth_auth and
MCPOAuthManager._build_provider) now wire per-flow waiters. The legacy
_wait_for_callback delegates for backwards compatibility.

Direction credit to @LeonSGP43 (NousResearch#34280) and the NousResearch#34260 analysis.
@DavidMetcalfe

Copy link
Copy Markdown
Contributor

This PR addresses the root cause of #40656 (Desktop remote gateway destabilizing self-hosted host when used concurrently with TUI/gateway).

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…cross ports

_wait_for_callback still read the legacy module-level _oauth_port, so
with two concurrent OAuth flows, flow A's callback wait bound flow B's
port while A's redirect URI pointed at A's port — the callback-side
half of the cross-flow collision that NousResearch#65622 fixed on the redirect
side. _make_callback_waiter(port) closes over each flow's resolved
port; both provider construction sites (build_oauth_auth and
MCPOAuthManager._build_provider) now wire per-flow waiters. The legacy
_wait_for_callback delegates for backwards compatibility.

Direction credit to @LeonSGP43 (NousResearch#34280) and the NousResearch#34260 analysis.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

5 participants