fix(debug): target the traced thread and keep the kill from ever blocking

The Go runtime can migrate the debuggee's target-mode goroutine off the
process leader before PTRACE_TRACEME, which left the trace relation on a
thread the session never addressed: its stops starved the waits on the
leader, and a kill sequence that resumed nothing and then blocked in
Wait4 hung the whole package.  The debuggee now reports the traced thread
in the launch handshake and parks with a thread-directed stop, every
ptrace request and wait addresses that thread, a SIGURG arriving on a
single-step resumes it as a single-step again instead of letting the
tracee run uncontrolled, a resume rejected with ESRCH lifts a group-stop
with SIGCONT and retries once, and Kill resumes, kills and reaps through
non-blocking waits so it returns for a tracee in any state.

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:46:18 +02:00
1 parent 409c8b348d
commit b5d6f1b46a
14 files changed
+265 -80

No files matched your search

+9 -9
View File
@@ -36,7 +36,7 @@ const (
// read, or the next hit on a different slot would still see this slot's bit
// set and report this slot's address again.
func archWatchpointAddr(s *Session, siAddr uint64) uint64 {
dr6, err := ptracePeekUser(s.pid, dr6Off)
dr6, err := ptracePeekUser(s.tid, dr6Off)
if err != nil {
return siAddr
}
@@ -46,10 +46,10 @@ func archWatchpointAddr(s *Session, siAddr uint64) uint64 {
}
// Best effort: the write-back only fails for a debuggee that died, in
// which case no further watchpoint can fire anyway.
_ = ptracePokeUser(s.pid, dr6Off, dr6&^0xF)
_ = ptracePokeUser(s.tid, dr6Off, dr6&^0xF)
for slot := range 4 {
if status&(1<<slot) != 0 {
addr, err := ptracePeekUser(s.pid, drOffset+uintptr(slot*8))
addr, err := ptracePeekUser(s.tid, drOffset+uintptr(slot*8))
if err == nil && addr != 0 {
return addr
}
@@ -112,11 +112,11 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
}
if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), addr); err != nil {
if err := ptracePokeUser(s.tid, drOffset+uintptr(slot*8), addr); err != nil {
return fmt.Errorf("debug: set DR%d: %w", slot, err)
}
dr7, err := ptracePeekUser(s.pid, dr7Off)
dr7, err := ptracePeekUser(s.tid, dr7Off)
if err != nil {
return fmt.Errorf("debug: read DR7: %w", err)
}
@@ -128,7 +128,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil {
if err := ptracePokeUser(s.tid, dr7Off, dr7); err != nil {
return fmt.Errorf("debug: set DR7: %w", err)
}
s.wpSlots[slot] = true
@@ -143,17 +143,17 @@ func (s *Session) ClearWatchpoint(slot int) error {
if !s.wpSlots[slot] {
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
}
dr7, err := ptracePeekUser(s.pid, dr7Off)
dr7, err := ptracePeekUser(s.tid, dr7Off)
if err != nil {
return err
}
dr7 &^= uint64(1) << (2 * slot)
if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil {
if err := ptracePokeUser(s.tid, dr7Off, dr7); err != nil {
return err
}
// Zero the address register too: a stale address in a disabled slot
// turns any sticky DR6 bit into a misattributed watchpoint report.
if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), 0); err != nil {
if err := ptracePokeUser(s.tid, drOffset+uintptr(slot*8), 0); err != nil {
return err
}
s.wpSlots[slot] = false