gastownhall/beads · error

read cmdline for pid %d: %w

Error message

read cmdline for pid %d: %w

What it means

unverifiedProcess.commandLineContains on Linux reads /proc/<pid>/cmdline to check what the PID is running. Missing cmdline (ENoENT/ESRCH) or empty data (zombie) is treated as 'gone'; any other read error is wrapped here. This gate exists so the proxy never kills a PID it could not identify.

Source

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

		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 {
		// Zombies expose an empty cmdline; the process has effectively exited.
		return false, true, nil
	}
	cmdline := strings.ReplaceAll(strings.TrimRight(string(data), "\x00"), "\x00", " ")
	return strings.Contains(cmdline, needle), false, nil
}

// kill sends SIGKILL through the held handle. gone reports a target that had
// already exited.
func (p *unverifiedProcess) kill() (gone bool, err error) {
	if p.pidfd >= 0 {
		if err := unix.PidfdSendSignal(p.pidfd, unix.SIGKILL, nil, 0); err != nil {
			if errors.Is(err, unix.ESRCH) {
				return true, nil
			}
			return false, fmt.Errorf("pidfd signal %d: %w", p.pid, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure /proc is mounted inside the container/chroot (mount -t proc proc /proc).
  2. Fix /proc permissions (avoid hidepid=2, or run the tool as the process owner/root).
  3. Adjust SELinux/AppArmor policy to allow reading /proc/<pid>/cmdline.
  4. Kill the PID manually after verifying identity another way, then remove the stale pidfile.

Example fix

// before: /proc missing in chroot
mount -t proc proc /proc
// after: cmdline readable
ls /proc/<pid>/cmdline && bd ...
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := os.Stat("/proc/self/cmdline"); err != nil {
    // /proc unavailable: force-stop via /proc cannot work; use manual cleanup
}

Try / catch

matched, gone, err := proc.commandLineContains(needle)
if err != nil {
    var pathErr *fs.PathError
    if errors.As(err, &pathErr) && errors.Is(pathErr.Err, fs.ErrPermission) {
        // hidepid/LSM issue: rerun as process owner or fix mount options
    }
    return err
}

Prevention

When it happens

Trigger: os.ReadFile("/proc/<pid>/cmdline") fails with an error other than ENOENT/ESRCH — e.g. EACCES on the /proc entry, or the /proc filesystem is not mounted (containers/chroots).

Common situations: Running inside a container without /proc mounted; hidepid mount option on /proc making other users' process entries inaccessible; hardened LSM (SELinux/AppArmor) denying reads; PID namespaces mismatch between host and container.

Related errors


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