Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 127 additions & 3 deletions pkg/monitor/ptrace/ptrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"strconv"
"strings"
"syscall"
"unsafe"

"github.com/armon/go-radix"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -724,9 +725,13 @@ func (app *App) collect() {
wpid, err := syscall.Wait4(waitFor, &ws, syscall.WALL, nil)
if err != nil {
if err.(syscall.Errno) == syscall.ECHILD {
logger.Debug("wait4 ECHILD error (ignoring)")
doSyscall = false
continue
// No tracees left to wait for (we missed their exit events
// or they were reaped elsewhere). Nothing else will ever
// show up here, so finish instead of spinning on wait4.
logger.Debugf("[%d/%d]: wait4 ECHILD - no more tracees (tracked=%d) - done",
app.cmd.Process.Pid, app.pgid, len(pidSyscallState))
app.collectorDoneCh <- 0
return
}

logger.Debugf("wait4 error - %v (errno=%d)", err, err.(syscall.Errno))
Expand Down Expand Up @@ -777,6 +782,9 @@ func (app *App) collect() {
eventCode = ws.TrapCause()
} else {
callSig = statusCode
if isStopSignal(callSig) && !app.shouldInjectStopSignal(wpid, callSig) {
callSig = 0
}
}
}

Expand Down Expand Up @@ -836,6 +844,10 @@ func (app *App) collect() {
if err != nil {
logger.Debugf("[%d/%d]: wpid=%v onSyscall error - %v",
app.cmd.Process.Pid, app.pgid, wpid, err)
// Still need to resume wpid (not the previous callPid),
// otherwise it stays stopped forever.
doSyscall = true
callPid = wpid
continue
}

Expand All @@ -861,6 +873,10 @@ func (app *App) collect() {
if err := onSyscallReturn(wpid, cstate); err != nil {
logger.Debugf("[%d/%d]: wpid=%v onSyscallReturn error - %v",
app.cmd.Process.Pid, app.pgid, wpid, err)
// Still need to resume wpid (not the previous callPid),
// otherwise it stays stopped forever.
doSyscall = true
callPid = wpid
continue
}
}
Expand Down Expand Up @@ -953,6 +969,52 @@ func (app *App) collect() {
} // eof: main for loop
}

// shouldInjectStopSignal decides whether a stop signal (SIGSTOP, SIGTSTP,
// SIGTTIN, SIGTTOU) reported by wait4 for a stopped tracee must be delivered
// to the tracee when it's resumed.
//
// A tracee reports a stop with one of these signals in three situations:
//
// 1. A newly cloned/forked tracee reports its initial, kernel-generated
// SIGSTOP (PTRACE_O_TRACECLONE/FORK/VFORK auto-attach).
// 2. The thread group is in a group-stop and every traced thread
// reports it (PTRACE_GETSIGINFO fails with EINVAL in this case).
// 3. Some process actually sent the signal (e.g., `kill -STOP`).
//
// Only (3) is a real signal that should be delivered. Re-injecting it in
// (1) or (2) delivers a SIGSTOP the target never asked for and puts
// the whole thread group into a group-stop. The traced threads get resumed
// by the next PTRACE_SYSCALL (group-stops aren't honored without PTRACE_SEIZE),
// but the untraced threads in the group - e.g., the kernel's io_uring
// SQPOLL/worker threads (iou-sqp-*, iou-wrk-*) that Node.js/libuv relies on -
// stay stopped until a SIGCONT arrives, and the app hangs forever waiting for them.
func (app *App) shouldInjectStopSignal(pid int, sig int) bool {
logger := app.logger.WithField("op", "shouldInjectStopSignal")

si, err := ptraceGetSigInfo(pid)
if err != nil {
if err == syscall.EINVAL {
logger.Tracef("[%d/%d]: pid=%v sig=%v - group-stop notification - not injecting",
app.cmd.Process.Pid, app.pgid, pid, sig)
} else {
logger.Debugf("[%d/%d]: pid=%v sig=%v - PTRACE_GETSIGINFO error - %v - not injecting",
app.cmd.Process.Pid, app.pgid, pid, sig, err)
}

return false
}

if si.Code == siUser && si.Pid == 0 {
logger.Tracef("[%d/%d]: pid=%v sig=%v - initial ptrace attach stop - not injecting",
app.cmd.Process.Pid, app.pgid, pid, sig)
return false
}

logger.Debugf("[%d/%d]: pid=%v sig=%v - real stop signal (si_code=%d si_pid=%d si_uid=%d) - injecting",
app.cmd.Process.Pid, app.pgid, pid, sig, si.Code, si.Pid, si.Uid)
return true
}

func (app *App) startSignalForwarding() context.CancelFunc {
logger := app.logger.WithField("op", "startSignalForwarding")
logger.Debug("call")
Expand Down Expand Up @@ -1512,3 +1574,65 @@ var sigEnums = [...]string{
syscall.SIGXCPU: "SIGXCPU",
syscall.SIGXFSZ: "SIGXFSZ",
}

///////////////////////////////////

// siUser is the si_code value (SI_USER) for signals sent by kill(2)/raise(3).
// The kernel also uses it for the SIGSTOP it queues on a freshly auto-attached
// tracee (PTRACE_O_TRACECLONE/FORK/VFORK), but with si_pid == 0 in that case.
const siUser = 0

// ptraceSigInfo is the subset of the kernel siginfo_t the tracer cares about.
type ptraceSigInfo struct {
Signo int32
Errno int32
Code int32
Pid int32 // si_pid - sending process (0 when generated by the kernel)
Uid uint32 // si_uid - sending process' real UID
}

// ptraceGetSigInfo returns the siginfo for the signal that stopped the tracee.
// It fails with EINVAL if the tracee is in a group-stop rather than
// in a signal-delivery-stop (see ptrace(2)).
func ptraceGetSigInfo(pid int) (*ptraceSigInfo, error) {
// siginfo_t is 128 bytes on every Linux architecture.
var buf [128]byte
_, _, errno := unix.Syscall6(
unix.SYS_PTRACE,
unix.PTRACE_GETSIGINFO,
uintptr(pid),
0,
uintptr(unsafe.Pointer(&buf[0])),
0, 0)
if errno != 0 {
return nil, errno
}

// Generic siginfo_t layout: si_signo, si_errno, si_code (3 x int32),
// followed by the _sifields union aligned to the native word size
// (offset 16 on 64-bit targets, 12 on 32-bit ones).
// For SI_USER the union starts with si_pid (int32) and si_uid (uint32).
fieldsOff := 12
if unsafe.Sizeof(uintptr(0)) == 8 {
fieldsOff = 16
}

return &ptraceSigInfo{
Signo: *(*int32)(unsafe.Pointer(&buf[0])),
Errno: *(*int32)(unsafe.Pointer(&buf[4])),
Code: *(*int32)(unsafe.Pointer(&buf[8])),
Pid: *(*int32)(unsafe.Pointer(&buf[fieldsOff])),
Uid: *(*uint32)(unsafe.Pointer(&buf[fieldsOff+4])),
}, nil
}

// isStopSignal reports whether sig is one of the job control signals
// whose default action is to stop the whole thread group.
func isStopSignal(sig int) bool {
switch syscall.Signal(sig) {
case syscall.SIGSTOP, syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU:
return true
}

return false
}
Loading