Skip to content

Commit 3c4e58b

Browse files
authored
service/dap: fix race condition between disconnect and continue (#4028)
If a disconnect requests comes in while the runUntilStopAndNotify goroutine is hitting a breakpoint a nil debugger dereference can happen at various points. This change resolves the problem by never resetting the debugger field to nil, instead a separate flag is set (which mostly just exist to avoid duplicate logging and to keep tests happy). Fixes #4006, #4007, #4020
1 parent 990621f commit 3c4e58b

File tree

2 files changed

+84
-7
lines changed

2 files changed

+84
-7
lines changed

service/dap/server.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ type Session struct {
143143
// to ensure that messages do not get interleaved
144144
sendingMu sync.Mutex
145145

146+
disconnected bool // disconnect was called
147+
146148
// runningCmd tracks whether the server is running an asynchronous
147149
// command that resumes execution, which may not correspond to the actual
148150
// running state of the process (e.g. if a command is temporarily interrupted).
@@ -412,6 +414,8 @@ func (s *Session) Close() {
412414
s.stopDebugSession(killProcess)
413415
} else if s.noDebugProcess != nil {
414416
s.stopNoDebugProcess()
417+
} else {
418+
s.disconnected = true
415419
}
416420
// The binary is no longer in use by the debugger. It is safe to remove it.
417421
if s.binaryToRemove != "" {
@@ -1238,6 +1242,7 @@ func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd stri
12381242
// onDisconnectRequest (run goroutine) and requires holding mu lock.
12391243
func (s *Session) stopNoDebugProcess() {
12401244
if s.noDebugProcess == nil {
1245+
s.disconnected = true
12411246
// We already handled termination or there was never a process
12421247
return
12431248
}
@@ -1285,7 +1290,7 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
12851290
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
12861291
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
12871292
s.conn.Close()
1288-
s.debugger = nil
1293+
s.disconnected = true
12891294
// The target is left in whatever state it is already in - halted or running.
12901295
// The users therefore have the flexibility to choose the appropriate state
12911296
// for their case before disconnecting. This is also desirable in case of
@@ -1308,6 +1313,8 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
13081313
err = s.stopDebugSession(killProcess)
13091314
} else if s.noDebugProcess != nil {
13101315
s.stopNoDebugProcess()
1316+
} else {
1317+
s.disconnected = true
13111318
}
13121319
if err != nil {
13131320
s.sendErrorResponse(request.Request, DisconnectError, "Error while disconnecting", err.Error())
@@ -1327,10 +1334,10 @@ func (s *Session) stopDebugSession(killProcess bool) error {
13271334
defer func() {
13281335
// Avoid running stop sequence twice.
13291336
// It's not fatal, but will result in duplicate logging.
1330-
s.debugger = nil
1337+
s.disconnected = true
13311338
s.changeStateMu.Unlock()
13321339
}()
1333-
if s.debugger == nil {
1340+
if s.debugger == nil || s.disconnected {
13341341
return nil
13351342
}
13361343
var exited error

service/dap/server_test.go

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"math"
1010
"math/rand"
1111
"net"
12+
"net/http"
1213
"os"
1314
"os/exec"
1415
"path/filepath"
@@ -145,8 +146,8 @@ func verifySessionStopped(t *testing.T, session *Session) {
145146
t.Error("session must always have a set connection")
146147
}
147148
verifyConnStopped(t, session.conn)
148-
if session.debugger != nil {
149-
t.Error("session should have no pointer to debugger after shutdown")
149+
if !session.disconnected {
150+
t.Error("session should have disconnected set after shutdown")
150151
}
151152
if session.binaryToRemove != "" {
152153
t.Error("session should have no binary to remove after shutdown")
@@ -234,8 +235,9 @@ func TestSessionStop(t *testing.T) {
234235
if binaryToRemoveSet && s.binaryToRemove == "" || !binaryToRemoveSet && s.binaryToRemove != "" {
235236
t.Errorf("binaryToRemove: got %s, want set=%v", s.binaryToRemove, binaryToRemoveSet)
236237
}
237-
if debuggerSet && s.debugger == nil || !debuggerSet && s.debugger != nil {
238-
t.Errorf("debugger: got %v, want set=%v", s.debugger, debuggerSet)
238+
connected := s.debugger != nil && !s.disconnected
239+
if debuggerSet != connected {
240+
t.Errorf("debugger: got debugger!=nil: %v disconnected: %v, want set=%v", s.debugger != nil, s.disconnected, debuggerSet)
239241
}
240242
if disconnectChanSet && s.config.DisconnectChan == nil || !disconnectChanSet && s.config.DisconnectChan != nil {
241243
t.Errorf("disconnectChan: got %v, want set=%v", s.config.DisconnectChan, disconnectChanSet)
@@ -7694,6 +7696,74 @@ func TestRedirect(t *testing.T) {
76947696
})
76957697
}
76967698

7699+
func TestBreakpointAfterDisconnect(t *testing.T) {
7700+
fixture := protest.BuildFixture(t, "testnextnethttp", protest.AllNonOptimized)
7701+
7702+
cmd := exec.Command(fixture.Path)
7703+
cmd.Stdout = os.Stdout
7704+
cmd.Stderr = os.Stderr
7705+
if err := cmd.Start(); err != nil {
7706+
t.Fatal(err)
7707+
}
7708+
7709+
var server MultiClientCloseServerMock
7710+
server.stopped = make(chan struct{})
7711+
server.impl, server.forceStop = startDAPServer(t, false, server.stopped)
7712+
7713+
dbg, err := debugger.New(&debugger.Config{Backend: "default", AttachPid: cmd.Process.Pid}, nil)
7714+
if err != nil {
7715+
t.Fatalf("debugger.New: %v", err)
7716+
}
7717+
7718+
server.debugger = dbg
7719+
7720+
client := server.acceptNewClient(t)
7721+
7722+
client.InitializeRequest()
7723+
client.ExpectInitializeResponse(t)
7724+
7725+
client.SetBreakpointsRequestWithArgs(fixture.Source, []int{16}, nil, nil, map[int]string{16: "{w}"})
7726+
client.ExpectSetBreakpointsResponse(t)
7727+
7728+
client.ContinueRequest(1)
7729+
client.ExpectContinueResponse(t)
7730+
7731+
client.DisconnectRequestWithKillOption(false)
7732+
client.ExpectOutputEvent(t)
7733+
client.ExpectDisconnectResponse(t)
7734+
client.Close()
7735+
7736+
time.Sleep(200 * time.Millisecond)
7737+
7738+
server.impl.session.conn = &connection{ReadWriteCloser: discard{}} // fake a race condition between onDisconnectRequest and the runUntilStopAndNotify goroutine
7739+
7740+
httpClient := &http.Client{Timeout: time.Second}
7741+
7742+
_, err = httpClient.Get("http://127.0.0.1:9191/nobp")
7743+
if err != nil {
7744+
t.Fatalf("Page request after disconnect failed: %v", err)
7745+
}
7746+
7747+
time.Sleep(200 * time.Millisecond)
7748+
7749+
cmd.Process.Kill()
7750+
}
7751+
7752+
type discard struct {
7753+
}
7754+
7755+
func (discard) Read([]byte) (int, error) {
7756+
return 0, nil
7757+
}
7758+
7759+
func (discard) Close() error {
7760+
return nil
7761+
}
7762+
7763+
func (discard) Write(buf []byte) (int, error) {
7764+
return len(buf), nil
7765+
}
7766+
76977767
// Helper functions for checking ErrorMessage field values.
76987768

76997769
func checkErrorMessageId(er *dap.ErrorMessage, id int) bool {

0 commit comments

Comments
 (0)