Files
reverse-engineering-anything/src/process/LinuxProcessStat.ts
T
2SinceJuly 5bb6591d58 fix(process): exonerate kernel threads from ownership token checks
An unrelated kernel thread that appeared during a provider run has no
readable /proc/<pid>/environ, so the ownership sweep recorded it as a live
candidate with an unreadable token and failed cleanup with
cleanup_incomplete. Kernel threads exit quickly, so the read usually
reports ESRCH and is not attributable to environment withholding, which
meant a cleanly finished provider run discarded its result.

Kernel threads are forked by the kernel, never by a provider, so they can
never carry that run's token. Read PF_KTHREAD from the same /proc/<pid>/stat
identity observation and treat a kernel thread with no readable token as
unverified evidence, like an unrelated process whose environment the OS
withholds. The candidate still has to be otherwise unrelated and holds no
signal authority.
2026-11-14 05:36:23 +01:00

104 lines
3.3 KiB
TypeScript

import type { ProcessLifecycle } from "./ProcessLifecycle.js";
/** Native process facts parsed from one Linux `/proc/<pid>/stat` record. */
export interface LinuxProcessStat {
readonly pid: number;
/** The kernel's short command name (comm), possibly truncated; display only. */
readonly name: string;
readonly startTime: string;
readonly lifecycle: ProcessLifecycle | undefined;
readonly parentPid: number;
readonly processGroupId: number;
readonly sessionId: number;
/**
* CF_KTHREAD|PF_KTHREAD (Linux `fs/proc/array.c` task_flags). Kernel threads
* are never forked by a provider, never inherit its environment, and cannot
* be token-owned; undefined when field 9 could not be parsed.
*/
readonly kernelThread: boolean | undefined;
}
/**
* Parse the changing command field by its final `)`, then validate the native
* identity and relation fields. Zero is valid for the native group/session
* facts; -1 for group/session means lock_task_sighand could not supply them
* during teardown. Preserve these sentinels separately from the start identity.
*/
export const parseLinuxProcessStat = (
value: string,
): LinuxProcessStat | undefined => {
const commandStart = value.indexOf("(");
const commandEnd = value.lastIndexOf(")");
if (commandStart <= 0 || commandEnd <= commandStart) return undefined;
const pidText = value.slice(0, commandStart).trim();
if (!/^\d+$/u.test(pidText)) return undefined;
const pid = Number(pidText);
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
const fields = value
.slice(commandEnd + 1)
.trim()
.split(/\s+/u);
const state = fields[0];
const parentPidText = fields[1];
const processGroupIdText = fields[2];
const sessionIdText = fields[3];
const startTime = fields[19];
if (
state === undefined ||
!/^[A-Za-z]$/u.test(state) ||
parentPidText === undefined ||
!/^\d+$/u.test(parentPidText) ||
processGroupIdText === undefined ||
!/^(?:-1|\d+)$/u.test(processGroupIdText) ||
sessionIdText === undefined ||
!/^(?:-1|\d+)$/u.test(sessionIdText) ||
startTime === undefined ||
!/^\d+$/u.test(startTime)
)
return undefined;
const parentPid = Number(parentPidText);
const processGroupId = Number(processGroupIdText);
const sessionId = Number(sessionIdText);
if (
!Number.isSafeInteger(parentPid) ||
!Number.isSafeInteger(processGroupId) ||
!Number.isSafeInteger(sessionId)
)
return undefined;
// Field 9 is task flags. PF_EXITING can precede state Z and procfs token
// access can already fail during that interval. Neither proves terminal exit.
const flagsText = fields[6];
const flags =
flagsText !== undefined && /^\d+$/u.test(flagsText)
? Number(flagsText)
: Number.NaN;
const kernelThread = Number.isSafeInteger(flags)
? (flags & 0x00200000) !== 0
: undefined;
const lifecycle: ProcessLifecycle | undefined =
state === "Z"
? "zombie"
: state === "X" || state === "x"
? "dead"
: !Number.isSafeInteger(flags)
? undefined
: (flags & 0x00000004) !== 0
? "exiting"
: "non-zombie";
return {
pid,
name: value.slice(commandStart + 1, commandEnd),
startTime,
lifecycle,
parentPid,
processGroupId,
sessionId,
kernelThread,
};
};