mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
b2048d46d1
* fix(process): retain unresolved descendants during lineage observation * fix(process): preserve lineage across ancestor generation changes * fix(process): avoid retaining replacements of exited candidates * test(cli): avoid unused cache writes in module trace acceptance * fix(process): retire replaced lineage candidates * fix(process): preserve uncertain owners and validate launcher scope
1445 lines
41 KiB
TypeScript
1445 lines
41 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
cleanupOwnedProcessGroup,
|
|
verifyNoTokenOwnedProcesses,
|
|
type ProcessOwnershipHost,
|
|
type ProcessTableEntry,
|
|
} from "./ProcessOwnership.js";
|
|
import { createSystemProcessOwnershipHost } from "./ProcessOwnershipObservation.js";
|
|
import { ProcessOwnershipEvidenceOwner } from "./ProcessOwnershipEvidence.js";
|
|
|
|
const itOnLinux = process.platform === "linux" ? it : it.skip;
|
|
|
|
const observedIdentity = (entry: ProcessTableEntry, identity: string) => ({
|
|
state: "readable" as const,
|
|
identity,
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: entry.parentPid,
|
|
processGroupId: entry.processGroupId,
|
|
},
|
|
});
|
|
|
|
it.each([undefined, "run-token"])(
|
|
"validates a current generation with token %s when its baseline identity was unknown",
|
|
async (runId) => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 909,
|
|
parentPid: 1,
|
|
processGroupId: 909,
|
|
state: "S",
|
|
command: "foreign-process",
|
|
};
|
|
const environment = vi.fn(() =>
|
|
Promise.resolve(runId === undefined ? {} : { REA_PROCESS_RUN_ID: runId }),
|
|
);
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment,
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, "current-start"),
|
|
]),
|
|
),
|
|
),
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, [
|
|
{ pid: row.pid, identity: null },
|
|
]),
|
|
).resolves.toEqual(
|
|
runId === undefined
|
|
? { cleaned: true, signaled: false }
|
|
: {
|
|
cleaned: false,
|
|
reason: "token-owned process remained after cleanup",
|
|
},
|
|
);
|
|
expect(environment).toHaveBeenCalledWith(row.pid);
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
describe("Linux process start identities", () => {
|
|
itOnLinux("reads Linux process start identities from proc stat", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
expect(host.processIdentities).toBeDefined();
|
|
expect(host.captureBaseline).toBeDefined();
|
|
const observedProcess = {
|
|
pid: process.pid,
|
|
parentPid: process.ppid,
|
|
processGroupId: process.pid,
|
|
state: "S",
|
|
command: process.execPath,
|
|
} satisfies ProcessTableEntry;
|
|
const identities = await host.processIdentities?.(
|
|
[observedProcess],
|
|
undefined,
|
|
new Set([observedProcess.pid]),
|
|
);
|
|
expect(identities?.get(observedProcess.pid)).toMatchObject({
|
|
state: "readable",
|
|
identity: expect.stringMatching(/^linux-starttime:\d+$/u),
|
|
lifecycle: { state: "observed", lifecycle: "non-zombie" },
|
|
relation: {
|
|
state: "observed",
|
|
parentPid: process.ppid,
|
|
processGroupId: expect.any(Number),
|
|
},
|
|
});
|
|
expect(identities?.get(observedProcess.pid)).toHaveProperty(
|
|
"effectiveUid",
|
|
process.geteuid!(),
|
|
);
|
|
const baseline = await host.captureBaseline?.();
|
|
expect(baseline).toContainEqual({
|
|
pid: observedProcess.pid,
|
|
identity: expect.stringMatching(/^linux-starttime:\d+$/u),
|
|
});
|
|
});
|
|
});
|
|
|
|
it("validates and cleans an exact-token owner without a capture baseline", async () => {
|
|
const process: ProcessTableEntry = {
|
|
pid: 904,
|
|
parentPid: 1,
|
|
processGroupId: 904,
|
|
state: "S",
|
|
command: "generic-darwin-owner",
|
|
};
|
|
let live = true;
|
|
const signalGroup = vi.fn(() => {
|
|
live = false;
|
|
});
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve(live ? [process] : []),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable", runId: "run-token" } as const,
|
|
]),
|
|
),
|
|
),
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
live
|
|
? observedIdentity(entry, "owner-start")
|
|
: {
|
|
state: "absent" as const,
|
|
reason: "not-in-snapshot" as const,
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: process.pid,
|
|
processGroupId: process.processGroupId,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toEqual({ cleaned: true, signaled: true });
|
|
expect(signalGroup).toHaveBeenCalledWith(process.processGroupId, "SIGKILL");
|
|
});
|
|
|
|
it
|
|
.skipIf(typeof process.getuid !== "function")
|
|
.each(["EINVAL", "EACCES: permission denied"])(
|
|
"uses a same-generation native UID instead of the process-table UID for unreadable tokens: %s",
|
|
async (diagnostic) => {
|
|
const callerUid = process.getuid!();
|
|
const foreignUid = callerUid === 0 ? 1 : 0;
|
|
const verify = async (rowUid: number, effectiveUid: number) => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 906,
|
|
parentPid: 1,
|
|
processGroupId: 906,
|
|
uid: rowUid,
|
|
state: "S",
|
|
command: "unrelated-process",
|
|
};
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "darwin",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: () =>
|
|
Promise.resolve(
|
|
new Map([
|
|
[906, { state: "unavailable", reason: diagnostic } as const],
|
|
]),
|
|
),
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
{
|
|
...observedIdentity(entry, "reused-current-generation"),
|
|
effectiveUid,
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
};
|
|
return verifyNoTokenOwnedProcesses("run-token", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
});
|
|
};
|
|
|
|
await expect(verify(foreignUid, callerUid)).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: 906, reason: "environment-unreadable" }],
|
|
});
|
|
await expect(verify(callerUid, foreignUid)).resolves.toMatchObject({
|
|
cleaned: true,
|
|
unverified: [{ pid: 906, diagnostic }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it
|
|
.skipIf(typeof process.getuid !== "function")
|
|
.each(["credentials", "non-dumpable"] as const)(
|
|
"records an unrelated same-UID Linux process whose environment is withheld for %s",
|
|
async (restriction) => {
|
|
const diagnostic = "EACCES: permission denied, open '/proc/908/environ'";
|
|
const verify = async (
|
|
processGroupId: number,
|
|
environmentRestriction: typeof restriction | undefined,
|
|
) => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 908,
|
|
parentPid: 1,
|
|
processGroupId,
|
|
uid: process.getuid!(),
|
|
state: "S",
|
|
command: "unrelated-process",
|
|
};
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "linux",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: () =>
|
|
Promise.resolve(
|
|
new Map([
|
|
[908, { state: "unavailable", reason: diagnostic } as const],
|
|
]),
|
|
),
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
{
|
|
...observedIdentity(entry, "current-generation"),
|
|
effectiveUid: process.getuid!(),
|
|
...(environmentRestriction === undefined
|
|
? {}
|
|
: { environmentRestriction }),
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
};
|
|
return verifyNoTokenOwnedProcesses("run-token", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
});
|
|
};
|
|
|
|
await expect(verify(908, restriction)).resolves.toMatchObject({
|
|
cleaned: true,
|
|
unverified: [{ pid: 908, diagnostic }],
|
|
});
|
|
// EACCES alone and a withheld member of the owned group stay fail-closed.
|
|
for (const [processGroupId, environmentRestriction] of [
|
|
[908, undefined],
|
|
[100, restriction],
|
|
] as const)
|
|
await expect(
|
|
verify(processGroupId, environmentRestriction),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: 908, reason: "environment-unreadable" }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["omitted", "throws"] as const)(
|
|
"keeps a matching token unresolved when the first native identity is %s",
|
|
async (firstRead) => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 907,
|
|
parentPid: 1,
|
|
processGroupId: 907,
|
|
state: "S",
|
|
command: "identity-read-failure-candidate",
|
|
};
|
|
let identityReads = 0;
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "linux",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: () =>
|
|
Promise.resolve(
|
|
new Map([[row.pid, { state: "readable", runId: "run-token" }]]),
|
|
),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
if (identityReads === 1) {
|
|
if (firstRead === "throws")
|
|
return Promise.reject(new Error("native identity read denied"));
|
|
return Promise.resolve(new Map());
|
|
}
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, "current-generation"),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
failures: [{ pid: row.pid, reason: "process-identity-unavailable" }],
|
|
});
|
|
expect(identityReads).toBe(2);
|
|
},
|
|
);
|
|
|
|
it("keeps a matching-token replacement through omission and retries cleanup only with fresh identity proof", async () => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 913,
|
|
parentPid: 1,
|
|
processGroupId: 913,
|
|
state: "S",
|
|
command: "replacement-owner",
|
|
};
|
|
let listed = true;
|
|
let replaced = false;
|
|
let identityUnavailable = false;
|
|
let live = true;
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
const signalGroup = vi.fn(() => {
|
|
live = false;
|
|
});
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "linux",
|
|
listProcesses: async () => (listed && live ? [row] : []),
|
|
environment: async () => ({}),
|
|
processIdentities: async (entries) =>
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
!live
|
|
? { state: "absent" as const, reason: "not-in-snapshot" as const }
|
|
: identityUnavailable
|
|
? { state: "unavailable" as const, reason: "native read denied" }
|
|
: observedIdentity(entry, replaced ? "new-owner" : "old-owner"),
|
|
]),
|
|
),
|
|
runTokens: async (entries) => {
|
|
// The token belongs to the replacement, but the surrounding identity
|
|
// reads straddle reuse and therefore cannot yet grant signal authority.
|
|
replaced = true;
|
|
return new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable" as const, runId: "run-token" },
|
|
]),
|
|
);
|
|
},
|
|
signalGroup,
|
|
};
|
|
const ownership = {
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner,
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, ownership),
|
|
).resolves.toMatchObject({ cleaned: false });
|
|
listed = false;
|
|
identityUnavailable = true;
|
|
await expect(
|
|
cleanupOwnedProcessGroup(ownership, host),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "process-identity-unavailable" }],
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
expect(live).toBe(true);
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{ pid: row.pid, identity: undefined, applicability: "identity-unresolved" },
|
|
]);
|
|
|
|
identityUnavailable = false;
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, ownership),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "token-owned process remained after cleanup",
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
listed = true;
|
|
await expect(
|
|
cleanupOwnedProcessGroup(ownership, host),
|
|
).resolves.toMatchObject({
|
|
cleaned: true,
|
|
signaled: true,
|
|
});
|
|
expect(signalGroup).toHaveBeenCalledExactlyOnceWith(
|
|
row.processGroupId,
|
|
"SIGKILL",
|
|
);
|
|
expect(live).toBe(false);
|
|
expect(evidenceOwner.candidates).toEqual([]);
|
|
});
|
|
|
|
it("retains a scoped unknown generation when a later table omits its PID", async () => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 101,
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "identity-unavailable-related-process",
|
|
};
|
|
const tables = [[row], []] as const;
|
|
let tableRead = 0;
|
|
let identityReads = 0;
|
|
let nativeAbsence = false;
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "linux",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve(tables[tableRead++] ?? []),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "unavailable" as const, reason: "EPERM: denied" },
|
|
]),
|
|
),
|
|
),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
nativeAbsence
|
|
? {
|
|
state: "absent" as const,
|
|
reason: "not-found-in-caller-pid-namespace" as const,
|
|
}
|
|
: {
|
|
state: "unavailable" as const,
|
|
reason: "native identity read denied",
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
};
|
|
const relation = {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner,
|
|
};
|
|
|
|
const first = await verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
relation,
|
|
);
|
|
expect(first).toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "process-identity-unavailable" }],
|
|
});
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: undefined,
|
|
applicability: "identity-unresolved",
|
|
scopeEvidence: "native-identity-unresolved",
|
|
},
|
|
]);
|
|
|
|
const readsAfterFirstSnapshot = identityReads;
|
|
const second = await verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
relation,
|
|
);
|
|
|
|
expect(second).toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "process-identity-unavailable" }],
|
|
});
|
|
expect(identityReads).toBeGreaterThan(readsAfterFirstSnapshot);
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: undefined,
|
|
applicability: "identity-unresolved",
|
|
},
|
|
]);
|
|
expect(evidenceOwner.coverage).toMatchObject({
|
|
listedRows: 0,
|
|
retainedOnlyPids: 1,
|
|
retainedUnresolved: 1,
|
|
});
|
|
|
|
nativeAbsence = true;
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, relation),
|
|
).resolves.toMatchObject({ cleaned: true });
|
|
expect(evidenceOwner.candidates).toEqual([]);
|
|
});
|
|
|
|
it("requires a scoped current relation before exempting a withheld token", async () => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 908,
|
|
parentPid: 1,
|
|
processGroupId: 908,
|
|
state: "S",
|
|
command: "opaque-unrelated-process",
|
|
};
|
|
const verify = async (scoped: boolean, nativeRelation: boolean) => {
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "darwin",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: () =>
|
|
Promise.resolve(
|
|
new Map([
|
|
[
|
|
row.pid,
|
|
{
|
|
state: "unavailable",
|
|
reason: "platform_binary_environment_withheld",
|
|
} as const,
|
|
],
|
|
]),
|
|
),
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
nativeRelation
|
|
? observedIdentity(entry, "opaque-generation")
|
|
: {
|
|
state: "readable" as const,
|
|
identity: "opaque-generation",
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
};
|
|
return verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
scoped
|
|
? {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
}
|
|
: undefined,
|
|
);
|
|
};
|
|
|
|
await expect(verify(true, false)).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "environment-unreadable" }],
|
|
});
|
|
await expect(verify(false, false)).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "environment-unreadable" }],
|
|
});
|
|
await expect(verify(false, true)).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "environment-unreadable" }],
|
|
});
|
|
await expect(verify(true, true)).resolves.toMatchObject({
|
|
cleaned: true,
|
|
unverified: [
|
|
{ pid: row.pid, diagnostic: "platform_binary_environment_withheld" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("retains related scope when a same-generation process reparents during its first token read", async () => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 909,
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "reparented-withheld-process",
|
|
};
|
|
let identityReads = 0;
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "darwin",
|
|
signalGroup: vi.fn(),
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{
|
|
state: "unavailable" as const,
|
|
reason: "platform_binary_environment_withheld",
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
const reparented = identityReads > 1;
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{
|
|
state: "readable" as const,
|
|
identity: "generation-909",
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: reparented ? 1 : 100,
|
|
processGroupId: reparented ? 777 : 100,
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [
|
|
{
|
|
pid: row.pid,
|
|
reason: "environment-unreadable",
|
|
diagnostic: "platform_binary_environment_withheld",
|
|
},
|
|
],
|
|
});
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: "generation-909",
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "previously-related-generation",
|
|
relation: { parentPid: 1, processGroupId: 777 },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each([undefined, "foreign-token"] as const)(
|
|
"does not treat a readable %s token as exonerating a related generation that moved during its first read",
|
|
async (observedRunId) => {
|
|
const row: ProcessTableEntry = {
|
|
pid: 909,
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "reparented-cleared-token-process",
|
|
};
|
|
let identityReads = 0;
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "darwin",
|
|
signalGroup,
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({}),
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable" as const, runId: observedRunId },
|
|
]),
|
|
),
|
|
),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
const reparented = identityReads > 1;
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{
|
|
state: "readable" as const,
|
|
identity: "generation-909",
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: reparented ? 1 : 100,
|
|
processGroupId: reparented ? 777 : 100,
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: row.pid, reason: "run-token-mismatch" }],
|
|
});
|
|
expect(identityReads).toBe(2);
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: "generation-909",
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "captured-process-group-id",
|
|
relation: { parentPid: 1, processGroupId: 777 },
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.skipIf(typeof process.getuid !== "function")(
|
|
"cleans verified owners and records unrelated opaque candidates as unverified",
|
|
async () => {
|
|
const owned: ProcessTableEntry = {
|
|
pid: 905,
|
|
parentPid: 1,
|
|
processGroupId: 905,
|
|
state: "S",
|
|
command: "owned-detached-child",
|
|
};
|
|
const opaque: ProcessTableEntry = {
|
|
pid: 906,
|
|
parentPid: 1,
|
|
processGroupId: 906,
|
|
state: "S",
|
|
command: "/usr/bin/unrelated-process",
|
|
};
|
|
const malformed: ProcessTableEntry = {
|
|
...opaque,
|
|
pid: 907,
|
|
processGroupId: 907,
|
|
uid: process.getuid?.() === 0 ? 1 : 0,
|
|
command: "/usr/bin/malformed-procargs-process",
|
|
};
|
|
const sysctlDenied: ProcessTableEntry = {
|
|
...opaque,
|
|
pid: 908,
|
|
processGroupId: 908,
|
|
uid: process.getuid?.() === 0 ? 1 : 0,
|
|
command: "/usr/bin/sysctl-denied-process",
|
|
};
|
|
const processes = [owned, opaque, malformed, sysctlDenied];
|
|
const signalGroup = vi.fn((processGroupId: number) => {
|
|
for (let index = processes.length - 1; index >= 0; index -= 1)
|
|
if (processes[index]?.processGroupId === processGroupId)
|
|
processes.splice(index, 1);
|
|
});
|
|
const host: ProcessOwnershipHost = {
|
|
platform: "darwin",
|
|
listProcesses: () => Promise.resolve(processes),
|
|
environment: (pid) =>
|
|
pid === owned.pid
|
|
? Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" })
|
|
: Promise.reject(new Error("permission denied")),
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
pid === owned.pid
|
|
? { state: "readable", runId: "run-token" as const }
|
|
: {
|
|
state: "unavailable",
|
|
reason:
|
|
pid === malformed.pid
|
|
? ("malformed_procargs" as const)
|
|
: pid === sysctlDenied.pid
|
|
? ("sysctl_failed_1" as const)
|
|
: ("platform_binary_environment_withheld" as const),
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: true,
|
|
signaled: true,
|
|
unverified: [
|
|
{ pid: 906, diagnostic: "platform_binary_environment_withheld" },
|
|
{ pid: 907, diagnostic: "malformed_procargs" },
|
|
{ pid: 908, diagnostic: "sysctl_failed_1" },
|
|
],
|
|
});
|
|
expect(signalGroup).toHaveBeenCalledWith(owned.processGroupId, "SIGKILL");
|
|
expect(signalGroup).not.toHaveBeenCalledWith(
|
|
opaque.processGroupId,
|
|
"SIGKILL",
|
|
);
|
|
expect(signalGroup).not.toHaveBeenCalledWith(
|
|
malformed.processGroupId,
|
|
"SIGKILL",
|
|
);
|
|
expect(signalGroup).not.toHaveBeenCalledWith(
|
|
sysctlDenied.processGroupId,
|
|
"SIGKILL",
|
|
);
|
|
},
|
|
);
|
|
|
|
describe("capture-local process identity baselines", () => {
|
|
it("checks reused and unknown PIDs while skipping unchanged identities", async () => {
|
|
const tokenReads: number[][] = [];
|
|
const processes: readonly ProcessTableEntry[] = [
|
|
{
|
|
pid: 899,
|
|
parentPid: 1,
|
|
processGroupId: 899,
|
|
state: "S",
|
|
command: "stable",
|
|
},
|
|
{
|
|
pid: 900,
|
|
parentPid: 1,
|
|
processGroupId: 900,
|
|
state: "S",
|
|
command: "reused",
|
|
},
|
|
{
|
|
pid: 902,
|
|
parentPid: 1,
|
|
processGroupId: 902,
|
|
state: "S",
|
|
command: "unknown",
|
|
},
|
|
];
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve(processes),
|
|
environment: () => Promise.resolve({}),
|
|
processIdentities: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
entry.pid === 902
|
|
? { state: "unavailable", reason: "fixture" }
|
|
: observedIdentity(
|
|
entry,
|
|
entry.pid === 899 ? "stable-start" : "reused-start",
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
runTokens: (entries) => {
|
|
tokenReads.push(entries.map(({ pid }) => pid));
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{
|
|
state: "readable",
|
|
runId: pid === 900 ? "run-token" : undefined,
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
signalGroup: vi.fn(),
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", host, [
|
|
{ pid: 899, identity: "stable-start" },
|
|
{ pid: 900, identity: "old-start" },
|
|
{ pid: 902, identity: null },
|
|
]),
|
|
).resolves.toEqual({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
failures: [
|
|
{
|
|
pid: 902,
|
|
reason: "process-identity-unavailable",
|
|
diagnostic:
|
|
"process identity was unavailable during token validation",
|
|
},
|
|
],
|
|
});
|
|
expect(tokenReads).toEqual([[900, 902]]);
|
|
});
|
|
|
|
it("revalidates fallback environment owners before signaling after PID reuse", async () => {
|
|
let identityReads = 0;
|
|
const process: ProcessTableEntry = {
|
|
pid: 910,
|
|
parentPid: 1,
|
|
processGroupId: 910,
|
|
state: "S",
|
|
command: "fallback-environment-owner",
|
|
};
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([process]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
const identity = identityReads < 3 ? "owned-start" : "reused-start";
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, identity),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
captureBaseline: [{ pid: process.pid, identity: "old-start" }],
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
expect(identityReads).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
it("retains initially related scope when bounded token retries later move out of scope", async () => {
|
|
const process: ProcessTableEntry = {
|
|
pid: 921,
|
|
parentPid: 919,
|
|
processGroupId: 919,
|
|
state: "S",
|
|
command: "retry-reparented-candidate",
|
|
};
|
|
const tokenReads: number[][] = [];
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
let tokenAttempt = 0;
|
|
let identityRead = 0;
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([process]),
|
|
environment: () => Promise.resolve({}),
|
|
processIdentities: (entries) => {
|
|
identityRead += 1;
|
|
const moved = identityRead > 2;
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => {
|
|
return [
|
|
entry.pid,
|
|
{
|
|
state: "readable" as const,
|
|
identity: "generation-921",
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: moved ? 1 : 919,
|
|
processGroupId: moved ? 930 : 919,
|
|
},
|
|
},
|
|
];
|
|
}),
|
|
),
|
|
);
|
|
},
|
|
runTokens: (entries) => {
|
|
tokenAttempt += 1;
|
|
tokenReads.push(entries.map(({ pid }) => pid));
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{
|
|
state: "unavailable" as const,
|
|
reason:
|
|
tokenAttempt === 1
|
|
? "permission denied"
|
|
: "platform_binary_environment_withheld",
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
signalGroup: vi.fn(),
|
|
};
|
|
|
|
const result = await verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
{
|
|
leaderPid: 919,
|
|
processGroupId: 919,
|
|
sampledProcessGroupIds: [919],
|
|
evidenceOwner,
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
cleaned: false,
|
|
failures: [{ pid: 921, reason: "environment-unreadable" }],
|
|
});
|
|
expect(tokenReads).toEqual([[921], [921], [921], [921]]);
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: 921,
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "previously-related-generation",
|
|
relation: { state: "observed", parentPid: 1, processGroupId: 930 },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rechecks retained related generations when a later table omits them", async () => {
|
|
const process: ProcessTableEntry = {
|
|
pid: 920,
|
|
parentPid: 919,
|
|
processGroupId: 919,
|
|
state: "S",
|
|
command: "retained-related-candidate",
|
|
};
|
|
const tables = [[process], []] as const;
|
|
let tableRead = 0;
|
|
const tokenReads: number[][] = [];
|
|
const evidenceOwner = new ProcessOwnershipEvidenceOwner();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve(tables[tableRead++] ?? []),
|
|
environment: () => Promise.resolve({}),
|
|
processIdentities: (entries) => {
|
|
const processGroupId = tableRead < 2 ? 919 : 930;
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
{
|
|
state: "readable" as const,
|
|
identity: "generation-920",
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: tableRead < 2 ? 919 : 1,
|
|
processGroupId,
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
runTokens: (entries) => {
|
|
tokenReads.push(entries.map(({ pid }) => pid));
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "unavailable" as const, reason: "permission denied" },
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
signalGroup: vi.fn(),
|
|
};
|
|
const relation = {
|
|
leaderPid: 919,
|
|
processGroupId: 919,
|
|
sampledProcessGroupIds: [919],
|
|
evidenceOwner,
|
|
};
|
|
|
|
const first = await verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
relation,
|
|
);
|
|
expect(tokenReads).toEqual([[920], [920], [920], [920]]);
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{ pid: 920, applicability: "related-ownership-unresolved" },
|
|
]);
|
|
const second = await verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
undefined,
|
|
relation,
|
|
);
|
|
|
|
expect(first).toMatchObject({ cleaned: false, failures: [{ pid: 920 }] });
|
|
expect(second).toMatchObject({ cleaned: false, failures: [{ pid: 920 }] });
|
|
expect(tokenReads).toEqual([
|
|
[920],
|
|
[920],
|
|
[920],
|
|
[920],
|
|
[920],
|
|
[920],
|
|
[920],
|
|
[920],
|
|
]);
|
|
expect(evidenceOwner.candidates).toMatchObject([
|
|
{
|
|
pid: 920,
|
|
identity: "generation-920",
|
|
applicability: "related-ownership-unresolved",
|
|
relation: { parentPid: 1, processGroupId: 930 },
|
|
},
|
|
]);
|
|
expect(evidenceOwner.coverage).toMatchObject({
|
|
snapshotCount: 2,
|
|
listedRows: 0,
|
|
retainedOnlyPids: 1,
|
|
processTableCoverage: "unreported-by-host",
|
|
});
|
|
});
|
|
|
|
it("revalidates identity after the final environment token read", async () => {
|
|
let environmentReads = 0;
|
|
let reusedDuringFinalRead = false;
|
|
const process: ProcessTableEntry = {
|
|
pid: 912,
|
|
parentPid: 1,
|
|
processGroupId: 912,
|
|
state: "S",
|
|
command: "reused-during-final-token-read",
|
|
};
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([process]),
|
|
environment: () => {
|
|
environmentReads += 1;
|
|
if (environmentReads === 3) reusedDuringFinalRead = true;
|
|
return Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" });
|
|
},
|
|
processIdentities: (entries) => {
|
|
const identity = reusedDuringFinalRead ? "reused-start" : "owned-start";
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, identity),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
captureBaseline: [{ pid: process.pid, identity: "old-start" }],
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
expect(reusedDuringFinalRead).toBe(true);
|
|
});
|
|
|
|
it("fails closed when candidate PIDs change identity during token reading", async () => {
|
|
let identityReads = 0;
|
|
const process: ProcessTableEntry = {
|
|
pid: 911,
|
|
parentPid: 1,
|
|
processGroupId: 911,
|
|
state: "S",
|
|
command: "reused-during-token-read",
|
|
};
|
|
const processes = [process, { ...process, pid: 912, processGroupId: 912 }];
|
|
const listProcesses = vi.fn(async () => processes);
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses,
|
|
environment: () => Promise.resolve({}),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
const identity = identityReads === 1 ? "new-start" : "reused-start";
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, identity),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable", runId: undefined } as const,
|
|
]),
|
|
),
|
|
),
|
|
signalGroup: vi.fn(),
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses(
|
|
"run-token",
|
|
host,
|
|
processes.map(({ pid }) => ({ pid, identity: "old-start" })),
|
|
),
|
|
).resolves.toEqual({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
failures: processes.map(({ pid }) => ({
|
|
pid,
|
|
reason: "process-identity-unavailable",
|
|
diagnostic: "process identity changed during token validation",
|
|
})),
|
|
});
|
|
// Retry the exact candidate generations with native identity reads; do not
|
|
// rescan the full process table, which would add unrelated transient rows.
|
|
expect(listProcesses).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("uses the launcher's native relation instead of stale process-table fields", async () => {
|
|
const row = {
|
|
pid: 100,
|
|
parentPid: 999,
|
|
processGroupId: 999,
|
|
state: "S",
|
|
command: "/owned/hopper",
|
|
} satisfies ProcessTableEntry;
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
runTokens: (members) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
members.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable" as const, runId: "run-token" },
|
|
]),
|
|
),
|
|
),
|
|
processIdentities: (members) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
members.map((member) => [
|
|
member.pid,
|
|
observedIdentity(
|
|
{ ...member, parentPid: 1, processGroupId: 100 },
|
|
"start-100",
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
signalGroup,
|
|
};
|
|
const result = await cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
expectedParentPid: 1,
|
|
expectedCommand: "/owned/hopper",
|
|
},
|
|
host,
|
|
);
|
|
expect(result).toMatchObject({ cleaned: true, signaled: true });
|
|
expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL");
|
|
});
|
|
|
|
it("does not settle a live launcher from stale table ancestry when native relation mismatches", async () => {
|
|
const row = {
|
|
pid: 100,
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "/owned/hopper",
|
|
} satisfies ProcessTableEntry;
|
|
const signalGroup = vi.fn();
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([row]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
processIdentities: () =>
|
|
Promise.resolve(
|
|
new Map([
|
|
[
|
|
100,
|
|
{
|
|
...observedIdentity(row, "start-100"),
|
|
relation: {
|
|
state: "observed" as const,
|
|
parentPid: 2,
|
|
processGroupId: 200,
|
|
},
|
|
},
|
|
],
|
|
]),
|
|
),
|
|
signalGroup,
|
|
};
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
expectedParentPid: 1,
|
|
expectedCommand: "/owned/hopper",
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "owned launcher process-group identity did not match",
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refuses to signal when PID identity changes after token validation", async () => {
|
|
const signalGroup = vi.fn();
|
|
let identityReads = 0;
|
|
const process: ProcessTableEntry = {
|
|
pid: 903,
|
|
parentPid: 1,
|
|
processGroupId: 903,
|
|
state: "S",
|
|
command: "reused-before-signal",
|
|
};
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: () => Promise.resolve([process]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
processIdentities: (entries) => {
|
|
identityReads += 1;
|
|
const identity = identityReads < 3 ? "owned-start" : "reused-start";
|
|
return Promise.resolve(
|
|
new Map(
|
|
entries.map((entry) => [
|
|
entry.pid,
|
|
observedIdentity(entry, identity),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
runTokens: (entries) =>
|
|
Promise.resolve(
|
|
new Map(
|
|
entries.map(({ pid }) => [
|
|
pid,
|
|
{ state: "readable", runId: "run-token" } as const,
|
|
]),
|
|
),
|
|
),
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run-token",
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
captureBaseline: [{ pid: process.pid, identity: "old-start" }],
|
|
},
|
|
host,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: "process identity could not be revalidated",
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
expect(identityReads).toBeGreaterThanOrEqual(3);
|
|
});
|