Skip to content

server: fix data race on handshakeDeadline after the handshake - #14

Merged
raggi merged 1 commit into
masterfrom
raggi/tsnet-listenssh
Oct 25, 2026
Merged

raggi merged 1 commit into
masterfrom
raggi/tsnet-listenssh

Conversation

@raggi

@raggi raggi commented Oct 25, 2026

Copy link
Copy Markdown
Member

HandleConn cleared the handshake deadline by writing the field directly after the handshake completed. The x/crypto transport goroutines started by the handshake call Read and Write concurrently; both recompute deadlines via updateDeadline when IdleTimeout is set, which reads the same field without synchronization. The race is only reachable with IdleTimeout set, which is why it only showed up in the tailscale SSH servers when TS_SSH_IDLE_TIMEOUT was enabled.

handshakeDeadline is now an atomic.Pointer[time.Time]. HandleConn stores a pointer to the deadline before the handshake starts and nil once the handshake completes; updateDeadline skips the deadline when it loads nil. All accesses are atomic loads and stores.

TestServerConnHandshakeDeadlineRace fails under -race if handshakeDeadline is a non-atomic *time.Time field and passes with the atomic. TestServerHandshakeTimeoutClearedAfterHandshake checks that the handshake deadline stops applying after the handshake.

HandleConn cleared the handshake deadline by writing the field
directly after the handshake completed. The x/crypto transport
goroutines started by the handshake call Read and Write concurrently;
both recompute deadlines via updateDeadline when IdleTimeout is set,
which reads the same field without synchronization. The race is only
reachable with IdleTimeout set, which is why it only showed up in the
tailscale SSH servers when TS_SSH_IDLE_TIMEOUT was enabled.

handshakeDeadline is now an atomic.Pointer[time.Time]. HandleConn
stores a pointer to the deadline before the handshake starts and nil
once the handshake completes; updateDeadline skips the deadline when
it loads nil. All accesses are atomic loads and stores.

TestServerConnHandshakeDeadlineRace fails under -race if
handshakeDeadline is a non-atomic *time.Time field and passes with
the atomic. TestServerHandshakeTimeoutClearedAfterHandshake checks
that the handshake deadline stops applying after the handshake.
Comment thread server.go
if srv.HandshakeTimeout > 0 {
conn.handshakeDeadline = time.Now().Add(srv.HandshakeTimeout)
handshakeDeadline := time.Now().Add(srv.HandshakeTimeout)
conn.handshakeDeadline.Store(&handshakeDeadline)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or drop the local and use new(expr)

@raggi
raggi merged commit 2172284 into master Oct 25, 2026
4 checks passed
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.

2 participants