mirror of
				https://github.com/go-delve/delve.git
				synced 2025-10-31 02:36:18 +08:00 
			
		
		
		
	 4ed41e9060
			
		
	
	4ed41e9060
	
	
	
		
			
			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.
		
			
				
	
	
		
			1017 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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, " ")
 | |
| }
 |