gastownhall/beads · error

pidfd open %d: %w

Error message

pidfd open %d: %w

What it means

On Linux, openUnverifiedProcess first tries pidfd_open to get a stable handle on the PID (preventing PID-recycling races). If pidfd_open fails with anything other than ESRCH (gone) or ENOSYS (kernel too old, fallback allowed), the error is wrapped here and the operation aborts. The proxy needs either a pidfd or the ENOSYS fallback to inspect the process safely.

Source

Thrown at internal/storage/dbproxy/proxy/unverified_process_linux.go:40

type unverifiedProcess struct {
	pid   int
	pidfd int // -1 when the kernel has no pidfd support
}

// openUnverifiedProcess opens a stable handle for pid. gone reports a PID
// that no longer exists.
func openUnverifiedProcess(pid int) (proc *unverifiedProcess, gone bool, err error) {
	fd, err := unix.PidfdOpen(pid, 0)
	if err == nil {
		return &unverifiedProcess{pid: pid, pidfd: fd}, false, nil
	}
	if errors.Is(err, unix.ESRCH) {
		return nil, true, nil
	}
	if errors.Is(err, unix.ENOSYS) {
		return &unverifiedProcess{pid: pid, pidfd: -1}, false, nil
	}
	return nil, false, fmt.Errorf("pidfd open %d: %w", pid, err)
}

func (p *unverifiedProcess) executableBasename() (basename string, gone bool, err error) {
	return processExecutableBasename(p.pid)
}

// commandLineContains reports whether the process command line contains
// needle. The managed proxy child is spawned as "db-proxy-child --root
// <rootDir>", so a workspace's own processes always match their root path.
func (p *unverifiedProcess) commandLineContains(needle string) (matched bool, gone bool, err error) {
	data, err := os.ReadFile("/proc/" + strconv.Itoa(p.pid) + "/cmdline")
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) || errors.Is(err, unix.ESRCH) {
			return false, true, nil
		}
		return false, false, fmt.Errorf("read cmdline for pid %d: %w", p.pid, err)
	}
	if len(data) == 0 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Raise the process fd limit (ulimit -n) if EMFILE/ENFILE is the cause.
  2. Upgrade the kernel to >=5.3 so pidfd_open is supported normally.
  3. Adjust seccomp/container profile to allow pidfd_open (or return ENOSYS so the fallback path is used).
  4. Retry the stop operation — transient fd exhaustion often clears.

Example fix

// before: seccomp blocks pidfd_open with EPERM
// docker run --security-opt seccomp=default.json ...
// after: allow or downgrade unknown syscalls to ENOSYS
// seccomp profile: {"names":["pidfd_open"],"action":"SCMP_ACT_ERRNO","errnoRet":38}
Defensive patterns

Strategy: retry

Validate before calling

// probe pidfd support before relying on the fast path
fd, _, errno := unix.Syscall(unix.SYS_PIDFD_OPEN, uintptr(pid), 0, 0)
supported := errno != unix.ENOSYS && errno != unix.EPERM
if !supported { /* expect/require the ENOSYS fallback or skip force-stop */ }

Try / catch

proc, gone, err := openUnverifiedProcess(pid)
if err != nil {
    if errors.Is(err, unix.EMFILE) || errors.Is(err, unix.ENFILE) {
        // transient fd exhaustion: wait and retry once
    }
    return err
}

Prevention

When it happens

Trigger: openUnverifiedProcess calls pidfd_open(pid) on Linux and the syscall returns an unexpected errno — e.g. EMFILE/ENFILE (fd/table exhaustion), EINVAL (bad flags/kernel), EPERM in hardened environments.

Common situations: Very old kernels (<5.3) lacking pidfd_open combined with seccomp returning something other than ENOSYS; fd limits exhausted after long daemon uptime; restricted containers/seccomp profiles that return EPERM for unknown syscalls.

Related errors


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