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:
morluto
2026-10-08 04:46:46 +08:00
committed by GitHub
parent 63ba7424c0
commit 63cf28fd8f
51 changed files with 5506 additions and 316 deletions
+204 -26
View File
@@ -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);