Skip to content

Commit

Permalink
ssh: ensure that handshakeTransport goroutines have finished before C…
Browse files Browse the repository at this point in the history
…lose returns

This fixes a data race in the tests for x/crypto/ssh, which expects to
be able to examine a transport's read and write counters without
locking after closing it.

(Given the number of goroutines, channels, and mutexes used in this
package, I wouldn't be surprised if other concurrency bugs remain.
I would suggest simplifying the concurrency in this package, but I
don't intend to follow up on that myself at the moment.)

Fixes golang/go#56957.

Change-Id: Ib1f1390b66707c66a3608e48f3f52483cff3c1f5
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/456758
Reviewed-by: Roland Shoemaker <roland@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
  • Loading branch information
Bryan C. Mills authored and gopherbot committed Dec 12, 2022
1 parent f495dc3 commit 23edec0
Showing 1 changed file with 30 additions and 18 deletions.
48 changes: 30 additions & 18 deletions ssh/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ type handshakeTransport struct {
incoming chan []byte
readError error

mu sync.Mutex
writeError error
sentInitPacket []byte
sentInitMsg *kexInitMsg
pendingPackets [][]byte // Used when a key exchange is in progress.
mu sync.Mutex
writeError error
sentInitPacket []byte
sentInitMsg *kexInitMsg
pendingPackets [][]byte // Used when a key exchange is in progress.
writePacketsLeft uint32
writeBytesLeft int64

// If the read loop wants to schedule a kex, it pings this
// channel, and the write loop will send out a kex
Expand All @@ -71,7 +73,8 @@ type handshakeTransport struct {

// If the other side requests or confirms a kex, its kexInit
// packet is sent here for the write loop to find it.
startKex chan *pendingKex
startKex chan *pendingKex
kexLoopDone chan struct{} // closed (with writeError non-nil) when kexLoop exits

// data for host key checking
hostKeyCallback HostKeyCallback
Expand All @@ -86,12 +89,10 @@ type handshakeTransport struct {
// Algorithms agreed in the last key exchange.
algorithms *algorithms

// Counters exclusively owned by readLoop.
readPacketsLeft uint32
readBytesLeft int64

writePacketsLeft uint32
writeBytesLeft int64

// The session ID or nil if first kex did not complete yet.
sessionID []byte
}
Expand All @@ -108,7 +109,8 @@ func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion,
clientVersion: clientVersion,
incoming: make(chan []byte, chanSize),
requestKex: make(chan struct{}, 1),
startKex: make(chan *pendingKex, 1),
startKex: make(chan *pendingKex),
kexLoopDone: make(chan struct{}),

config: config,
}
Expand Down Expand Up @@ -340,16 +342,17 @@ write:
t.mu.Unlock()
}

// Unblock reader.
t.conn.Close()

// drain startKex channel. We don't service t.requestKex
// because nobody does blocking sends there.
go func() {
for init := range t.startKex {
init.done <- t.writeError
}
}()
for request := range t.startKex {
request.done <- t.getWriteError()
}

// Unblock reader.
t.conn.Close()
// Mark that the loop is done so that Close can return.
close(t.kexLoopDone)
}

// The protocol uses uint32 for packet counters, so we can't let them
Expand Down Expand Up @@ -545,7 +548,16 @@ func (t *handshakeTransport) writePacket(p []byte) error {
}

func (t *handshakeTransport) Close() error {
return t.conn.Close()
// Close the connection. This should cause the readLoop goroutine to wake up
// and close t.startKex, which will shut down kexLoop if running.
err := t.conn.Close()

// Wait for the kexLoop goroutine to complete.
// At that point we know that the readLoop goroutine is complete too,
// because kexLoop itself waits for readLoop to close the startKex channel.
<-t.kexLoopDone

return err
}

func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
Expand Down

0 comments on commit 23edec0

Please sign in to comment.