Fix data-race in endpoint.Readiness

PiperOrigin-RevId: 302924789
This commit is contained in:
Bhasker Hariharan
2020-03-25 10:55:22 -07:00
committed by gVisor bot
parent d8c4eff3f7
commit d04adebaab
4 changed files with 82 additions and 42 deletions
+5
View File
@@ -29,3 +29,8 @@ type (
// Map is an alias of sync.Map.
Map = sync.Map
)
// NewCond is a wrapper around sync.NewCond.
func NewCond(l Locker) *Cond {
return sync.NewCond(l)
}
+28 -18
View File
@@ -365,21 +365,29 @@ func (l *listenContext) closeAllPendingEndpoints() {
}
// deliverAccepted delivers the newly-accepted endpoint to the listener. If the
// endpoint has transitioned out of the listen state, the new endpoint is closed
// instead.
// endpoint has transitioned out of the listen state (acceptedChan is nil),
// the new endpoint is closed instead.
func (e *endpoint) deliverAccepted(n *endpoint) {
e.mu.Lock()
state := e.EndpointState()
e.pendingAccepted.Add(1)
defer e.pendingAccepted.Done()
acceptedChan := e.acceptedChan
e.mu.Unlock()
defer e.pendingAccepted.Done()
if state == StateListen {
acceptedChan <- n
e.waiterQueue.Notify(waiter.EventIn)
} else {
n.Close()
e.acceptMu.Lock()
for {
if e.acceptedChan == nil {
e.acceptMu.Unlock()
n.Close()
return
}
select {
case e.acceptedChan <- n:
e.acceptMu.Unlock()
e.waiterQueue.Notify(waiter.EventIn)
return
default:
e.acceptCond.Wait()
}
}
}
@@ -420,11 +428,13 @@ func (e *endpoint) handleSynSegment(ctx *listenContext, s *segment, opts *header
}
func (e *endpoint) incSynRcvdCount() bool {
if e.synRcvdCount >= cap(e.acceptedChan) {
return false
e.acceptMu.Lock()
canInc := e.synRcvdCount < cap(e.acceptedChan)
e.acceptMu.Unlock()
if canInc {
e.synRcvdCount++
}
e.synRcvdCount++
return true
return canInc
}
func (e *endpoint) decSynRcvdCount() {
@@ -432,10 +442,10 @@ func (e *endpoint) decSynRcvdCount() {
}
func (e *endpoint) acceptQueueIsFull() bool {
if l, c := len(e.acceptedChan)+e.synRcvdCount, cap(e.acceptedChan); l >= c {
return true
}
return false
e.acceptMu.Lock()
full := len(e.acceptedChan)+e.synRcvdCount >= cap(e.acceptedChan)
e.acceptMu.Unlock()
return full
}
// handleListenSegment is called when a listening endpoint receives a segment
+46 -24
View File
@@ -291,6 +291,7 @@ func (*EndpointInfo) IsEndpointInfo() {}
// The following three mutexes can be acquired independent of e.mu but if
// acquired with e.mu then e.mu must be acquired first.
//
// e.acceptMu -> protects acceptedChan.
// e.rcvListMu -> Protects the rcvList and associated fields.
// e.sndBufMu -> Protects the sndQueue and associated fields.
// e.lastErrorMu -> Protects the lastError field.
@@ -533,6 +534,23 @@ type endpoint struct {
// to the acceptedChan below terminate before we close acceptedChan.
pendingAccepted sync.WaitGroup `state:"nosave"`
// acceptMu protects acceptedChan.
acceptMu sync.Mutex `state:"nosave"`
// acceptCond is a condition variable that can be used to block on when
// acceptedChan is full and an endpoint is ready to be delivered.
//
// This condition variable is required because just blocking on sending
// to acceptedChan does not work in cases where endpoint.Listen is
// called twice with different backlog values. In such cases the channel
// is closed and a new one created. Any pending goroutines blocking on
// the write to the channel will panic.
//
// We use this condition variable to block/unblock goroutines which
// tried to deliver an endpoint but couldn't because accept backlog was
// full ( See: endpoint.deliverAccepted ).
acceptCond *sync.Cond `state:"nosave"`
// acceptedChan is used by a listening endpoint protocol goroutine to
// send newly accepted connections to the endpoint so that they can be
// read by Accept() calls.
@@ -814,6 +832,7 @@ func newEndpoint(s *stack.Stack, netProto tcpip.NetworkProtocolNumber, waiterQue
e.segmentQueue.setLimit(MaxUnprocessedSegments)
e.tsOffset = timeStampOffset()
e.acceptCond = sync.NewCond(&e.acceptMu)
return e
}
@@ -834,9 +853,11 @@ func (e *endpoint) Readiness(mask waiter.EventMask) waiter.EventMask {
case StateListen:
// Check if there's anything in the accepted channel.
if (mask & waiter.EventIn) != 0 {
e.acceptMu.Lock()
if len(e.acceptedChan) > 0 {
result |= waiter.EventIn
}
e.acceptMu.Unlock()
}
}
if e.EndpointState().connected() {
@@ -981,29 +1002,19 @@ func (e *endpoint) closeNoShutdownLocked() {
// closePendingAcceptableConnections closes all connections that have completed
// handshake but not yet been delivered to the application.
func (e *endpoint) closePendingAcceptableConnectionsLocked() {
done := make(chan struct{})
// Spin a goroutine up as ranging on e.acceptedChan will just block when
// there are no more connections in the channel. Using a non-blocking
// select does not work as it can potentially select the default case
// even when there are pending writes but that are not yet written to
// the channel.
go func() {
defer close(done)
for n := range e.acceptedChan {
n.notifyProtocolGoroutine(notifyReset)
// close all connections that have completed but
// not accepted by the application.
n.Close()
}
}()
// pendingAccepted(see endpoint.deliverAccepted) tracks the number of
// endpoints which have completed handshake but are not yet written to
// the e.acceptedChan. We wait here till the goroutine above can drain
// all such connections from e.acceptedChan.
e.pendingAccepted.Wait()
e.acceptMu.Lock()
if e.acceptedChan == nil {
e.acceptMu.Unlock()
return
}
close(e.acceptedChan)
<-done
e.acceptedChan = nil
e.acceptCond.Broadcast()
e.acceptMu.Unlock()
// Wait for all pending endpoints to close.
e.pendingAccepted.Wait()
}
// cleanupLocked frees all resources associated with the endpoint. It is called
@@ -1012,9 +1023,7 @@ func (e *endpoint) closePendingAcceptableConnectionsLocked() {
func (e *endpoint) cleanupLocked() {
// Close all endpoints that might have been accepted by TCP but not by
// the client.
if e.acceptedChan != nil {
e.closePendingAcceptableConnectionsLocked()
}
e.closePendingAcceptableConnectionsLocked()
e.workerCleanup = false
@@ -2204,6 +2213,8 @@ func (e *endpoint) listen(backlog int) *tcpip.Error {
if e.EndpointState() == StateListen && !e.workerCleanup {
// Adjust the size of the channel iff we can fix existing
// pending connections into the new one.
e.acceptMu.Lock()
defer e.acceptMu.Unlock()
if len(e.acceptedChan) > backlog {
return tcpip.ErrInvalidEndpointState
}
@@ -2216,6 +2227,11 @@ func (e *endpoint) listen(backlog int) *tcpip.Error {
for ep := range origChan {
e.acceptedChan <- ep
}
// Notify any blocked goroutines that they can attempt to
// deliver endpoints again.
e.acceptCond.Broadcast()
return nil
}
@@ -2245,9 +2261,12 @@ func (e *endpoint) listen(backlog int) *tcpip.Error {
// The channel may be non-nil when we're restoring the endpoint, and it
// may be pre-populated with some previously accepted (but not Accepted)
// endpoints.
e.acceptMu.Lock()
if e.acceptedChan == nil {
e.acceptedChan = make(chan *endpoint, backlog)
}
e.acceptMu.Unlock()
e.workerRunning = true
go e.protocolListenLoop( // S/R-SAFE: drained on save.
seqnum.Size(e.receiveBufferAvailable()))
@@ -2276,9 +2295,12 @@ func (e *endpoint) Accept() (tcpip.Endpoint, *waiter.Queue, *tcpip.Error) {
}
// Get the new accepted endpoint.
e.acceptMu.Lock()
defer e.acceptMu.Unlock()
var n *endpoint
select {
case n = <-e.acceptedChan:
e.acceptCond.Signal()
default:
return nil, nil, tcpip.ErrWouldBlock
}
@@ -173,6 +173,9 @@ func (e *endpoint) afterLoad() {
// Restore the endpoint to InitialState as it will be moved to
// its origEndpointState during Resume.
e.state = StateInitial
// Condition variables and mutexs are not S/R'ed so reinitialize
// acceptCond with e.acceptMu.
e.acceptCond = sync.NewCond(&e.acceptMu)
stack.StackFromEnv.RegisterRestoredEndpoint(e)
}