go-delve/delve · error

could not attach to new thread %d %s

Error message

could not attach to new thread %d %s

What it means

Delve's native Linux backend fails to ptrace-attach to a newly discovered thread (TID) while adding it to the process's thread list. PTRACE_ATTACH returned a non-EPERM error (EPERM is tolerated because PTRACE_O_TRACECLONE may already be tracing the thread). This means the debugger genuinely cannot gain control of the thread, typically due to a race with thread exit or ptrace restrictions.

Source

Thrown at pkg/proc/native/proc_linux.go:362

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

View on GitHub (pinned to a23773e6c3)

Solutions

  1. Run the debugger as root or with CAP_SYS_PTRACE (e.g. sudo dlv attach <pid>, or setsetcap cap_sys_ptrace on the dlv binary) to eliminate permission-based failures.
  2. Check kernel.yama.ptrace_scope (sysctl kernel.yama.ptrace_scope=0) if Yama LSM is blocking attach to unrelated processes.
  3. Retry the attach: a transient ESRCH usually means the thread exited; a subsequent updateThreadList will pick up live threads.
  4. Verify the target process is not already being traced by another debugger (can only have one tracer per process).
  5. Ensure the target binary's ptrace_seize protection (/proc/<pid>/dumpable) is not reset, e.g. after credential changes.

Example fix

// before: attach to every TID seen in /proc, failing on race
for _, tidpath := range tids {
    if _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {
        return err // aborts whole update on one dead thread
    }
}
// after: tolerate threads that vanished mid-scan
for _, tidpath := range tids {
    if _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {
        if _, ok := err.(proc.ErrThreadExited); ok {
            continue
        }
        return err
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// before attaching, verify permissions and Yama policy
func canAttach(pid int) error {
    if err := syscall.Kill(pid, 0); err != nil {
        return fmt.Errorf("process %d not accessible: %w", pid, err)
    }
    yama, err := os.ReadFile("/proc/sys/kernel/yama/ptrace_scope")
    if err == nil && strings.TrimSpace(string(yama)) != "0" && os.Geteuid() != 0 {
        return fmt.Errorf("yama ptrace_scope restricts attach; run as root or CAP_SYS_PTRACE")
    }
    return nil
}

Type guard

func isAttachDenied(err error) bool {
    return err != nil && (errors.Is(err, syscall.EPERM) || strings.Contains(err.Error(), "could not attach to new thread"))
}

Try / catch

th, err := updateThreadList()
if err != nil {
    if isAttachDenied(err) {
        // surface actionable hint: sudo / CAP_SYS_PTRACE / yama scope
        return fmt.Errorf("attach failed: %w (try: sudo, or sysctl kernel.yama.ptrace_scope=0)", err)
    }
    return err
}

Prevention

When it happens

Trigger: Called from addThread(tid, attach=true), e.g. during updateThreadList when enumerating /proc/<pid>/task/*, when sys.PtraceAttach(tid) returns an error other than EPERM — most commonly ESRCH because the thread exited between the /proc glob and the attach, or EPERM blocked by Yama ptrace_scope (only if not already traced), or EINVAL from an invalid TID.

Common situations: Attaching (dlv attach) to a multithreaded process whose threads are churning/exiting rapidly; running inside containers or hardened kernels with kernel.yama.ptrace_scope=2/3 that block attach to non-child processes; trying to attach to a process owned by another user without root/CAP_SYS_PTRACE; stale TIDs read from /proc that died before attach.

Related errors


AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31). Data as JSON: /api/errors/05be62834a631083. Report an issue: GitHub.