mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(process): preserve partial evidence and verify detached cleanup (#1011)
* test(ci): allow transport delivery in source identity regression * fix(process): preserve partial evidence and verify detached cleanup * fix(process): scope Linux token sweeps by stable process identity * fix(process): retain environment permission failure codes * fix(process): wait for signaled processes before verifying cleanup * fix(process): verify sampled detached groups during cleanup * fix(electron): preserve prelaunch identities through cleanup * fix(process): retry growing Darwin process table snapshots * fix(process): retain compatibility with saved v3 capture evidence * fix(process): migrate saved v3 captures during reconstruction verification * fix(process): preserve cancellation during Darwin helper compilation * fix(process): propagate cancellation through ownership baselines * perf(javascript): cache canonical export comparison sort keys * fix(process): cancel provider ownership preparation before spawn * fix(process): isolate native compiler waiter cancellation
This commit is contained in:
@@ -3,11 +3,14 @@ import { readFile } from "node:fs/promises";
|
||||
import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js";
|
||||
import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js";
|
||||
import { execFileOutput } from "./ExecFileOutput.js";
|
||||
import { createDarwinProcessRunTokenReader } from "./DarwinProcessRunTokenReader.js";
|
||||
import type {
|
||||
OwnedProcessGroup,
|
||||
ProcessGroupObservation,
|
||||
ProcessLineageObservation,
|
||||
ProcessOwnershipHost,
|
||||
ProcessIdentityObservation,
|
||||
ProcessOwnershipBaseline,
|
||||
ProcessTableEntry,
|
||||
} from "./ProcessOwnership.js";
|
||||
|
||||
@@ -144,18 +147,53 @@ export const parseProcessEnvironment = (
|
||||
}),
|
||||
);
|
||||
|
||||
/** Read Linux stat field 22, tolerating spaces and closing parentheses in comm. */
|
||||
export const parseLinuxProcessStartTime = (
|
||||
value: string,
|
||||
): string | undefined => {
|
||||
const commandEnd = value.lastIndexOf(")");
|
||||
if (commandEnd < 0) return undefined;
|
||||
const fields = value
|
||||
.slice(commandEnd + 1)
|
||||
.trim()
|
||||
.split(/\s+/u);
|
||||
const startTime = fields[19];
|
||||
return startTime !== undefined && /^\d+$/u.test(startTime)
|
||||
? startTime
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const isExpectedAbort = (
|
||||
cause: unknown,
|
||||
signal: AbortSignal | undefined,
|
||||
): boolean =>
|
||||
signal?.aborted === true &&
|
||||
(cause === signal.reason ||
|
||||
(cause instanceof Error && cause.name === "AbortError"));
|
||||
|
||||
/** Create the operating-system process inspector for an explicit host context. */
|
||||
export const createSystemProcessOwnershipHost = (
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
hostEnvironment: NodeJS.ProcessEnv = process.env,
|
||||
): ProcessOwnershipHost => ({
|
||||
platform,
|
||||
async listProcesses() {
|
||||
options: {
|
||||
/** Override the compiler executable used by the Darwin native reader. */
|
||||
readonly darwinXcrun?: string;
|
||||
} = {},
|
||||
): ProcessOwnershipHost => {
|
||||
const darwinTokens =
|
||||
platform === "darwin"
|
||||
? createDarwinProcessRunTokenReader(
|
||||
options.darwinXcrun === undefined
|
||||
? {}
|
||||
: { xcrun: options.darwinXcrun },
|
||||
)
|
||||
: undefined;
|
||||
const listProcesses = async (signal?: AbortSignal) => {
|
||||
if (platform === "win32") return [];
|
||||
const { stdout } = await execFileOutput(
|
||||
"ps",
|
||||
["-axo", "pid=,ppid=,pgid=,stat=,command="],
|
||||
{ env: hostEnvironment },
|
||||
{ env: hostEnvironment, ...(signal === undefined ? {} : { signal }) },
|
||||
);
|
||||
return stdout
|
||||
.split("\n")
|
||||
@@ -168,35 +206,175 @@ export const createSystemProcessOwnershipHost = (
|
||||
state: match[4] ?? "",
|
||||
command: match[5] ?? "",
|
||||
}));
|
||||
},
|
||||
async environment(pid) {
|
||||
};
|
||||
const processIdentities: NonNullable<
|
||||
ProcessOwnershipHost["processIdentities"]
|
||||
> = async (processes, signal) => {
|
||||
if (processes.length === 0) return new Map();
|
||||
if (platform === "linux")
|
||||
return parseProcessEnvironment(
|
||||
await readFile(`/proc/${pid}/environ`, "utf8"),
|
||||
return new Map<number, ProcessIdentityObservation>(
|
||||
await Promise.all(
|
||||
processes.map(async ({ pid }) => {
|
||||
try {
|
||||
const stat = await readFile(`/proc/${String(pid)}/stat`, {
|
||||
encoding: "utf8",
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
});
|
||||
const identity = parseLinuxProcessStartTime(stat);
|
||||
return [
|
||||
pid,
|
||||
identity === undefined
|
||||
? ({
|
||||
state: "unavailable",
|
||||
reason: "malformed_proc_stat",
|
||||
} as const)
|
||||
: ({
|
||||
state: "readable",
|
||||
identity: `linux-starttime:${identity}`,
|
||||
} as const),
|
||||
] as const;
|
||||
} catch (cause: unknown) {
|
||||
if (isExpectedAbort(cause, signal)) throw cause;
|
||||
return [
|
||||
pid,
|
||||
{ state: "unavailable", reason: errorMessage(cause) } as const,
|
||||
] as const;
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (darwinTokens === undefined) return new Map();
|
||||
try {
|
||||
return await darwinTokens.identities(processes, signal);
|
||||
} catch (cause: unknown) {
|
||||
if (isExpectedAbort(cause, signal)) throw cause;
|
||||
const reason = errorMessage(cause);
|
||||
return new Map(
|
||||
processes.map(({ pid }) => [
|
||||
pid,
|
||||
{ state: "unavailable", reason } as const,
|
||||
]),
|
||||
);
|
||||
const { stdout } = await execFileOutput("ps", ["eww", "-p", String(pid)], {
|
||||
env: hostEnvironment,
|
||||
});
|
||||
const observedEnvironment: Record<string, string> = {};
|
||||
for (const match of stdout.matchAll(
|
||||
/(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu,
|
||||
)) {
|
||||
const name = match[1];
|
||||
if (name !== undefined) observedEnvironment[name] = match[2] ?? "";
|
||||
}
|
||||
return observedEnvironment;
|
||||
},
|
||||
signalGroup(processGroupId, signal) {
|
||||
process.kill(-processGroupId, signal);
|
||||
},
|
||||
});
|
||||
};
|
||||
const captureBaseline = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProcessOwnershipBaseline> => {
|
||||
if (platform !== "linux" && darwinTokens === undefined) return [];
|
||||
const processes = liveProcesses(await listProcesses(signal));
|
||||
let identities = await processIdentities(processes, signal);
|
||||
const unreadable = processes.filter(
|
||||
({ pid }) => identities.get(pid)?.state !== "readable",
|
||||
);
|
||||
if (unreadable.length > 0) {
|
||||
const stillLive = liveProcesses(await listProcesses(signal)).filter(
|
||||
({ pid }) => unreadable.some((entry) => entry.pid === pid),
|
||||
);
|
||||
const retried = await processIdentities(stillLive, signal);
|
||||
identities = new Map([...identities, ...retried]);
|
||||
const unresolved = stillLive.filter(
|
||||
({ pid }) => identities.get(pid)?.state !== "readable",
|
||||
);
|
||||
const liveAfterRetry = liveProcesses(await listProcesses(signal));
|
||||
const stillUnresolved = unresolved.filter(({ pid }) =>
|
||||
liveAfterRetry.some((entry) => entry.pid === pid),
|
||||
);
|
||||
if (stillUnresolved.length > 0)
|
||||
throw new Error(
|
||||
`process identity snapshot is unavailable for ${String(stillUnresolved.length)} live processes`,
|
||||
);
|
||||
}
|
||||
return processes.map(({ pid }) => {
|
||||
const observation = identities.get(pid);
|
||||
return {
|
||||
pid,
|
||||
identity:
|
||||
observation?.state === "readable" ? observation.identity : null,
|
||||
};
|
||||
});
|
||||
};
|
||||
return {
|
||||
platform,
|
||||
prepare: async (signal) => {
|
||||
await darwinTokens?.prepare(signal);
|
||||
},
|
||||
listProcesses,
|
||||
async environment(pid) {
|
||||
if (platform === "linux")
|
||||
return parseProcessEnvironment(
|
||||
await readFile(`/proc/${pid}/environ`, "utf8"),
|
||||
);
|
||||
if (platform === "darwin") {
|
||||
const process = (await listProcesses()).find(
|
||||
(entry) => entry.pid === pid,
|
||||
);
|
||||
if (process === undefined)
|
||||
throw new Error(`process ${String(pid)} is not live`);
|
||||
const observation = (await darwinTokens?.read([process]))?.get(pid);
|
||||
if (observation === undefined || observation.state === "unavailable")
|
||||
throw new Error(
|
||||
observation?.reason ?? "process run token could not be read",
|
||||
);
|
||||
return observation.runId === undefined
|
||||
? {}
|
||||
: { REA_PROCESS_RUN_ID: observation.runId };
|
||||
}
|
||||
const { stdout } = await execFileOutput(
|
||||
"ps",
|
||||
["eww", "-p", String(pid)],
|
||||
{
|
||||
env: hostEnvironment,
|
||||
},
|
||||
);
|
||||
const observedEnvironment: Record<string, string> = {};
|
||||
for (const match of stdout.matchAll(
|
||||
/(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu,
|
||||
)) {
|
||||
const name = match[1];
|
||||
if (name !== undefined) observedEnvironment[name] = match[2] ?? "";
|
||||
}
|
||||
return observedEnvironment;
|
||||
},
|
||||
async runTokens(processes) {
|
||||
if (platform !== "darwin" || processes.length === 0) return new Map();
|
||||
try {
|
||||
return (await darwinTokens?.read(processes)) ?? new Map();
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
return new Map(
|
||||
processes.map(({ pid }) => [
|
||||
pid,
|
||||
{ state: "unavailable", reason: "reader_failure" } as const,
|
||||
]),
|
||||
);
|
||||
}
|
||||
},
|
||||
...(platform !== "linux" && darwinTokens === undefined
|
||||
? {}
|
||||
: {
|
||||
processIdentities,
|
||||
captureBaseline,
|
||||
...(darwinTokens === undefined ? {} : { close: darwinTokens.close }),
|
||||
}),
|
||||
signalGroup(processGroupId, signal) {
|
||||
process.kill(-processGroupId, signal);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const systemHost = createSystemProcessOwnershipHost();
|
||||
export const systemProcessOwnershipHost = createSystemProcessOwnershipHost();
|
||||
|
||||
/** Prepare native token inspection before REA launches a captured child. */
|
||||
export const prepareProcessOwnershipInspection = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> => {
|
||||
await systemProcessOwnershipHost.prepare?.(signal);
|
||||
};
|
||||
|
||||
/** Observe one group without signaling it, failing closed on identity doubt. */
|
||||
export const observeOwnedProcessGroup = async (
|
||||
ownership: OwnedProcessGroup,
|
||||
host: ProcessOwnershipHost = systemHost,
|
||||
host: ProcessOwnershipHost = systemProcessOwnershipHost,
|
||||
): Promise<ProcessGroupObservation> =>
|
||||
observeOwnedProcessGroupWithHost(ownership, host);
|
||||
|
||||
@@ -209,7 +387,7 @@ export const observeOwnedProcessGroup = async (
|
||||
*/
|
||||
export const observeOwnedProcessLineage = async (
|
||||
ownership: OwnedProcessGroup,
|
||||
host: ProcessOwnershipHost = systemHost,
|
||||
host: ProcessOwnershipHost = systemProcessOwnershipHost,
|
||||
): Promise<ProcessLineageObservation> =>
|
||||
observeOwnedProcessLineageWithHost(ownership, host);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user