gastownhall/beads · warning

procid: malformed proc stat: missing starttime

Error message

procid: malformed proc stat: missing starttime

What it means

parseStartTime splits everything after the comm terminator ')' into fields. The remainder starts with state (field 3 of stat), so starttime (field 22) is the 20th token. This error means the post-comm portion had fewer than 20 fields, i.e. the stat line was truncated before starttime.

Source

Thrown at internal/procid/procid_linux.go:209

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
	err  error
}

func (e *bootIDReadError) Error() string {
	return fmt.Sprintf("procid: read boot ID %s: %v", e.path, e.err)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the read once — truncation is almost always a transient exit race
  2. Treat persistent failure as process-gone (the parser already maps Z/X/x states to ESRCH) and fall back to a liveness check via signal 0 or os.Stat on /proc/<pid>
  3. Log the raw stat content to confirm it is genuinely truncated rather than a parser bug
  4. Avoid caching stat contents; always re-read freshly per call

Example fix

// before
start, err := processStartTime(pid) // fails with missing starttime on racing exit
if err != nil { return err }
// after
start, err := processStartTime(pid)
if err != nil {
    if errors.Is(err, unix.ESRCH) || strings.Contains(err.Error(), "malformed proc stat") {
        return ErrProcessGone // treat as exited, retry discovery
    }
    return err
}
Defensive patterns

Strategy: retry

Validate before calling

data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); if err == nil && strings.Count(string(data), " ") >= 21 { /* likely complete */ }

Type guard

func isMissingStarttime(err error) bool { return strings.Contains(err.Error(), "missing starttime") }

Try / catch

start, err := processStartTime(pid)
if err != nil {
    if isMissingStarttime(err) { return ErrProcessGone } // exit race: treat as gone
    return err
}

Prevention

When it happens

Trigger: Calling processStartTime on a process that exited or is being reaped while /proc/<pid>/stat is read, yielding a partial line; reading a stat file whose content was cut short by a concurrent kernel update.

Common situations: Bulk PID scans over /proc racing with process churn; test suites spawning and killing processes rapidly; filesystem snapshots or overlays exposing partial procfs content.

Understand the failure class

Related errors


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