gastownhall/beads · warning

procid: process %d does not match token

Error message

procid: process %d does not match token

What it means

On kernels without pidfd support (pidfd_open returns ENOSYS), Open falls back to verifying the token before creating the handle. If the verification shows the PID's process start time does not match the captured token — the process exited, or the PID was recycled by a different process — Open fails with this error.

Source

Thrown at internal/procid/procid_linux.go:81

// 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 {
		_ = unix.Close(fd)
		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 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Call procid.Verify(pid, tok) first to confirm the process identity before expecting Open to succeed; if it no longer matches, treat the recorded process as gone.
  2. Re-capture the token with procid.Capture if you intend to control the new process occupying the PID.
  3. Upgrade the kernel to >=5.3 to get the safer pidfd-based path.
  4. Check with procid.IsProcessGone on the underlying error to distinguish exit from token mismatch.

Example fix

// before
h, err := procid.Open(oldPid, tok) // process does not match token
// after
match, verr := procid.Verify(oldPid, tok)
if verr != nil || !match {
    // mark record as dead / re-capture the replacement
    tok, _ = procid.Capture(oldPid)
}
h, err := procid.Open(oldPid, tok)
Defensive patterns

Strategy: validation

Validate before calling

match, err := procid.Verify(pid, tok)
if err != nil || !match {
    // recorded PID is stale or recycled: skip or re-capture
    return
}

Type guard

func isStaleRecord(pid int, tok procid.Token) bool {
    m, err := procid.Verify(pid, tok)
    return err != nil || !m
}

Try / catch

h, err := procid.Open(pid, tok)
if err != nil {
    if strings.Contains(err.Error(), "does not match token") && procid.IsProcessGone(err) {
        // plain exit: mark record dead
    } else if strings.Contains(err.Error(), "does not match token") {
        // PID recycled: invalidate old token, re-capture
    }
    return err
}

Prevention

When it happens

Trigger: procid.Open on a pre-5.3 Linux kernel (or ENOSYS pidfd path) where procid.Verify(pid, tok) returns match=false: target already exited, or PID reused by an unrelated process since Capture.

Common situations: Old kernels (CentOS 7 / Ubuntu 18.04 era, kernels <5.3); acting on stale PIDs recorded in a database after a restart; test environments with recycled PIDs.

Related errors


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