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
- Check errors.Is(err, unix.ESRCH) and treat the target as gone for non-fatal signals; reconcile state.
- Check errors.Is(err, unix.EPERM)/ENOSYS: update seccomp/container profile to allow pidfd_send_signal, or rely on the fallback path.
- Do not use a Handle after Close; reopen with procid.Open.
- 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
- Never use a Handle after Close; reopen with procid.Open
- Ensure container seccomp profiles allow pidfd_send_signal
- For non-fatal signals, Verify the process is alive right before signaling
- Classify wrapped errnos with errors.Is rather than string matching where possible
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
- procid: unsupported signal %v
- pidfd signal %d: %w
- procid: pidfd open %d: %w
- procid: process %d still matches token after fatal signal an
- pidfd open %d: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/fea811b9b2a62e24.
Report an issue: GitHub.