gastownhall/beads · error

procid: pidfd open %d: %w

Error message

procid: pidfd open %d: %w

What it means

On Linux, Open first calls pidfd_open to pin the PID against reuse. If that syscall fails with anything other than ENOSYS (kernel too old), the error is wrapped and returned — the handle could not be created safely. Typical errnos are ESRCH (process already gone), EINVAL, EMFILE/ENFILE (fd exhaustion), or EPERM in restricted environments (seccomp/containers blocking pidfd_open).

Source

Thrown at internal/procid/procid_linux.go:72

			return false, nil
		}
		return false, err
	}
	return current == tok, nil
}

// Open verifies pid's token and opens a handle suitable for safe signaling.
//
// The pidfd is opened before the token check: a pidfd pins the PID number
// against reuse, so verifying afterwards proves the fd refers to the process
// the token describes. Verifying first would leave a window where the
// verified process exits, the PID is recycled, and the pidfd targets the
// unrelated replacement.
func Open(pid int, tok Token) (*Handle, error) {
	fd, err := pidfdOpen(pid, 0)
	if err != nil {
		if !errors.Is(err, unix.ENOSYS) {
			return nil, fmt.Errorf("procid: pidfd open %d: %w", pid, err)
		}
		// Kernel without pidfds: fall back to verify-then-signal, which
		// retains the documented small PID-reuse race.
		match, verifyErr := Verify(pid, tok)
		if verifyErr != nil {
			return nil, verifyErr
		}
		if !match {
			return nil, fmt.Errorf("procid: process %d does not match token", pid)
		}
		return &Handle{pid: pid, token: tok, pidfd: -1}, nil
	}
	match, verifyErr := Verify(pid, tok)
	if verifyErr != nil {
		_ = unix.Close(fd)
		return nil, verifyErr
	}
	if !match {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, unix.ESRCH): the process is already gone; reconcile and skip.
  2. Check errors.Is(err, unix.EMFILE) || errors.Is(err, unix.ENFILE): close leaked fds / raise RLIMIT_NOFILE, then retry.
  3. If running in a container, update the seccomp/runtime profile to allow pidfd_open, or upgrade container runtime.
  4. Upgrade the kernel to >=5.3 so pidfd is available (older kernels fall back via ENOSYS path, which is different).

Example fix

// before
h, err := procid.Open(pid, tok) // procid: pidfd open 4242: too many open files
// after
if err != nil && (errors.Is(err, unix.EMFILE) || errors.Is(err, unix.ENFILE)) {
    runtime.GC() // release fds held by finalizers, close idle conns
    h, err = procid.Open(pid, tok)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Check kernel support and target liveness before Open
if _, err := os.Stat("/proc/sys/kernel/random/boot_id"); err != nil { /* env broken */ }
if err := syscall.Kill(pid, 0); err != nil {
    if errors.Is(err, unix.ESRCH) { return /* already gone */ }
}

Type guard

func isPidfdOpenFailure(err error) bool {
    return strings.HasPrefix(err.Error(), "procid: pidfd open ")
}

Try / catch

h, err := procid.Open(pid, tok)
if err != nil {
    switch {
    case errors.Is(err, unix.ESRCH):
        return // process gone
    case errors.Is(err, unix.EMFILE), errors.Is(err, unix.ENFILE):
        // free fds / raise RLIMIT_NOFILE and retry once
    case errors.Is(err, unix.EPERM):
        // seccomp blocked pidfd_open: fix container profile or accept fallback
    }
    return err
}

Prevention

When it happens

Trigger: procid.Open on Linux where unix.PidfdOpen fails with ESRCH (target exited before Open), EMFILE/ENFILE (process/file descriptor limit reached), EPERM (seccomp filter blocks pidfd_open), EINVAL.

Common situations: Killing an already-dead recorded PID; containers with restrictive seccomp profiles (older Docker default) that deny pidfd syscalls; hitting RLIMIT_NOFILE in servers with many open fds; very old kernels (<5.3) return ENOSYS and fall back instead — those do NOT produce this error.

Related errors


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