delve/pkg/proc/native/proc_linux.go
Alessandro Arzilli 4ed41e9060
proc: fix TestIssue1101 flake (#3585)
If the target process exits and receives a signal at the same time and
we receive the signal first we should call waitpid again to read the
target's exit status.

This also fixes a nil pointer dereference when trapWaitInternal returns
an error, this fix is probably incomplete but I wasn't able to
reproduce its circumstances after 30000 runs of TestIssue1101 to
properly address it.
2023-11-27 08:58:27 -08:00

1017 lines
27 KiB
Go

package native
import (
"bufio"
"bytes"
"debug/elf"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
sys "golang.org/x/sys/unix"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
"github.com/go-delve/delve/pkg/proc/linutil"
isatty "github.com/mattn/go-isatty"
)
// Process statuses
const (
statusSleeping = 'S'
statusRunning = 'R'
statusTraceStop = 't'
statusZombie = 'Z'
// Kernel 2.6 has TraceStop as T
// TODO(derekparker) Since this means something different based on the
// version of the kernel ('T' is job control stop on modern 3.x+ kernels) we
// may want to differentiate at some point.
statusTraceStopT = 'T'
personalityGetPersonality = 0xffffffff // argument to pass to personality syscall to get the current personality
_ADDR_NO_RANDOMIZE = 0x0040000 // ADDR_NO_RANDOMIZE linux constant
)
// osProcessDetails contains Linux specific
// process details.
type osProcessDetails struct {
comm string
ebpf *ebpf.EBPFContext
}
func (os *osProcessDetails) Close() {
if os.ebpf != nil {
os.ebpf.Close()
}
}
// Launch creates and begins debugging a new process. First entry in
// `cmd` is the program to run, and then rest are the arguments
// to be supplied to that process. `wd` is working directory of the program.
// If the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in.
func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect) (*proc.TargetGroup, error) {
var (
process *exec.Cmd
err error
)
foreground := flags&proc.LaunchForeground != 0
stdin, stdout, stderr, closefn, err := openRedirects(stdinPath, stdoutOR, stderrOR, foreground)
if err != nil {
return nil, err
}
if stdin == nil || !isatty.IsTerminal(stdin.Fd()) {
// exec.(*Process).Start will fail if we try to send a process to
// foreground but we are not attached to a terminal.
foreground = false
}
dbp := newProcess(0)
defer func() {
if err != nil && dbp.pid != 0 {
_ = detachWithoutGroup(dbp, true)
}
}()
dbp.execPtraceFunc(func() {
if flags&proc.LaunchDisableASLR != 0 {
oldPersonality, _, err := syscall.Syscall(sys.SYS_PERSONALITY, personalityGetPersonality, 0, 0)
if err == syscall.Errno(0) {
newPersonality := oldPersonality | _ADDR_NO_RANDOMIZE
syscall.Syscall(sys.SYS_PERSONALITY, newPersonality, 0, 0)
defer syscall.Syscall(sys.SYS_PERSONALITY, oldPersonality, 0, 0)
}
}
process = exec.Command(cmd[0])
process.Args = cmd
process.Stdin = stdin
process.Stdout = stdout
process.Stderr = stderr
process.SysProcAttr = &syscall.SysProcAttr{
Ptrace: true,
Setpgid: true,
Foreground: foreground,
}
if foreground {
signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN)
}
if tty != "" {
dbp.ctty, err = attachProcessToTTY(process, tty)
if err != nil {
return
}
}
if wd != "" {
process.Dir = wd
}
err = process.Start()
})
closefn()
if err != nil {
return nil, err
}
dbp.pid = process.Process.Pid
dbp.childProcess = true
_, _, err = dbp.wait(process.Process.Pid, 0)
if err != nil {
return nil, fmt.Errorf("waiting for target execve failed: %s", err)
}
tgt, err := dbp.initialize(cmd[0], debugInfoDirs)
if err != nil {
return nil, err
}
return tgt, nil
}
// Attach to an existing process with the given PID. Once attached, if
// the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in.
func Attach(pid int, waitFor *proc.WaitFor, debugInfoDirs []string) (*proc.TargetGroup, error) {
if waitFor.Valid() {
var err error
pid, err = WaitFor(waitFor)
if err != nil {
return nil, err
}
}
dbp := newProcess(pid)
var err error
dbp.execPtraceFunc(func() { err = ptraceAttach(dbp.pid) })
if err != nil {
return nil, err
}
_, _, err = dbp.wait(dbp.pid, 0)
if err != nil {
return nil, err
}
tgt, err := dbp.initialize(findExecutable("", dbp.pid), debugInfoDirs)
if err != nil {
_ = detachWithoutGroup(dbp, false)
return nil, err
}
// ElfUpdateSharedObjects can only be done after we initialize because it
// needs an initialized BinaryInfo object to work.
err = linutil.ElfUpdateSharedObjects(dbp)
if err != nil {
return nil, err
}
return tgt, nil
}
func isProcDir(name string) bool {
for _, ch := range name {
if ch < '0' || ch > '9' {
return false
}
}
return true
}
func waitForSearchProcess(pfx string, seen map[int]struct{}) (int, error) {
log := logflags.DebuggerLogger()
des, err := os.ReadDir("/proc")
if err != nil {
log.Errorf("error reading proc: %v", err)
return 0, nil
}
for _, de := range des {
if !de.IsDir() {
continue
}
name := de.Name()
if !isProcDir(name) {
continue
}
pid, _ := strconv.Atoi(name)
if _, isseen := seen[pid]; isseen {
continue
}
seen[pid] = struct{}{}
buf, err := os.ReadFile(filepath.Join("/proc", name, "cmdline"))
if err != nil {
// probably we just don't have permissions
continue
}
for i := range buf {
if buf[i] == 0 {
buf[i] = ' '
}
}
log.Debugf("waitfor: new process %q", string(buf))
if strings.HasPrefix(string(buf), pfx) {
return pid, nil
}
}
return 0, nil
}
func initialize(dbp *nativeProcess) (string, error) {
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", dbp.pid))
if err == nil {
// removes newline character
comm = bytes.TrimSuffix(comm, []byte("\n"))
}
if comm == nil || len(comm) <= 0 {
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", dbp.pid))
if err != nil {
return "", fmt.Errorf("could not read proc stat: %v", err)
}
expr := fmt.Sprintf("%d\\s*\\((.*)\\)", dbp.pid)
rexp, err := regexp.Compile(expr)
if err != nil {
return "", fmt.Errorf("regexp compile error: %v", err)
}
match := rexp.FindSubmatch(stat)
if match == nil {
return "", fmt.Errorf("no match found using regexp '%s' in /proc/%d/stat", expr, dbp.pid)
}
comm = match[1]
}
dbp.os.comm = strings.ReplaceAll(string(comm), "%", "%%")
return getCmdLine(dbp.pid), nil
}
func (dbp *nativeProcess) GetBufferedTracepoints() []ebpf.RawUProbeParams {
if dbp.os.ebpf == nil {
return nil
}
return dbp.os.ebpf.GetBufferedTracepoints()
}
// kill kills the target process.
func (procgrp *processGroup) kill(dbp *nativeProcess) error {
if ok, _ := dbp.Valid(); !ok {
return nil
}
if !dbp.threads[dbp.pid].Stopped() {
return errors.New("process must be stopped in order to kill it")
}
if err := sys.Kill(-dbp.pid, sys.SIGKILL); err != nil {
return errors.New("could not deliver signal " + err.Error())
}
// wait for other threads first or the thread group leader (dbp.pid) will never exit.
for threadID := range dbp.threads {
if threadID != dbp.pid {
dbp.wait(threadID, 0)
}
}
for {
wpid, status, err := dbp.wait(dbp.pid, 0)
if err != nil {
return err
}
if wpid == dbp.pid && status != nil && status.Signaled() && status.Signal() == sys.SIGKILL {
dbp.postExit()
return err
}
}
}
func (dbp *nativeProcess) requestManualStop() (err error) {
return sys.Kill(dbp.pid, sys.SIGTRAP)
}
const (
ptraceOptionsNormal = syscall.PTRACE_O_TRACECLONE
ptraceOptionsFollowExec = syscall.PTRACE_O_TRACECLONE | syscall.PTRACE_O_TRACEVFORK | syscall.PTRACE_O_TRACEEXEC
)
// Attach to a newly created thread, and store that thread in our list of
// known threads.
func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error) {
if thread, ok := dbp.threads[tid]; ok {
return thread, nil
}
ptraceOptions := ptraceOptionsNormal
if dbp.followExec {
ptraceOptions = ptraceOptionsFollowExec
}
var err error
if attach {
dbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) })
if err != nil && err != sys.EPERM {
// Do not return err if err == EPERM,
// we may already be tracing this thread due to
// PTRACE_O_TRACECLONE. We will surely blow up later
// if we truly don't have permissions.
return nil, fmt.Errorf("could not attach to new thread %d %s", tid, err)
}
pid, status, err := dbp.waitFast(tid)
if err != nil {
return nil, err
}
if status.Exited() {
return nil, fmt.Errorf("thread already exited %d", pid)
}
}
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH {
if _, _, err = dbp.waitFast(tid); err != nil {
return nil, fmt.Errorf("error while waiting after adding thread: %d %s", tid, err)
}
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH {
return nil, err
}
if err != nil {
return nil, fmt.Errorf("could not set options for new traced thread %d %s", tid, err)
}
}
dbp.threads[tid] = &nativeThread{
ID: tid,
dbp: dbp,
os: new(osSpecificDetails),
}
if dbp.memthread == nil {
dbp.memthread = dbp.threads[tid]
}
for _, bp := range dbp.Breakpoints().M {
if bp.WatchType != 0 {
err := dbp.threads[tid].writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return nil, err
}
}
}
return dbp.threads[tid], nil
}
func (dbp *nativeProcess) updateThreadList() error {
tids, _ := filepath.Glob(fmt.Sprintf("/proc/%d/task/*", dbp.pid))
for _, tidpath := range tids {
tidstr := filepath.Base(tidpath)
tid, err := strconv.Atoi(tidstr)
if err != nil {
return err
}
if _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {
return err
}
}
return linutil.ElfUpdateSharedObjects(dbp)
}
func findExecutable(path string, pid int) string {
if path == "" {
path = fmt.Sprintf("/proc/%d/exe", pid)
}
return path
}
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return trapWaitInternal(procgrp, pid, 0)
}
type trapWaitOptions uint8
const (
trapWaitHalt trapWaitOptions = 1 << iota
trapWaitNohang
trapWaitDontCallExitGuard
)
func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) (*nativeThread, error) {
var waitdbp *nativeProcess = nil
if len(procgrp.procs) == 1 {
// Note that waitdbp is only used to call (*nativeProcess).wait which will
// behave correctly if waitdbp == nil.
waitdbp = procgrp.procs[0]
}
halt := options&trapWaitHalt != 0
for {
wopt := 0
if options&trapWaitNohang != 0 {
wopt = sys.WNOHANG
}
wpid, status, err := waitdbp.wait(pid, wopt)
if err != nil {
return nil, fmt.Errorf("wait err %s %d", err, pid)
}
if wpid == 0 {
if options&trapWaitNohang != 0 {
return nil, nil
}
continue
}
dbp := procgrp.procForThread(wpid)
var th *nativeThread
if dbp != nil {
var ok bool
th, ok = dbp.threads[wpid]
if ok {
th.Status = (*waitStatus)(status)
}
} else {
dbp = procgrp.procs[0]
}
if status.Exited() {
if wpid == dbp.pid {
dbp.postExit()
if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: status.ExitStatus()}
}
if halt {
return nil, nil
}
continue
}
delete(dbp.threads, wpid)
continue
}
if status.Signaled() {
// Signaled means the thread was terminated due to a signal.
if wpid == dbp.pid {
dbp.postExit()
if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: -int(status.Signal())}
}
if halt {
return nil, nil
}
continue
}
// does this ever happen?
delete(dbp.threads, wpid)
continue
}
if status.StopSignal() == sys.SIGTRAP && (status.TrapCause() == sys.PTRACE_EVENT_CLONE || status.TrapCause() == sys.PTRACE_EVENT_VFORK) {
// A traced thread has cloned a new thread, grab the pid and
// add it to our list of traced threads.
// If TrapCause() is sys.PTRACE_EVENT_VFORK this is actually a new
// process, but treat it as a normal thread until exec happens, so that
// we can initialize the new process normally.
var cloned uint
dbp.execPtraceFunc(func() { cloned, err = sys.PtraceGetEventMsg(wpid) })
if err != nil {
if err == sys.ESRCH {
// thread died while we were adding it
continue
}
return nil, fmt.Errorf("could not get event message: %s", err)
}
th, err = dbp.addThread(int(cloned), false)
if err != nil {
if err == sys.ESRCH {
// thread died while we were adding it
delete(dbp.threads, int(cloned))
continue
}
return nil, err
}
if halt {
th.os.running = false
dbp.threads[int(wpid)].os.running = false
return nil, nil
}
if err = th.resume(); err != nil {
if err == sys.ESRCH {
// thread died while we were adding it
delete(dbp.threads, th.ID)
continue
}
return nil, fmt.Errorf("could not continue new thread %d %s", cloned, err)
}
if err = dbp.threads[int(wpid)].resume(); err != nil {
if err != sys.ESRCH {
return nil, fmt.Errorf("could not continue existing thread %d %s", wpid, err)
}
}
continue
}
if status.StopSignal() == sys.SIGTRAP && (status.TrapCause() == sys.PTRACE_EVENT_EXEC) {
// A thread called exec and we now have a new process. Retrieve the
// thread ID of the exec'ing thread with PtraceGetEventMsg to remove it
// and create a new nativeProcess object to track the new process.
var tid uint
dbp.execPtraceFunc(func() { tid, err = sys.PtraceGetEventMsg(wpid) })
if err == nil {
delete(dbp.threads, int(tid))
}
dbp = newChildProcess(procgrp.procs[0], wpid)
dbp.followExec = true
cmdline, _ := dbp.initializeBasic()
tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, findExecutable("", dbp.pid), proc.StopLaunched, cmdline)
if err != nil {
return nil, err
}
if halt {
return nil, nil
}
if tgt != nil {
// If tgt is nil we decided we are not interested in debugging this
// process, and we have already detached from it.
err = dbp.threads[dbp.pid].resume()
if err != nil {
return nil, err
}
}
//TODO(aarzilli): if we want to give users the ability to stop the target
//group on exec here is where we should return
continue
}
if th == nil {
// Sometimes we get an unknown thread, ignore it?
continue
}
if (halt && status.StopSignal() == sys.SIGSTOP) || (status.StopSignal() == sys.SIGTRAP) {
th.os.running = false
if status.StopSignal() == sys.SIGTRAP {
th.os.setbp = true
}
return th, nil
}
// TODO(dp) alert user about unexpected signals here.
if halt && !th.os.running {
// We are trying to stop the process, queue this signal to be delivered
// to the thread when we resume.
// Do not do this for threads that were running because we sent them a
// STOP signal and we need to observe it so we don't mistakenly deliver
// it later.
th.os.delayedSignal = int(status.StopSignal())
th.os.running = false
return th, nil
} else if err := th.resumeWithSig(int(status.StopSignal())); err != nil {
if err != sys.ESRCH {
return nil, err
}
// do the same thing we do if a thread quit
if wpid == dbp.pid {
exitStatus := 0
if procgrp.numValid() == 1 {
// try to recover the real exit status using waitpid
for {
wpid2, status2, err := dbp.wait(-1, sys.WNOHANG)
if wpid2 <= 0 || err != nil {
break
}
if status2.Exited() {
exitStatus = status2.ExitStatus()
}
}
}
dbp.postExit()
if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: exitStatus}
}
continue
}
delete(dbp.threads, wpid)
}
}
}
func status(pid int, comm string) rune {
f, err := os.Open(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
return '\000'
}
defer f.Close()
rd := bufio.NewReader(f)
var (
p int
state rune
)
// The second field of /proc/pid/stat is the name of the task in parentheses.
// The name of the task is the base name of the executable for this process limited to TASK_COMM_LEN characters
// Since both parenthesis and spaces can appear inside the name of the task and no escaping happens we need to read the name of the executable first
// See: include/linux/sched.c:315 and include/linux/sched.c:1510
_, _ = fmt.Fscanf(rd, "%d ("+comm+") %c", &p, &state)
return state
}
// waitFast is like wait but does not handle process-exit correctly
func (dbp *nativeProcess) waitFast(pid int) (int, *sys.WaitStatus, error) {
var s sys.WaitStatus
wpid, err := sys.Wait4(pid, &s, sys.WALL, nil)
return wpid, &s, err
}
func (dbp *nativeProcess) wait(pid, options int) (int, *sys.WaitStatus, error) {
var s sys.WaitStatus
if (dbp == nil) || (pid != dbp.pid) || (options != 0) {
wpid, err := sys.Wait4(pid, &s, sys.WALL|options, nil)
return wpid, &s, err
}
// If we call wait4/waitpid on a thread that is the leader of its group,
// with options == 0, while ptracing and the thread leader has exited leaving
// zombies of its own then waitpid hangs forever this is apparently intended
// behaviour in the linux kernel because it's just so convenient.
// Therefore we call wait4 in a loop with WNOHANG, sleeping a while between
// calls and exiting when either wait4 succeeds or we find out that the thread
// has become a zombie.
// References:
// https://sourceware.org/bugzilla/show_bug.cgi?id=12702
// https://sourceware.org/bugzilla/show_bug.cgi?id=10095
// https://sourceware.org/bugzilla/attachment.cgi?id=5685
for {
wpid, err := sys.Wait4(pid, &s, sys.WNOHANG|sys.WALL|options, nil)
if err != nil {
return 0, nil, err
}
if wpid != 0 {
return wpid, &s, err
}
if status(pid, dbp.os.comm) == statusZombie {
return pid, nil, nil
}
time.Sleep(200 * time.Millisecond)
}
}
func exitGuard(dbp *nativeProcess, procgrp *processGroup, err error) error {
if err != sys.ESRCH {
return err
}
if status(dbp.pid, dbp.os.comm) == statusZombie {
_, err := trapWaitInternal(procgrp, -1, trapWaitDontCallExitGuard)
return err
}
return err
}
func (procgrp *processGroup) resume() error {
// all threads stopped over a breakpoint are made to step over it
for _, dbp := range procgrp.procs {
if valid, _ := dbp.Valid(); valid {
for _, thread := range dbp.threads {
if thread.CurrentBreakpoint.Breakpoint != nil {
if err := procgrp.stepInstruction(thread); err != nil {
return err
}
thread.CurrentBreakpoint.Clear()
}
}
}
}
// everything is resumed
for _, dbp := range procgrp.procs {
if valid, _ := dbp.Valid(); valid {
for _, thread := range dbp.threads {
if err := thread.resume(); err != nil && err != sys.ESRCH {
return err
}
}
}
}
return nil
}
// stop stops all running threads and sets breakpoints
func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: procgrp.procs[0].pid}
}
for _, dbp := range procgrp.procs {
if ok, _ := dbp.Valid(); !ok {
continue
}
for _, th := range dbp.threads {
th.os.setbp = false
}
}
trapthread.os.setbp = true
// check if any other thread simultaneously received a SIGTRAP
for {
th, err := trapWaitInternal(procgrp, -1, trapWaitNohang)
if err != nil {
return nil, exitGuard(procgrp.procs[0], procgrp, err)
}
if th == nil {
break
}
}
// stop all threads that are still running
for _, dbp := range procgrp.procs {
if ok, _ := dbp.Valid(); !ok {
continue
}
for _, th := range dbp.threads {
if th.os.running {
if err := th.stop(); err != nil {
if err == sys.ESRCH {
// thread exited
delete(dbp.threads, th.ID)
} else {
return nil, exitGuard(dbp, procgrp, err)
}
}
}
}
}
// wait for all threads to stop
for {
allstopped := true
for _, dbp := range procgrp.procs {
if ok, _ := dbp.Valid(); !ok {
continue
}
for _, th := range dbp.threads {
if th.os.running {
allstopped = false
break
}
}
}
if allstopped {
break
}
_, err := trapWaitInternal(procgrp, -1, trapWaitHalt)
if err != nil {
return nil, err
}
}
switchTrapthread := false
for _, dbp := range procgrp.procs {
if ok, _ := dbp.Valid(); !ok {
continue
}
err := stop1(cctx, dbp, trapthread, &switchTrapthread)
if err != nil {
return nil, err
}
}
if switchTrapthread {
trapthreadID := trapthread.ID
trapthread = nil
for _, dbp := range procgrp.procs {
if ok, _ := dbp.Valid(); !ok {
continue
}
for _, th := range dbp.threads {
if th.os.setbp && th.ThreadID() != trapthreadID {
return th, nil
}
}
}
}
return trapthread, nil
}
func stop1(cctx *proc.ContinueOnceContext, dbp *nativeProcess, trapthread *nativeThread, switchTrapthread *bool) error {
if err := linutil.ElfUpdateSharedObjects(dbp); err != nil {
return err
}
// set breakpoints on SIGTRAP threads
var err1 error
for _, th := range dbp.threads {
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() {
manualStop = cctx.GetManualStopRequested()
}
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
// we have removed before we could receive its signal). Check if it is a
// hardcoded breakpoint, otherwise rewind the thread.
isHardcodedBreakpoint := false
pc, _ := th.PC()
for _, bpinstr := range [][]byte{
dbp.BinInfo().Arch.BreakpointInstruction(),
dbp.BinInfo().Arch.AltBreakpointInstruction()} {
if bpinstr == nil {
continue
}
buf := make([]byte, len(bpinstr))
_, _ = th.ReadMemory(buf, pc-uint64(len(buf)))
if bytes.Equal(buf, bpinstr) {
isHardcodedBreakpoint = true
break
}
}
if !isHardcodedBreakpoint {
// phantom breakpoint hit
_ = th.setPC(pc - uint64(len(dbp.BinInfo().Arch.BreakpointInstruction())))
th.os.setbp = false
if trapthread.ThreadID() == th.ThreadID() {
// Will switch to a different thread for trapthread because we don't
// want pkg/proc to believe that this thread was stopped by a
// hardcoded breakpoint.
*switchTrapthread = true
}
}
}
}
}
return err1
}
func (procgrp *processGroup) detachChild(dbp *nativeProcess) error {
return procgrp.Detach(dbp.pid, false)
}
func (dbp *nativeProcess) detach(kill bool) error {
for threadID := range dbp.threads {
err := ptraceDetach(threadID, 0)
if err != nil {
return err
}
}
if kill {
return nil
}
// For some reason the process will sometimes enter stopped state after a
// detach, this doesn't happen immediately either.
// We have to wait a bit here, then check if the main thread is stopped and
// SIGCONT it if it is.
time.Sleep(50 * time.Millisecond)
if s := status(dbp.pid, dbp.os.comm); s == 'T' {
_ = sys.Kill(dbp.pid, sys.SIGCONT)
}
return nil
}
// EntryPoint will return the process entry point address, useful for
// debugging PIEs.
func (dbp *nativeProcess) EntryPoint() (uint64, error) {
auxvbuf, err := os.ReadFile(fmt.Sprintf("/proc/%d/auxv", dbp.pid))
if err != nil {
return 0, fmt.Errorf("could not read auxiliary vector: %v", err)
}
return linutil.EntryPointFromAuxv(auxvbuf, dbp.bi.Arch.PtrSize()), nil
}
func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf.UProbeArgMap) error {
// Lazily load and initialize the BPF program upon request to set a uprobe.
if dbp.os.ebpf == nil {
var err error
dbp.os.ebpf, err = ebpf.LoadEBPFTracingProgram(dbp.bi.Images[0].Path)
if err != nil {
return err
}
}
// We only allow up to 12 args for a BPF probe.
// 6 inputs + 6 outputs.
// Return early if we have more.
if len(args) > 12 {
return errors.New("too many arguments in traced function, max is 12 input+return")
}
fns := dbp.bi.LookupFunc()[fnName]
if len(fns) != 1 {
return &proc.ErrFunctionNotFound{FuncName: fnName}
}
fn := fns[0]
offset, err := dbp.BinInfo().GStructOffset(dbp.Memory())
if err != nil {
return err
}
key := fn.Entry
err = dbp.os.ebpf.UpdateArgMap(key, goidOffset, args, offset, false)
if err != nil {
return err
}
debugname := dbp.bi.Images[0].Path
// First attach a uprobe at all return addresses. We do this instead of using a uretprobe
// for two reasons:
// 1. uretprobes do not play well with Go
// 2. uretprobes seem to not restore the function return addr on the stack when removed, destroying any
// kind of workaround we could come up with.
// TODO(derekparker): this whole thing could likely be optimized a bit.
img := dbp.BinInfo().PCToImage(fn.Entry)
f, err := elf.Open(img.Path)
if err != nil {
return fmt.Errorf("could not open elf file to resolve symbol offset: %w", err)
}
var regs proc.Registers
mem := dbp.Memory()
regs, _ = dbp.memthread.Registers()
instructions, err := proc.Disassemble(mem, regs, &proc.BreakpointMap{}, dbp.BinInfo(), fn.Entry, fn.End)
if err != nil {
return err
}
var addrs []uint64
for _, instruction := range instructions {
if instruction.IsRet() {
addrs = append(addrs, instruction.Loc.PC)
}
}
addrs = append(addrs, proc.FindDeferReturnCalls(instructions)...)
for _, addr := range addrs {
err := dbp.os.ebpf.UpdateArgMap(addr, goidOffset, args, offset, true)
if err != nil {
return err
}
off, err := ebpf.AddressToOffset(f, addr)
if err != nil {
return err
}
err = dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off)
if err != nil {
return err
}
}
off, err := ebpf.AddressToOffset(f, fn.Entry)
if err != nil {
return err
}
return dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off)
}
// FollowExec enables (or disables) follow exec mode
func (dbp *nativeProcess) FollowExec(v bool) error {
dbp.followExec = v
ptraceOptions := ptraceOptionsNormal
if dbp.followExec {
ptraceOptions = ptraceOptionsFollowExec
}
var err error
dbp.execPtraceFunc(func() {
for tid := range dbp.threads {
err = syscall.PtraceSetOptions(tid, ptraceOptions)
if err != nil {
return
}
}
})
return err
}
func killProcess(pid int) error {
return sys.Kill(pid, sys.SIGINT)
}
func getCmdLine(pid int) string {
buf, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
args := strings.SplitN(string(buf), "\x00", -1)
for i := range args {
if strings.Contains(args[i], " ") {
args[i] = strconv.Quote(args[i])
}
}
if len(args) > 0 && args[len(args)-1] == "" {
args = args[:len(args)-1]
}
return strings.Join(args, " ")
}