Files
rea/src/process/LinuxProcessStatus.ts
T
Kaoru0822 26356dc34a fix(process): record withheld unrelated Linux processes as unverified (#6220)
* fix(process): record withheld unrelated Linux processes as unverified

The token sweep after an owned command (for example the Ghidra Java probe
run by setup and doctor) failed cleanup when an unrelated same-UID process
started during the sweep and Linux denied its /proc/<pid>/environ. Setgid or
setuid programs (mismatched real/saved IDs) and non-dumpable processes are
withheld by the kernel exactly like another account's processes, which were
already recorded as unverified.

Derive the kernel restriction from the same stat-bracketed procfs read that
already supplies the effective UID: all Uid/Gid IDs from status, and the
root ownership procfs reports for a non-dumpable process's environ. Keep that
fact through exit settlement. Related candidates still fail closed.

Fixes #6204

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(process): define caller credentials on hosts without geteuid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:18:47 -07:00

61 lines
2.2 KiB
TypeScript

/** Real, effective, saved and filesystem IDs from one Linux status row. */
export type LinuxProcessIds = readonly [number, number, number, number];
/** Parse the single `Uid:` or `Gid:` row of one Linux `/proc/<pid>/status` record. */
export const parseLinuxProcessIds = (
value: string,
row: "Uid" | "Gid",
): LinuxProcessIds | undefined => {
let ids: LinuxProcessIds | undefined;
for (const line of value.split("\n")) {
if (!line.startsWith(`${row}:`)) continue;
if (ids !== undefined) return undefined;
const match =
/^[UG]id:\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s*$/u.exec(line);
if (match === null) return undefined;
const [real, effective, saved, filesystem] = match.slice(1).map(Number);
if (
real === undefined ||
effective === undefined ||
saved === undefined ||
filesystem === undefined ||
[real, effective, saved, filesystem].some(
(id) => !Number.isSafeInteger(id) || id < 0,
)
)
return undefined;
ids = [real, effective, saved, filesystem];
}
return ids;
};
/**
* Why Linux withholds `/proc/<pid>/environ` from a caller without
* CAP_SYS_PTRACE (PTRACE_MODE_READ_FSCREDS): the target's real, effective or
* saved UID/GID differs from the caller's, or the target is non-dumpable, which
* procfs reports as root ownership of `/proc/<pid>/environ`.
*/
export type LinuxEnvironmentRestriction = "credentials" | "non-dumpable";
/** Derive the kernel's environment restriction from one generation's procfs facts. */
export const linuxEnvironmentRestriction = (facts: {
readonly uids: LinuxProcessIds;
readonly gids: LinuxProcessIds | undefined;
readonly procfsOwnerUid: number | undefined;
readonly callerUid: number | undefined;
readonly callerGid: number | undefined;
}): LinuxEnvironmentRestriction | undefined => {
const { uids, gids, procfsOwnerUid, callerUid, callerGid } = facts;
if (
(callerUid !== undefined &&
uids.slice(0, 3).some((uid) => uid !== callerUid)) ||
(callerGid !== undefined &&
gids !== undefined &&
gids.slice(0, 3).some((gid) => gid !== callerGid))
)
return "credentials";
return procfsOwnerUid !== undefined && procfsOwnerUid !== uids[1]
? "non-dumpable"
: undefined;
};