pkg/proc/native/linux: fix target crashes induced by RequestManualStop (#2484)

A RequestManualStop received while the target program is stopped can
induce a crash when the target is restarted.
This is caused by the phantom breakpoint detection that was introduced
in PR #2179 / commit e69d536.
Instead of always interpreting an unexplained SIGTRAP as a phantom
breakpoint memorize all possible unreported breakpoint hits and only
act on it when the thread hasn't moved from one.

Also clarifies the behavior of the halt command when it is received
while the target is stopped or in the process of stopping.
This commit is contained in:
Alessandro Arzilli 2021-05-17 18:56:42 +02:00 committed by GitHub
parent bd2a4fe56e
commit d2bca7a307
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 17 deletions

@ -1,30 +1,30 @@
Tests skipped by each supported backend:
* 386 skipped = 4% (6/149)
* 386 skipped = 4% (6/150)
* 1 broken
* 3 broken - cgo stacktraces
* 2 not implemented
* arm64 skipped = 3.4% (5/149)
* arm64 skipped = 3.3% (5/150)
* 1 broken
* 1 broken - cgo stacktraces
* 1 broken - global variable symbolication
* 2 not implemented
* darwin skipped = 1.3% (2/149)
* darwin skipped = 1.3% (2/150)
* 2 not implemented
* darwin/arm64 skipped = 0.67% (1/149)
* darwin/arm64 skipped = 0.67% (1/150)
* 1 broken - cgo stacktraces
* darwin/lldb skipped = 0.67% (1/149)
* darwin/lldb skipped = 0.67% (1/150)
* 1 upstream issue
* freebsd skipped = 9.4% (14/149)
* freebsd skipped = 9.3% (14/150)
* 11 broken
* 3 not implemented
* linux/386/pie skipped = 0.67% (1/149)
* linux/386/pie skipped = 0.67% (1/150)
* 1 broken
* pie skipped = 0.67% (1/149)
* pie skipped = 0.67% (1/150)
* 1 upstream issue - https://github.com/golang/go/issues/29322
* rr skipped = 1.3% (2/149)
* rr skipped = 1.3% (2/150)
* 2 not implemented
* windows skipped = 2.7% (4/149)
* windows skipped = 2.7% (4/150)
* 1 broken
* 2 not implemented
* 1 upstream issue

@ -576,12 +576,28 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
switchTrapthread := false
// set breakpoints on SIGTRAP threads
var err1 error
for _, th := range dbp.threads {
if th.CurrentBreakpoint.Breakpoint == nil && th.os.setbp {
if err := th.SetCurrentBreakpoint(true); err != nil {
return nil, err
pc, _ := th.PC()
if !th.os.setbp && pc != th.os.phantomBreakpointPC {
// check if this could be a breakpoint hit anyway that the OS hasn't notified us about, yet.
if _, ok := dbp.FindBreakpoint(pc, dbp.BinInfo().Arch.BreakInstrMovesPC()); ok {
th.os.phantomBreakpointPC = pc
}
}
if pc != th.os.phantomBreakpointPC {
th.os.phantomBreakpointPC = 0
}
if th.CurrentBreakpoint.Breakpoint == nil && th.os.setbp {
if err := th.SetCurrentBreakpoint(true); err != nil {
err1 = err
continue
}
}
if th.CurrentBreakpoint.Breakpoint == nil && th.os.setbp && (th.Status != nil) && ((*sys.WaitStatus)(th.Status).StopSignal() == sys.SIGTRAP) && dbp.BinInfo().Arch.BreakInstrMovesPC() {
manualStop := false
if th.ThreadID() == trapthread.ThreadID() {
@ -589,7 +605,7 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
manualStop = dbp.manualStopRequested
dbp.stopMu.Unlock()
}
if !manualStop {
if !manualStop && th.os.phantomBreakpointPC == pc {
// Thread received a SIGTRAP but we don't have a breakpoint for it and
// it wasn't sent by a manual stop request. It's either a hardcoded
// breakpoint or a phantom breakpoint hit (a breakpoint that was hit but
@ -624,6 +640,9 @@ func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error)
}
}
}
if err1 != nil {
return nil, err1
}
if switchTrapthread {
trapthreadID := trapthread.ID

@ -13,9 +13,10 @@ type waitStatus sys.WaitStatus
// osSpecificDetails hold Linux specific
// process details.
type osSpecificDetails struct {
delayedSignal int
running bool
setbp bool
delayedSignal int
running bool
setbp bool
phantomBreakpointPC uint64
}
func (t *nativeThread) stop() (err error) {

@ -5307,3 +5307,51 @@ func TestWatchpointCounts(t *testing.T) {
}
})
}
func TestManualStopWhileStopped(t *testing.T) {
// Checks that RequestManualStop sent to a stopped thread does not cause the target process to die.
withTestProcess("loopprog", t, func(p *proc.Target, fixture protest.Fixture) {
asyncCont := func(done chan struct{}) {
defer close(done)
err := p.Continue()
t.Logf("%v\n", err)
if err != nil {
panic(err)
}
for _, th := range p.ThreadList() {
if th.Breakpoint().Breakpoint != nil {
t.Logf("unexpected stop at breakpoint: %v", th.Breakpoint().Breakpoint)
panic("unexpected stop at breakpoint")
}
}
}
const (
repeatsSlow = 3
repeatsFast = 5
)
for i := 0; i < repeatsSlow; i++ {
t.Logf("Continue %d (slow)", i)
done := make(chan struct{})
go asyncCont(done)
time.Sleep(1 * time.Second)
p.RequestManualStop()
time.Sleep(1 * time.Second)
p.RequestManualStop()
time.Sleep(1 * time.Second)
<-done
}
for i := 0; i < repeatsFast; i++ {
t.Logf("Continue %d (fast)", i)
rch := make(chan struct{})
done := make(chan struct{})
p.ResumeNotify(rch)
go asyncCont(done)
<-rch
p.RequestManualStop()
p.RequestManualStop()
<-done
}
})
}

@ -414,6 +414,9 @@ const (
// SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine
SwitchGoroutine = "switchGoroutine"
// Halt suspends the process.
// The effect of Halt while the target process is stopped, or in the
// process of stopping, is operating system and timing dependent. It will
// either have no effect or cause the following resume to stop immediately.
Halt = "halt"
// Call resumes process execution injecting a function call.
Call = "call"