gastownhall/beads · error

procid: unsupported signal %v

Error message

procid: unsupported signal %v

What it means

Handle.Signal on the Linux pidfd path only accepts signals of concrete type syscall.Signal, because PidfdSendSignal needs a raw signal number. Any other os.Signal implementation (custom wrapper, foreign enum) fails this check and nothing is sent.

Source

Thrown at internal/procid/procid_linux.go:102

	}
	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 {
		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.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass syscall.Signal constants (syscall.SIGTERM, syscall.SIGKILL, etc.).
  2. Convert with an explicit cast: syscall.Signal(sigNum) before calling Signal.
  3. Centralize signal construction in one helper that always returns syscall.Signal.

Example fix

// before
type appSignal string
func (a appSignal) Signal() {}
var s os.Signal = appSignal("kill")
h.Signal(s) // unsupported signal
// after
h.Signal(syscall.SIGKILL)
Defensive patterns

Strategy: type-guard

Validate before calling

sig, ok := desired.(syscall.Signal)
if !ok {
    return fmt.Errorf("need syscall.Signal, got %T", desired)
}

Type guard

func isSyscallSignal(s os.Signal) (syscall.Signal, bool) {
    ss, ok := s.(syscall.Signal)
    return ss, ok
}

Try / catch

if err := h.Signal(sig); err != nil {
    if strings.Contains(err.Error(), "unsupported signal") {
        return fmt.Errorf("signal %T must be syscall.Signal", sig)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Handle.Signal on a Linux handle with pidfd >= 0 passing a value that implements os.Signal but is not syscall.Signal (custom signal type, mock, or signal from a non-syscall package).

Common situations: Wrapping signals in an app-level type for logging; passing os.Interral-like constants redefined locally; passing test doubles in unit tests; accidentally passing nil cast to os.Signal.

Related errors


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