gastownhall/beads · error

procid: malformed proc stat starttime: %w

Error message

procid: malformed proc stat starttime: %w

What it means

parseStartTime validates that the starttime field of /proc/<pid>/stat (field 22, its twentieth field after the state field due to the parentheses-stripping split) is a valid unsigned integer. If strconv.ParseUint fails, the /proc data was structurally unexpected — the format is kernel-defined, so this almost always indicates parsing drift (e.g. comm containing unhandled characters) or a non-Linux/non-standard /proc rather than a caller mistake.

Source

Thrown at internal/procid/procid_linux.go:215

	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)
}

type processStatReadError struct {
	pid int
	err error
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the raw /proc/<pid>/stat content for the affected PID and confirm field 22 (starttime) is a number
  2. Upgrade procid/beads to the latest version — comm-parsing fixes land frequently
  3. If running under a sandbox (gVisor/WSL1), test on standard Linux or file an issue with the captured stat line
  4. Note that the comm field can contain ')' — verify the parser takes the LAST ')' before splitting (parse after rindex of ')')

Example fix

// before
fields := strings.Fields(string(data))
// after
// strip up to the LAST ')' so a comm like "my)proc" cannot shift fields
if i := strings.LastIndex(string(data), ")"); i >= 0 {
	fields = strings.Fields(string(data)[i+1:])
}
Defensive patterns

Strategy: validation

Validate before calling

func validateStatLine(data []byte) error {
	s := string(data)
	i := strings.LastIndexByte(s, ')')
	if i < 0 {
		return errors.New("no closing paren in proc stat")
	}
	fields := strings.Fields(s[i+1:])
	if len(fields) < 20 {
		return errors.New("too few fields")
	}
	_, err := strconv.ParseUint(fields[19], 10, 64)
	return err
}

Try / catch

tok, err := procid.Capture(pid)
if err != nil && strings.Contains(err.Error(), "malformed proc stat") {
	// log the raw /proc/<pid>/stat line and report a bug; do not trust the token
	raw, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
	log.Errorf("malformed stat for pid %d: %q: %v", pid, raw, err)
	return errUntrustedIdentity
}

Prevention

When it happens

Trigger: Reading /proc/<pid>/stat where the split produced fewer/misaligned fields than expected — typically when the process name (comm) contains characters the parser's parenthesis-stripping did not fully account for, a modified/hardened kernel altering stat layout, or reading from an unusual /proc (e.g. some sandboxed or emulated environments) with different field ordering.

Common situations: Running under gVisor, WSL1, or other compatibility layers whose /proc/stat differs; containers with masked /proc; older or patched kernels with layout differences; a bug in the field-splitting logic when comm contains ')' characters.

Understand the failure class

Related errors


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