
* Handle race between fork and task_for_pid On macOS a call to fork and a subsequent call to task_for_pid will race each other. This is because the macOS kernel assigns a new proc_t structure early but the new task, thread and uthread come much later. The function exec_mach_imgact in the XNU sources contains this logic. In a system under load or one with delays in fork processing (i.e. various security software), task_for_pid as currently called by Delve often returns the parent task. This can be seen by printing out the task number around line 86. In a normal system we would see three calls: -> ~/go/bin/dlv --listen=localhost:59115 --headless=true --api-version=2 --backend=native exec ./___main_go -- Task: 9731 Task: 9731 Task: 9731 API server listening at: 127.0.0.1:59115 This is the result on a system where the race is lost: -> ~/go/bin/dlv --listen=localhost:59115 --headless=true --api-version=2 --backend=native exec ./___main_go -- Task: 8707 Task: 10499 Task: 10499 could not launch process: could not get thread count In this latter case, task 8707 is the parent task. The child task of 10499 was desired and hence the error. This code change checks to make sure the returned task is not that of the parent. If it is, it retries. It's possible other macOS reported Delve issues are the result of this failed race. * proc: correct formatting
452 lines
11 KiB
Go
452 lines
11 KiB
Go
package native
|
|
|
|
// #include "proc_darwin.h"
|
|
// #include "threads_darwin.h"
|
|
// #include "exec_darwin.h"
|
|
// #include <stdlib.h>
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
sys "golang.org/x/sys/unix"
|
|
|
|
"github.com/derekparker/delve/pkg/proc"
|
|
)
|
|
|
|
// OSProcessDetails holds Darwin specific information.
|
|
type OSProcessDetails struct {
|
|
task C.task_t // mach task for the debugged process.
|
|
exceptionPort C.mach_port_t // mach port for receiving mach exceptions.
|
|
notificationPort C.mach_port_t // mach port for dead name notification (process exit).
|
|
initialized bool
|
|
|
|
// the main port we use, will return messages from both the
|
|
// exception and notification ports.
|
|
portSet C.mach_port_t
|
|
}
|
|
|
|
// Launch creates and begins debugging a new process. Uses a
|
|
// custom fork/exec process in order to take advantage of
|
|
// PT_SIGEXC on Darwin which will turn Unix signals into
|
|
// Mach exceptions.
|
|
func Launch(cmd []string, wd string) (*Process, error) {
|
|
// check that the argument to Launch is an executable file
|
|
if fi, staterr := os.Stat(cmd[0]); staterr == nil && (fi.Mode()&0111) == 0 {
|
|
return nil, proc.NotExecutableErr
|
|
}
|
|
argv0Go, err := filepath.Abs(cmd[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Make sure the binary exists.
|
|
if filepath.Base(cmd[0]) == cmd[0] {
|
|
if _, err := exec.LookPath(cmd[0]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := os.Stat(argv0Go); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
argv0 := C.CString(argv0Go)
|
|
argvSlice := make([]*C.char, 0, len(cmd)+1)
|
|
for _, arg := range cmd {
|
|
argvSlice = append(argvSlice, C.CString(arg))
|
|
}
|
|
// argv array must be null terminated.
|
|
argvSlice = append(argvSlice, nil)
|
|
|
|
dbp := New(0)
|
|
var pid int
|
|
dbp.execPtraceFunc(func() {
|
|
ret := C.fork_exec(argv0, &argvSlice[0], C.int(len(argvSlice)),
|
|
C.CString(wd),
|
|
&dbp.os.task, &dbp.os.portSet, &dbp.os.exceptionPort,
|
|
&dbp.os.notificationPort)
|
|
pid = int(ret)
|
|
})
|
|
if pid <= 0 {
|
|
return nil, fmt.Errorf("could not fork/exec")
|
|
}
|
|
dbp.pid = pid
|
|
dbp.childProcess = true
|
|
for i := range argvSlice {
|
|
C.free(unsafe.Pointer(argvSlice[i]))
|
|
}
|
|
|
|
// Initialize enough of the Process state so that we can use resume and
|
|
// trapWait to wait until the child process calls execve.
|
|
|
|
for {
|
|
task := C.get_task_for_pid(C.int(dbp.pid))
|
|
// The task_for_pid call races with the fork call. This can
|
|
// result in the parent task being returned instead of the child.
|
|
if task != dbp.os.task {
|
|
err = dbp.updateThreadListForTask(task)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if err != couldNotGetThreadCount && err != couldNotGetThreadList {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := dbp.resume(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dbp.allGCache = nil
|
|
for _, th := range dbp.threads {
|
|
th.CurrentBreakpoint.Clear()
|
|
}
|
|
|
|
trapthread, err := dbp.trapWait(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := dbp.Halt(); err != nil {
|
|
return nil, dbp.exitGuard(err)
|
|
}
|
|
|
|
_, err = dbp.waitForStop()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dbp.os.initialized = true
|
|
dbp, err = initializeDebugProcess(dbp, argv0Go)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := dbp.SwitchThread(trapthread.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dbp, err
|
|
}
|
|
|
|
// Attach to an existing process with the given PID.
|
|
func Attach(pid int) (*Process, error) {
|
|
dbp := New(pid)
|
|
|
|
kret := C.acquire_mach_task(C.int(pid),
|
|
&dbp.os.task, &dbp.os.portSet, &dbp.os.exceptionPort,
|
|
&dbp.os.notificationPort)
|
|
|
|
if kret != C.KERN_SUCCESS {
|
|
return nil, fmt.Errorf("could not attach to %d", pid)
|
|
}
|
|
|
|
dbp.os.initialized = true
|
|
|
|
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
|
|
}
|
|
|
|
dbp, err = initializeDebugProcess(dbp, "")
|
|
if err != nil {
|
|
dbp.Detach(false)
|
|
return nil, err
|
|
}
|
|
return dbp, nil
|
|
}
|
|
|
|
// Kill kills the process.
|
|
func (dbp *Process) Kill() (err error) {
|
|
if dbp.exited {
|
|
return nil
|
|
}
|
|
err = sys.Kill(-dbp.pid, sys.SIGKILL)
|
|
if err != nil {
|
|
return errors.New("could not deliver signal: " + err.Error())
|
|
}
|
|
for port := range dbp.threads {
|
|
if C.thread_resume(C.thread_act_t(port)) != C.KERN_SUCCESS {
|
|
return errors.New("could not resume task")
|
|
}
|
|
}
|
|
for {
|
|
var task C.task_t
|
|
port := C.mach_port_wait(dbp.os.portSet, &task, C.int(0))
|
|
if port == dbp.os.notificationPort {
|
|
break
|
|
}
|
|
}
|
|
dbp.postExit()
|
|
return
|
|
}
|
|
|
|
func (dbp *Process) requestManualStop() (err error) {
|
|
var (
|
|
task = C.mach_port_t(dbp.os.task)
|
|
thread = C.mach_port_t(dbp.currentThread.os.threadAct)
|
|
exceptionPort = C.mach_port_t(dbp.os.exceptionPort)
|
|
)
|
|
kret := C.raise_exception(task, thread, exceptionPort, C.EXC_BREAKPOINT)
|
|
if kret != C.KERN_SUCCESS {
|
|
return fmt.Errorf("could not raise mach exception")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var couldNotGetThreadCount = errors.New("could not get thread count")
|
|
var couldNotGetThreadList = errors.New("could not get thread list")
|
|
|
|
func (dbp *Process) updateThreadList() error {
|
|
return dbp.updateThreadListForTask(dbp.os.task)
|
|
}
|
|
|
|
func (dbp *Process) updateThreadListForTask(task C.task_t) error {
|
|
var (
|
|
err error
|
|
kret C.kern_return_t
|
|
count C.int
|
|
list []uint32
|
|
)
|
|
|
|
for {
|
|
count = C.thread_count(task)
|
|
if count == -1 {
|
|
return couldNotGetThreadCount
|
|
}
|
|
list = make([]uint32, count)
|
|
|
|
// TODO(dp) might be better to malloc mem in C and then free it here
|
|
// instead of getting count above and passing in a slice
|
|
kret = C.get_threads(task, unsafe.Pointer(&list[0]), count)
|
|
if kret != -2 {
|
|
break
|
|
}
|
|
}
|
|
if kret != C.KERN_SUCCESS {
|
|
return couldNotGetThreadList
|
|
}
|
|
|
|
for _, thread := range dbp.threads {
|
|
thread.os.exists = false
|
|
}
|
|
|
|
for _, port := range list {
|
|
thread, ok := dbp.threads[int(port)]
|
|
if !ok {
|
|
thread, err = dbp.addThread(int(port), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
thread.os.exists = true
|
|
}
|
|
|
|
for threadID, thread := range dbp.threads {
|
|
if !thread.os.exists {
|
|
delete(dbp.threads, threadID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) addThread(port int, attach bool) (*Thread, error) {
|
|
if thread, ok := dbp.threads[port]; ok {
|
|
return thread, nil
|
|
}
|
|
thread := &Thread{
|
|
ID: port,
|
|
dbp: dbp,
|
|
os: new(OSSpecificDetails),
|
|
}
|
|
dbp.threads[port] = thread
|
|
thread.os.threadAct = C.thread_act_t(port)
|
|
if dbp.currentThread == nil {
|
|
dbp.SwitchThread(thread.ID)
|
|
}
|
|
return thread, nil
|
|
}
|
|
|
|
func findExecutable(path string, pid int) string {
|
|
if path == "" {
|
|
path = C.GoString(C.find_executable(C.int(pid)))
|
|
}
|
|
return path
|
|
}
|
|
|
|
func (dbp *Process) trapWait(pid int) (*Thread, error) {
|
|
for {
|
|
task := dbp.os.task
|
|
port := C.mach_port_wait(dbp.os.portSet, &task, C.int(0))
|
|
|
|
switch port {
|
|
case dbp.os.notificationPort:
|
|
// on macOS >= 10.12.1 the task_t changes after an execve, we could
|
|
// receive the notification for the death of the pre-execve task_t,
|
|
// this could also happen *before* we are notified that our task_t has
|
|
// changed.
|
|
if dbp.os.task != task {
|
|
continue
|
|
}
|
|
if !dbp.os.initialized {
|
|
if pidtask := C.get_task_for_pid(C.int(dbp.pid)); pidtask != 0 && dbp.os.task != pidtask {
|
|
continue
|
|
}
|
|
}
|
|
_, status, err := dbp.wait(dbp.pid, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbp.postExit()
|
|
return nil, proc.ProcessExitedError{Pid: dbp.pid, Status: status.ExitStatus()}
|
|
|
|
case C.MACH_RCV_INTERRUPTED:
|
|
dbp.haltMu.Lock()
|
|
halt := dbp.halt
|
|
dbp.haltMu.Unlock()
|
|
if !halt {
|
|
// Call trapWait again, it seems
|
|
// MACH_RCV_INTERRUPTED is emitted before
|
|
// process natural death _sometimes_.
|
|
continue
|
|
}
|
|
return nil, nil
|
|
|
|
case 0:
|
|
return nil, fmt.Errorf("error while waiting for task")
|
|
}
|
|
|
|
// In macOS 10.12.1 if we received a notification for a task other than
|
|
// the inferior's task and the inferior's task is no longer valid, this
|
|
// means inferior called execve and its task_t changed.
|
|
if dbp.os.task != task && C.task_is_valid(dbp.os.task) == 0 {
|
|
dbp.os.task = task
|
|
kret := C.reset_exception_ports(dbp.os.task, &dbp.os.exceptionPort, &dbp.os.notificationPort)
|
|
if kret != C.KERN_SUCCESS {
|
|
return nil, fmt.Errorf("could not follow task across exec: %d\n", kret)
|
|
}
|
|
}
|
|
|
|
// Since we cannot be notified of new threads on OS X
|
|
// this is as good a time as any to check for them.
|
|
dbp.updateThreadList()
|
|
th, ok := dbp.threads[int(port)]
|
|
if !ok {
|
|
dbp.haltMu.Lock()
|
|
halt := dbp.halt
|
|
dbp.haltMu.Unlock()
|
|
if halt {
|
|
dbp.halt = false
|
|
return th, nil
|
|
}
|
|
if dbp.firstStart || th.singleStepping {
|
|
dbp.firstStart = false
|
|
return th, nil
|
|
}
|
|
if err := th.Continue(); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
return th, nil
|
|
}
|
|
}
|
|
|
|
func (dbp *Process) waitForStop() ([]int, error) {
|
|
ports := make([]int, 0, len(dbp.threads))
|
|
count := 0
|
|
for {
|
|
var task C.task_t
|
|
port := C.mach_port_wait(dbp.os.portSet, &task, C.int(1))
|
|
if port != 0 && port != dbp.os.notificationPort && port != C.MACH_RCV_INTERRUPTED {
|
|
count = 0
|
|
ports = append(ports, int(port))
|
|
} else {
|
|
n := C.num_running_threads(dbp.os.task)
|
|
if n == 0 {
|
|
return ports, nil
|
|
} else if n < 0 {
|
|
return nil, fmt.Errorf("error waiting for thread stop %d", n)
|
|
} else if count > 16 {
|
|
return nil, fmt.Errorf("could not stop process %d", n)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (dbp *Process) setCurrentBreakpoints(trapthread *Thread) error {
|
|
ports, err := dbp.waitForStop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
trapthread.SetCurrentBreakpoint()
|
|
for _, port := range ports {
|
|
if th, ok := dbp.threads[port]; ok {
|
|
err := th.SetCurrentBreakpoint()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) loadProcessInformation(wg *sync.WaitGroup) {
|
|
wg.Done()
|
|
}
|
|
|
|
func (dbp *Process) wait(pid, options int) (int, *sys.WaitStatus, error) {
|
|
var status sys.WaitStatus
|
|
wpid, err := sys.Wait4(pid, &status, options, nil)
|
|
return wpid, &status, err
|
|
}
|
|
|
|
func killProcess(pid int) error {
|
|
return sys.Kill(pid, sys.SIGINT)
|
|
}
|
|
|
|
func (dbp *Process) exitGuard(err error) error {
|
|
if err != ErrContinueThread {
|
|
return err
|
|
}
|
|
_, status, werr := dbp.wait(dbp.pid, sys.WNOHANG)
|
|
if werr == nil && status.Exited() {
|
|
dbp.postExit()
|
|
return proc.ProcessExitedError{Pid: dbp.pid, Status: status.ExitStatus()}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (dbp *Process) resume() error {
|
|
// all threads stopped over a breakpoint are made to step over it
|
|
for _, thread := range dbp.threads {
|
|
if thread.CurrentBreakpoint.Breakpoint != nil {
|
|
if err := thread.StepInstruction(); err != nil {
|
|
return err
|
|
}
|
|
thread.CurrentBreakpoint.Clear()
|
|
}
|
|
}
|
|
// everything is resumed
|
|
for _, thread := range dbp.threads {
|
|
if err := thread.resume(); err != nil {
|
|
return dbp.exitGuard(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) detach(kill bool) error {
|
|
return PtraceDetach(dbp.pid, 0)
|
|
}
|