Skip to content

Fix concurrent map writes in codespace port forwarding#13313

Open
williammartin wants to merge 5 commits into
trunkfrom
williammartin/fix-concurrent-map-writes-port-forward
Open

Fix concurrent map writes in codespace port forwarding#13313
williammartin wants to merge 5 commits into
trunkfrom
williammartin/fix-concurrent-map-writes-port-forward

Conversation

@williammartin

@williammartin williammartin commented Apr 29, 2026

Copy link
Copy Markdown
Member

Fixes #13399

When forwarding multiple ports concurrently with gh cs ports forward, the dev-tunnels TunnelManager mutates shared state on the Tunnel object in buildUri(), which is not goroutine-safe. This causes a fatal error: concurrent map writes panic.

Root cause

ForwardPorts runs all port forwards concurrently via errgroup, sharing a single CodespaceConnection. The shallow copy in NewPortForwarder (*codespaceConnection dereference) means all forwarders share the same TunnelManager and Tunnel pointers, so concurrent calls to GetTunnelPort / CreateTunnelPort race on the shared map.

Fix

  • Store connection as a pointer - changed CodespacesPortForwarder.connection from connection.CodespaceConnection (value) to *connection.CodespaceConnection (pointer), eliminating the unnecessary shallow copy. Since all the important fields (TunnelManager, TunnelClient, Tunnel, Options) were already pointers, the copy was pointless and created this class of bug.
  • Added sync.Mutex (ManagerMu) to CodespaceConnection to serialize TunnelManager operations that mutate shared state on the Tunnel object. Because the connection is now shared by pointer, a plain sync.Mutex (not a pointer) works naturally.
  • Extracted createTunnelPort helper from ForwardPort so the mutex-guarded critical section uses defer to unlock, replacing fragile manual Unlock() calls on each error path.
  • Connect and RefreshPorts already have their own synchronization and are left outside the lock.

Test

Added TestConcurrentForwardPortDoesNotRace which forwards 10 ports concurrently from a shared connection, reproducing the race detected by go test -race.

Performance

The mutex serializes TunnelManager API calls that were previously concurrent. Benchmarking with a real codespace (5 ports, 5 runs each) shows no measurable impact:

Run 1 Run 2 Run 3 Run 4 Run 5 Avg
Stock CLI (v2.92.0) 1.03s 0.89s 0.90s 0.91s 1.18s 0.98s
Fixed CLI 1.04s 1.04s 0.89s 0.98s 1.08s 1.01s

The serialization overhead is negligible because the bottleneck is network round-trips to the dev-tunnels API and SSH tunnel setup, not CPU time in the critical section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
williammartin and others added 2 commits May 12, 2026 13:30
Instead of copying CodespaceConnection by value and using a *sync.Mutex
pointer to share the mutex across copies, store a *CodespaceConnection
pointer in CodespacesPortForwarder. This makes all forwarders naturally
share the same connection (and its mutex) without pointer tricks.

Also extract the mutex-guarded section in ForwardPort into a helper
method (createTunnelPort) that uses defer to unlock, replacing fragile
manual Unlock calls on each error path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@williammartin williammartin marked this pull request as ready for review May 12, 2026 12:15
Copilot AI review requested due to automatic review settings May 12, 2026 12:15
@williammartin williammartin requested review from a team as code owners May 12, 2026 12:15
@williammartin williammartin requested a review from BagToad May 12, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a race in gh cs ports forward where concurrent port forwards can trigger fatal error: concurrent map writes due to goroutine-unsafe mutations inside the dev-tunnels TunnelManager/Tunnel objects.

Changes:

  • Switch CodespacesPortForwarder.connection to store *connection.CodespaceConnection (avoids shallow-copying shared pointers).
  • Add ManagerMu sync.Mutex to CodespaceConnection and use it to serialize TunnelManager operations (create/get/list/delete ports).
  • Add a concurrent forwarding test intended to reproduce the race under go test -race.
Show a summary per file
File Description
internal/codespaces/portforwarder/port_forwarder.go Avoids connection shallow-copying and serializes tunnel-manager operations with a mutex.
internal/codespaces/portforwarder/port_forwarder_test.go Adds/updates tests, including a new concurrent-forwarding regression test.
internal/codespaces/connection/connection.go Introduces ManagerMu on CodespaceConnection to coordinate manager access across forwarders.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment thread internal/codespaces/portforwarder/port_forwarder_test.go
Comment thread internal/codespaces/portforwarder/port_forwarder.go
Comment thread internal/codespaces/portforwarder/port_forwarder.go Outdated
williammartin and others added 2 commits May 13, 2026 15:51
Wrap GetTunnelPort and DeleteTunnelPort calls in anonymous functions
that use defer for the mutex unlock, making them robust against future
edits that add early returns between Lock and Unlock.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Hold ManagerMu across the entire get/check/delete sequence in
  UpdatePortVisibility to eliminate a time-of-check-time-of-use gap
- Remove IIFE pattern in favor of explicit lock/unlock
- Revert signal.NotifyContext change in cmd.go (out of scope)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

gh cs ports forward crashes with "fatal error: concurrent map writes" when forwarding multiple ports

2 participants