Files
reverse-engineering-anything/src/process/LinuxProcessStat.ts
T
gensokyo-devandClaude Opus 5.5 3f7d0d2d21 test(process): drive kernel thread exclusion through Linux procfs
Replace the simulated-host sweep test with one that parses PF_KTHREAD
from a controlled /proc/<pid>/stat record through the system ownership
host, with root-matching credentials and an ESRCH environ read. The
user-process control with identical facts stays fail-closed. Name the
task flag constants and correct the comments: the reported kernel
threads were live, not exiting.

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 05:36:23 +01:00

108 lines
3.4 KiB
TypeScript

import type { ProcessLifecycle } from "./ProcessLifecycle.js";
/** Linux `include/linux/sched.h` task flags reported by `/proc/<pid>/stat`. */
const PF_EXITING = 0x00000004;
const PF_KTHREAD = 0x00200000;
/** 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;
/**
* PF_KTHREAD from the task flags. A kernel thread has no user address space
* or environment, so it cannot carry a run token. Undefined when the flags
* field is malformed.
*/
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 & PF_KTHREAD) !== 0
: undefined;
const lifecycle: ProcessLifecycle | undefined =
state === "Z"
? "zombie"
: state === "X" || state === "x"
? "dead"
: !Number.isSafeInteger(flags)
? undefined
: (flags & PF_EXITING) !== 0
? "exiting"
: "non-zombie";
return {
pid,
name: value.slice(commandStart + 1, commandEnd),
startTime,
lifecycle,
parentPid,
processGroupId,
sessionId,
kernelThread,
};
};