gastownhall/beads · warning

procid: malformed proc stat: missing comm terminator

Error message

procid: malformed proc stat: missing comm terminator

What it means

parseStartTime parses /proc/<pid>/stat to extract field 22 (starttime). The kernel splits the stat line after the comm field, which is wrapped in parentheses; comm can contain spaces, so the parser uses the LAST ')' as the terminator. This error means no ')' was found at all, so the read stat content is not a well-formed stat line.

Source

Thrown at internal/procid/procid_linux.go:203

	}
}

func isFatalSignal(sig syscall.Signal) bool {
	return sig == syscall.SIGKILL || sig == syscall.SIGTERM
}

func processStartTime(pid int) (string, error) {
	data, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
	if err != nil {
		return "", &processStatReadError{pid: pid, err: err}
	}
	return parseStartTime(string(data))
}

func parseStartTime(stat string) (string, error) {
	endComm := strings.LastIndex(stat, ")")
	if endComm == -1 {
		return "", errors.New("procid: malformed proc stat: missing comm terminator")
	}
	fields := strings.Fields(stat[endComm+1:])
	// The remainder starts with state (field 3), so starttime (field 22) is
	// its twentieth field.
	if len(fields) < 20 {
		return "", errors.New("procid: malformed proc stat: missing starttime")
	}
	if fields[0] == "Z" || fields[0] == "X" || fields[0] == "x" {
		return "", fmt.Errorf("procid: process is no longer running: %w", unix.ESRCH)
	}
	if _, err := strconv.ParseUint(fields[19], 10, 64); err != nil {
		return "", fmt.Errorf("procid: malformed proc stat starttime: %w", err)
	}
	return fields[19], nil
}

type bootIDReadError struct {
	path string

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the read: re-read /proc/<pid>/stat; a transient truncation usually resolves or turns into a clean ESRCH/'no longer running' condition
  2. Check that the target PID still exists (os.Stat on /proc/<pid>) before parsing and treat failure as process-gone
  3. Verify you are reading /proc/<pid>/stat (not statm/status) and that the read returned a full buffer
  4. If running in a container, confirm /proc is the host or namespace proc you expect and not a masked path

Example fix

// before
data, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
start, err := parseStartTime(string(data))
// after
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil { return "", err } // PID gone -> treat as ESRCH
if !strings.Contains(string(data), ")") { return processStartTime(pid) } // retry once on truncation
start, err := parseStartTime(string(data))
Defensive patterns

Strategy: retry

Validate before calling

if _, err := os.Stat(fmt.Sprintf("/proc/%d/stat", pid)); err != nil { return ErrProcessGone }

Type guard

func isStatTruncated(err error) bool { return strings.Contains(err.Error(), "missing comm terminator") }

Try / catch

start, err := processStartTime(pid)
if err != nil {
    if isStatTruncated(err) || errors.Is(err, unix.ESRCH) { return retryOnce(pid) }
    return err
}

Prevention

When it happens

Trigger: Calling processStartTime (directly or via an anonymous caller) for a PID whose /proc/<pid>/stat read returned malformed or truncated content — typically a race where the process exits and the kernel zeroes/truncates the buffer mid-read, or reading the wrong file entirely.

Common situations: Short-lived child processes exiting between open() and read() of /proc; containerized environments where /proc is masked or virtualized; racy PID reuse checks in supervisory code.

Understand the failure class

Related errors


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