go-delve/delve · error

could not follow task across exec: %d

Error message

could not follow task across exec: %d

What it means

When the inferior calls execve on macOS its task_t changes. trapWait detects this (notification for a different task while the old one is invalid) and re-acquires the exception/notification ports with reset_exception_ports. If that mach call fails, Delve cannot follow the debuggee across the exec and returns "could not follow task across exec: <kret>" with the mach kernel return code.

Source

Thrown at pkg/proc/native/proc_darwin.go:355

				// 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 {
			halt := dbp.os.halt
			if halt {
				dbp.os.halt = false
				return th, nil
			}
			if dbp.firstStart || th.singleStepping {
				dbp.firstStart = false
				return th, nil
			}
			if err := th.resume(); err != nil {

View on GitHub (pinned to a23773e6c3)

Solutions

  1. Decode the printed kern_return_t to identify the mach failure (e.g. invalid task/port).
  2. Avoid exec-ing through wrappers; launch the final binary directly with `dlv exec`.
  3. Set breakpoints and use `follow-exec` compatible workflows: continue after the exec instead of stopping inside the wrapper.
  4. Check the new binary isn't SIP-protected or setuid.
  5. Update Delve; exec-following behavior is version-sensitive on macOS.

Example fix

// before
dlv exec ./run.sh        // script execs ./app
// after
go build -o ./app .
dlv exec ./app
Defensive patterns

Strategy: validation

Validate before calling

// Prefer launching the final binary directly over one that will exec:
fi, err := os.Stat(target)
if err != nil {
    return err
}
if hasShebangWrapper(fi) { // e.g. script that execs another binary
    return fmt.Errorf("debug the exec'd binary %q directly instead", actualBinary)
}

Try / catch

_, err := dbp.trapWait(-1)
if err != nil && strings.Contains(err.Error(), "could not follow task across exec") {
    // log the kern_return_t; relaunch pointing at the post-exec binary
}

Prevention

When it happens

Trigger: Debugging a program that calls execve (or a shell script wrapper) on macOS, and reset_exception_ports on the new task returns non-KERN_SUCCESS (invalid task, port rights problem).

Common situations: `dlv exec /bin/sh script.sh` where the script execs another binary; Go programs re-executing themselves; setuid or hardened-runtime targets refusing port manipulation.

Related errors


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