proc: fix signal handling during stepping (#2803)

Fix signal handling during thread single stepping so that signals that
are generated by executing the current instruction are immediately
propagated to the inferior, while signals other signals sent to the
thread are delayed until the full resume happens.

Fixes a bug where a breakpoint set on an instruction that causes a
SIGSEGV would make Delve hang and a bug where signals received during
single step would make it look like an instruction is executed twice.

Fixes #2801
Fixes #2792
This commit is contained in:
Alessandro Arzilli 2021-12-07 18:21:53 +01:00 committed by GitHub
parent 2cd9d268d3
commit b8a9ae26f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 24 deletions

@ -0,0 +1,9 @@
package main
import "fmt"
func asmFunc(*int) int
func main() {
fmt.Printf("%d\n", asmFunc(nil))
}

@ -0,0 +1,7 @@
#include "textflag.h"
TEXT ·asmFunc(SB),0,$0-16
MOVL arg+0(FP), AX
MOVL (AX), AX
MOVL AX, ret+4(FP)
RET

@ -0,0 +1,7 @@
#include "textflag.h"
TEXT ·asmFunc(SB),0,$0-16
MOVQ arg+0(FP), AX
MOVQ (AX), AX
MOVQ AX, ret+8(FP)
RET

@ -0,0 +1,7 @@
#include "textflag.h"
TEXT ·asmFunc(SB),0,$0-16
MOVD arg+0(FP), R5
MOVD (R5), R5
MOVD R5, ret+8(FP)
RET

@ -232,6 +232,7 @@ func newProcess(process *os.Process) *gdbProcess {
direction: proc.Forward,
log: logger,
goarch: runtime.GOARCH,
goos: runtime.GOOS,
},
threads: make(map[int]*gdbThread),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
@ -790,6 +791,9 @@ const (
childSignal = 0x11
stopSignal = 0x13
_SIGILL = 0x4 // ok
_SIGFPE = 0x8 // ok
debugServerTargetExcBadAccess = 0x91
debugServerTargetExcBadInstruction = 0x92
debugServerTargetExcArithmetic = 0x93
@ -880,21 +884,7 @@ continueLoop:
return nil, stopReason, fmt.Errorf("could not find thread %s", threadID)
}
var err error
switch trapthread.sig {
case 0x91:
err = errors.New("bad access")
case 0x92:
err = errors.New("bad instruction")
case 0x93:
err = errors.New("arithmetic exception")
case 0x94:
err = errors.New("emulation exception")
case 0x95:
err = errors.New("software exception")
case 0x96:
err = errors.New("breakpoint exception")
}
err := machTargetExcToError(trapthread.sig)
if err != nil {
// the signals that are reported here can not be propagated back to the target process.
trapthread.sig = 0
@ -1522,7 +1512,7 @@ func (t *gdbThread) StepInstruction() error {
// Reset thread registers so the next call to
// Thread.Registers will not be cached.
t.regs.regs = nil
return t.p.conn.step(t.strID, &threadUpdater{p: t.p}, false)
return t.p.conn.step(t, &threadUpdater{p: t.p}, false)
}
// Blocked returns true if the thread is blocked in runtime or kernel code.
@ -1784,7 +1774,7 @@ func (t *gdbThread) reloadGAtPC() error {
}
}()
err = t.p.conn.step(t.strID, nil, true)
err = t.p.conn.step(t, nil, true)
if err != nil {
if err == errThreadBlocked {
t.regs.tls = 0
@ -1837,7 +1827,7 @@ func (t *gdbThread) reloadGAlloc() error {
}
}()
err = t.p.conn.step(t.strID, nil, true)
err = t.p.conn.step(t, nil, true)
if err != nil {
if err == errThreadBlocked {
t.regs.tls = 0
@ -2056,3 +2046,21 @@ func registerName(arch *proc.Arch, regNum uint64) string {
regName, _, _ := arch.DwarfRegisterToString(int(regNum), nil)
return strings.ToLower(regName)
}
func machTargetExcToError(sig uint8) error {
switch sig {
case 0x91:
return errors.New("bad access")
case 0x92:
return errors.New("bad instruction")
case 0x93:
return errors.New("arithmetic exception")
case 0x94:
return errors.New("emulation exception")
case 0x95:
return errors.New("software exception")
case 0x96:
return errors.New("breakpoint exception")
}
return nil
}

@ -48,6 +48,7 @@ type gdbConn struct {
isDebugserver bool // true if the stub is debugserver
xcmdok bool // x command can be used to transfer memory
goarch string
goos string
useXcmd bool // forces writeMemory to use the 'X' command
@ -603,7 +604,8 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stop
}
// step executes a 'vCont' command on the specified thread with 's' action.
func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal bool) error {
func (conn *gdbConn) step(th *gdbThread, tu *threadUpdater, ignoreFaultSignal bool) error {
threadID := th.strID
if conn.direction != proc.Forward {
if err := conn.selectThread('c', threadID, "step"); err != nil {
return err
@ -616,6 +618,17 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
_, err := conn.waitForvContStop("singlestep", threadID, tu)
return err
}
var _SIGBUS uint8
switch conn.goos {
case "linux":
_SIGBUS = 0x7
case "darwin":
_SIGBUS = 0xa
default:
panic(fmt.Errorf("unknown GOOS %s", conn.goos))
}
var sig uint8 = 0
for {
conn.outbuf.Reset()
@ -640,6 +653,8 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
if ignoreFaultSignal { // we attempting to read the TLS, a fault here should be ignored
return nil
}
case _SIGILL, _SIGBUS, _SIGFPE:
// propagate these signals to inferior immediately
case interruptSignal, breakpointSignal, stopSignal:
return nil
case childSignal: // stop on debugserver but SIGCHLD on lldb-server/linux
@ -647,9 +662,15 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
return nil
}
case debugServerTargetExcBadAccess, debugServerTargetExcBadInstruction, debugServerTargetExcArithmetic, debugServerTargetExcEmulation, debugServerTargetExcSoftware, debugServerTargetExcBreakpoint:
return nil
if ignoreFaultSignal {
return nil
}
return machTargetExcToError(sig)
default:
// delay propagation of any other signal to until after the stepping is done
th.sig = sig
sig = 0
}
// any other signal is propagated to the inferior
}
}

@ -24,3 +24,12 @@ func ptraceDetach(tid, sig int) error {
func ptraceCont(tid, sig int) error {
return sys.PtraceCont(tid, sig)
}
// ptraceSingleStep executes ptrace PTRACE_SINGLESTEP
func ptraceSingleStep(pid, sig int) error {
_, _, e1 := sys.Syscall6(sys.SYS_PTRACE, uintptr(sys.PTRACE_SINGLESTEP), uintptr(pid), uintptr(0), uintptr(sig), 0, 0)
if e1 != 0 {
return e1
}
return nil
}

@ -48,8 +48,10 @@ func (t *nativeThread) resumeWithSig(sig int) (err error) {
}
func (t *nativeThread) singleStep() (err error) {
sig := 0
for {
t.dbp.execPtraceFunc(func() { err = sys.PtraceSingleStep(t.ID) })
t.dbp.execPtraceFunc(func() { err = ptraceSingleStep(t.ID, sig) })
sig = 0
if err != nil {
return err
}
@ -65,8 +67,19 @@ func (t *nativeThread) singleStep() (err error) {
}
return proc.ErrProcessExited{Pid: t.dbp.pid, Status: rs}
}
if wpid == t.ID && status.StopSignal() == sys.SIGTRAP {
return nil
if wpid == t.ID {
switch s := status.StopSignal(); s {
case sys.SIGTRAP:
return nil
case sys.SIGSTOP:
// delayed SIGSTOP, ignore it
case sys.SIGILL, sys.SIGBUS, sys.SIGFPE, sys.SIGSEGV, sys.SIGSTKFLT:
// propagate signals that can have been caused by the current instruction
sig = int(s)
default:
// delay propagation of all other signals
t.os.delayedSignal = int(s)
}
}
}
}

@ -5736,3 +5736,42 @@ func TestSetYMMRegister(t *testing.T) {
}
})
}
func TestNilPtrDerefInBreakInstr(t *testing.T) {
// Checks that having a breakpoint on the exact instruction that causes a
// nil pointer dereference does not cause problems.
var asmfile string
switch runtime.GOARCH {
case "amd64":
asmfile = "main_amd64.s"
case "arm64":
asmfile = "main_arm64.s"
case "386":
asmfile = "main_386.s"
default:
t.Fatalf("assembly file for %s not provided", runtime.GOARCH)
}
withTestProcess("asmnilptr/", t, func(p *proc.Target, fixture protest.Fixture) {
f := filepath.Join(fixture.BuildDir, asmfile)
f = strings.Replace(f, "\\", "/", -1)
setFileBreakpoint(p, t, f, 5)
t.Logf("first continue")
assertNoError(p.Continue(), t, "Continue()")
t.Logf("second continue")
err := p.Continue()
if runtime.GOOS == "darwin" && err != nil && err.Error() == "bad access" {
// this is also ok
return
}
assertNoError(err, t, "Continue()")
bp := p.CurrentThread().Breakpoint()
if bp != nil {
t.Logf("%#v\n", bp.Breakpoint)
}
if bp == nil || (bp.Name != proc.UnrecoveredPanic) {
t.Fatalf("no breakpoint hit or wrong breakpoint hit: %#v", bp)
}
})
}