gastownhall/beads · error

procid: pidfd signal %d: %w

Error message

procid: pidfd signal %d: %w

What it means

On the Linux pidfd path, this wraps the errno returned by PidfdSendSignal when it fails and the error is not the swallowed fatal-signal+ESRCH case. Common causes: ESRCH for non-fatal signals (process exited after Open), EPERM (blocked by seccomp or lacking permission), EINVAL, or EMFILE-adjacent fd issues on the pidfd itself.

Source

Thrown at internal/procid/procid_linux.go:110

		return nil, fmt.Errorf("procid: process %d does not match token", pid)
	}
	return &Handle{pid: pid, token: tok, pidfd: fd}, nil
}

// Signal sends sig to the verified process.
func (h *Handle) Signal(sig os.Signal) error {
	if h.pidfd >= 0 {
		unixSig, ok := sig.(syscall.Signal)
		if !ok {
			return fmt.Errorf("procid: unsupported signal %v", sig)
		}
		if err := unix.PidfdSendSignal(h.pidfd, unixSig, nil, 0); err != nil {
			if isFatalSignal(unixSig) && errors.Is(err, unix.ESRCH) {
				// The target exited on its own after Open; a fatal signal's
				// goal is already met, matching the fallback path.
				return nil
			}
			return fmt.Errorf("procid: pidfd signal %d: %w", h.pid, err)
		}
		return nil
	}
	if err := h.verifyThenSignal(sig); err != nil {
		return err
	}
	return nil
}

// Kill sends SIGKILL to the verified process.
func (h *Handle) Kill() error { return h.Signal(syscall.SIGKILL) }

// Close releases the underlying pidfd, if one was opened.
func (h *Handle) Close() error {
	if h.pidfd < 0 {
		return nil
	}
	err := unix.Close(h.pidfd)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, unix.ESRCH) and treat the target as gone for non-fatal signals; reconcile state.
  2. Check errors.Is(err, unix.EPERM)/ENOSYS: update seccomp/container profile to allow pidfd_send_signal, or rely on the fallback path.
  3. Do not use a Handle after Close; reopen with procid.Open.
  4. Check errors.Is(err, unix.EBADF): the pidfd was closed — reacquire the handle.

Example fix

// before
h.Close()
_ = h.Signal(syscall.SIGUSR1) // procid: pidfd signal 4242: bad file descriptor
// after
if err := h.Signal(syscall.SIGUSR1); err != nil {
    switch {
    case errors.Is(err, unix.ESRCH):
        // target already exited
    case errors.Is(err, unix.EBADF):
        h, err = procid.Open(h_pid, tok) // reopen handle
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if h.PidfdActive() { /* ensure not Closed before Signal */ }
// Probe target liveness for non-fatal signals:
match, err := procid.Verify(h_pid, tok)
if err != nil || !match { return /* gone; skip non-fatal signal */ }

Try / catch

if err := h.Signal(syscall.SIGUSR1); err != nil {
    if strings.Contains(err.Error(), "pidfd signal") {
        if errors.Is(err, unix.ESRCH) {
            return // exited after Open
        }
        if errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOSYS) {
            // seccomp blocks pidfd_send_signal: fix profile or fall back
        }
        if errors.Is(err, unix.EBADF) {
            // handle Closed: reopen via procid.Open
        }
    }
    return err
}

Prevention

When it happens

Trigger: Handle.Signal on a pidfd-backed Linux handle where unix.PidfdSendSignal returns an error other than fatal-signal+ESRCH: sending a non-fatal signal (e.g. SIGUSR1) to a process that already exited (ESRCH); seccomp-blocked pidfd_send_signal (EPERM/ENOSYS at syscall layer); invalid pidfd after Close was called.

Common situations: Signaling a process that exited between Open and Signal with a non-fatal signal; containers with restrictive seccomp profiles lacking pidfd_send_signal; reusing a Handle after calling Close (closed pidfd → EBADF).

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/fea811b9b2a62e24. Report an issue: GitHub.