fix: identify missing Electron lineage ownership markers

This commit is contained in:
morluto
2026-10-24 04:22:41 +08:00
parent 20ff68914b
commit 6739b3cd80
3 changed files with 67 additions and 4 deletions
+9
View File
@@ -182,6 +182,15 @@ If cleanup cannot be verified, the error retains collected observations in
`details.partial_observation` and marks their application cleanup `unverified`.
Cleanup uncertainty does not authorize signaling unrelated processes.
On Linux, Chromium-based processes can overwrite the initial environment memory
exposed through `/proc/<pid>/environ`. The environment may be readable while
`REA_PROCESS_RUN_ID` is absent, even if the application still sees it through
`process.env`. REA reports the missing marker and leaves lineage ownership and
cleanup unverified; the PID, executable, or parent relationship cannot replace
that ownership proof. This limitation can affect owned Electron captures on
native Linux as well as WSL. It does not establish that all Electron versions or
Linux environments behave the same way.
Main-process IPC recording keeps original-function removal for `on` and `once`
registrations and records a fired `once` callback once. Native Node.js owns
`once` registration, firing and callable dispatch as well as removal order and
@@ -43,6 +43,52 @@ const nativeIdentity = (
},
});
it.each(
[false, true].flatMap((native) =>
([undefined, "foreign"] as const).map((runId) => ({ native, runId })),
),
)(
"keeps missing ownership distinct from an observed foreign token, native=$native, runId=$runId",
async ({ native, runId }) => {
const signalGroup = vi.fn();
const host: ProcessOwnershipHost = {
platform: "linux",
listProcesses: async () => [launcher],
environment: async () =>
runId === undefined ? {} : { REA_PROCESS_RUN_ID: runId },
...(native
? {
processIdentities: async (rows: readonly ProcessTableEntry[]) =>
new Map(rows.map((row) => [row.pid, nativeIdentity(row)])),
}
: {}),
signalGroup,
};
const ownership = {
runId: "owned",
leaderPid: launcher.pid,
processGroupId: launcher.processGroupId,
expectedParentPid: launcher.parentPid,
expectedCommand: launcher.command,
};
await expect(
observeOwnedProcessLineage(ownership, host),
).resolves.toMatchObject({
status: "unavailable",
reason:
runId === undefined
? "REA_PROCESS_RUN_ID was absent from the readable environment for PID 100; process lineage ownership remains unverified"
: "process lineage contains an unowned or PID-reused process",
});
await expect(
cleanupOwnedProcessGroup(ownership, host),
).resolves.toMatchObject({
cleaned: false,
});
expect(signalGroup).not.toHaveBeenCalled();
},
);
it.each(
(["parent", "group", "command"] as const).flatMap((mismatch) =>
(
+12 -4
View File
@@ -275,7 +275,7 @@ export const observeOwnedProcessLineage = async (
if (observation.runId !== ownership.runId)
return unavailableLineage(
ownership,
"process lineage contains an unowned or PID-reused process",
lineageTokenMismatchReason(member.pid, observation.runId),
);
if (member.pid !== launcher.pid) verifiedDescendants.push(member);
const seeded = examinedRefresh.get(member.pid);
@@ -476,7 +476,7 @@ const observeNativeOwnedProcessLineage = async (
if (fact.token.runId !== ownership.runId)
return unavailableLineage(
ownership,
"process lineage contains an unowned or PID-reused process",
lineageTokenMismatchReason(member.pid, fact.token.runId),
);
const nativeMember = processWithNativeRelation(
fact.process,
@@ -585,7 +585,7 @@ const classifyIdentitylessLineageRefresh = async (
if (observation.runId !== ownership.runId)
return {
status: "unavailable",
reason: "process lineage contains an unowned or PID-reused process",
reason: lineageTokenMismatchReason(launcherPid, observation.runId),
};
}
const identityFailure = launcherIdentityFailure(refreshedLauncher, ownership);
@@ -621,7 +621,7 @@ const classifyIdentitylessLineageRefresh = async (
if (inTree.has(process.pid))
return {
status: "unavailable",
reason: "process lineage contains an unowned or PID-reused process",
reason: lineageTokenMismatchReason(process.pid, observation.runId),
};
continue;
}
@@ -679,6 +679,14 @@ const processWithNativeRelation = (
}
: undefined;
const lineageTokenMismatchReason = (
pid: number,
observedRunId: string | undefined,
): string =>
observedRunId === undefined
? `REA_PROCESS_RUN_ID was absent from the readable environment for PID ${String(pid)}; process lineage ownership remains unverified`
: "process lineage contains an unowned or PID-reused process";
const unavailableLineage = (
ownership: OwnedProcessGroup,
reason: string,