mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
* fix(process): stop failing captures on tokens macOS cannot expose On a busy macOS 26 host the residual process sweep fails every capture: KERN_PROCARGS2 omits the environment of Apple platform binaries for any caller and fails with EINVAL for processes owned by another account, so an unrelated process created after the capture baseline escalates to a hard cleanup failure with cleanup_incomplete. Report environment withholding distinctly from the Darwin token reader by reading the platform-binary flag through csops, carry the effective uid in the process table, and re-read unresolved tokens with a bounded retry plus a final liveness confirmation so transient exec and exit states settle. Candidates whose token the OS provably withholds, foreign-uid candidates, and candidates that exited during validation are now recorded as unverified processes instead of failing an otherwise clean capture or being signaled. Related candidates and every other unreadable candidate stay fail-closed. * test(process): align ownership lanes with the uid-aware process table The Darwin sweep reads the effective uid through ps, so the offline verification lanes' syscall allowlists must accept the new argv in verify-binary-layout and verify-evm-interface. The foreign-uid ownership test requires POSIX process uids; skip it where the platform has none (the curated Windows lane). * fix(verify): recognize UID-bearing ownership process tables --------- Co-authored-by: N0zoM1z0 <1705004398@qq.com>
252 lines
7.4 KiB
TypeScript
252 lines
7.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
cleanupOwnedProcessGroup,
|
|
verifyNoTokenOwnedProcesses,
|
|
type ProcessOwnershipHost,
|
|
} from "./ProcessOwnership.js";
|
|
import { observeOwnedProcessLineage } from "./ProcessOwnershipObservation.js";
|
|
import { host, ownership } from "./ProcessOwnership.fixture.js";
|
|
describe("owned process-group cleanup discovery", () => {
|
|
it("signals token-owned groups that were reparented outside the launcher tree", async () => {
|
|
const signalGroup = vi.fn();
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 100,
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "fixture",
|
|
},
|
|
{
|
|
pid: 900,
|
|
parentPid: 1,
|
|
processGroupId: 900,
|
|
state: "S",
|
|
command: "detached-helper",
|
|
},
|
|
{
|
|
pid: 901,
|
|
parentPid: 900,
|
|
processGroupId: 900,
|
|
state: "S",
|
|
command: "detached-worker",
|
|
},
|
|
]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
signalGroup,
|
|
};
|
|
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{ ...ownership, sweepTokenOwnedProcesses: true },
|
|
adapter,
|
|
),
|
|
).resolves.toEqual({ cleaned: true, signaled: true });
|
|
expect(signalGroup.mock.calls.map(([groupId]) => groupId)).toEqual([
|
|
100, 900,
|
|
]);
|
|
});
|
|
|
|
it("fails closed when a token-owned process remains after cleanup", async () => {
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 900,
|
|
parentPid: 1,
|
|
processGroupId: 900,
|
|
state: "S",
|
|
command: "detached-helper",
|
|
},
|
|
]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
signalGroup: vi.fn(),
|
|
};
|
|
|
|
await expect(
|
|
verifyNoTokenOwnedProcesses("run-token", adapter),
|
|
).resolves.toEqual({
|
|
cleaned: false,
|
|
reason: "token-owned process remained after cleanup",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("owned process-group cleanup discovery", () => {
|
|
it("fails before signaling when any descendant-group member is unowned", async () => {
|
|
const signalGroup = vi.fn();
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 100,
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "fixture",
|
|
},
|
|
{
|
|
pid: 101,
|
|
parentPid: 100,
|
|
processGroupId: 101,
|
|
state: "S",
|
|
command: "child-session",
|
|
},
|
|
{
|
|
pid: 104,
|
|
parentPid: 101,
|
|
processGroupId: 101,
|
|
state: "S",
|
|
command: "child-helper",
|
|
},
|
|
]),
|
|
environment: (pid) =>
|
|
Promise.resolve({
|
|
REA_PROCESS_RUN_ID: pid === 104 ? "other-run" : "run-token",
|
|
}),
|
|
signalGroup,
|
|
};
|
|
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
|
|
{
|
|
cleaned: false,
|
|
reason: "process tree contains an unowned or PID-reused process",
|
|
failures: [{ pid: 104, reason: "run-token-mismatch" }],
|
|
},
|
|
);
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
});
|
|
it("fails before signaling when a descendant group leader is not rooted", async () => {
|
|
const signalGroup = vi.fn();
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 100,
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
state: "S",
|
|
command: "fixture",
|
|
},
|
|
{
|
|
pid: 105,
|
|
parentPid: 100,
|
|
processGroupId: 101,
|
|
state: "S",
|
|
command: "descendant",
|
|
},
|
|
{
|
|
pid: 101,
|
|
parentPid: 999,
|
|
processGroupId: 101,
|
|
state: "S",
|
|
command: "unrelated-group-leader",
|
|
},
|
|
]),
|
|
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
|
|
signalGroup,
|
|
};
|
|
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
|
|
{
|
|
cleaned: false,
|
|
reason:
|
|
"descendant process-group leader identity could not be verified",
|
|
},
|
|
);
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
});
|
|
it("records sorted launcher and descendant lineage after token checks", async () => {
|
|
const { adapter } = host({
|
|
102: { REA_PROCESS_RUN_ID: "run-token" },
|
|
100: { REA_PROCESS_RUN_ID: "run-token" },
|
|
101: { REA_PROCESS_RUN_ID: "run-token" },
|
|
});
|
|
await expect(
|
|
observeOwnedProcessLineage(ownership, adapter),
|
|
).resolves.toEqual({
|
|
status: "verified",
|
|
observedAt: expect.any(String),
|
|
lineage: {
|
|
runId: "run-token",
|
|
launcherPid: 100,
|
|
launcherParentPid: 1,
|
|
processGroupId: 100,
|
|
descendants: [
|
|
{ pid: 101, parentPid: 100, processGroupId: 100 },
|
|
{ pid: 102, parentPid: 100, processGroupId: 100 },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("process environment permission diagnostics", () => {
|
|
it.each(["EACCES", "EPERM"])(
|
|
"preserves %s while refusing to signal an unverifiable live process",
|
|
async (code) => {
|
|
const signalGroup = vi.fn();
|
|
const diagnostic = `${code}: permission denied, open '/proc/900/environ'`;
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 900,
|
|
parentPid: 1,
|
|
processGroupId: 900,
|
|
state: "S",
|
|
command: "unverifiable-process",
|
|
},
|
|
]),
|
|
environment: () => Promise.reject(new Error(diagnostic)),
|
|
signalGroup,
|
|
};
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{ ...ownership, sweepTokenOwnedProcesses: true },
|
|
adapter,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: false,
|
|
reason: `process ownership token could not be read for 1 live process(es): environment_errno_${code}=1`,
|
|
failures: [{ pid: 900, reason: "environment-unreadable", diagnostic }],
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it.skipIf(typeof process.getuid !== "function")(
|
|
"records a foreign-uid unreadable candidate as unverified without signaling",
|
|
async () => {
|
|
const foreignUid = process.getuid?.() === 0 ? 1 : 0;
|
|
const signalGroup = vi.fn();
|
|
const adapter: ProcessOwnershipHost = {
|
|
listProcesses: () =>
|
|
Promise.resolve([
|
|
{
|
|
pid: 900,
|
|
parentPid: 1,
|
|
processGroupId: 900,
|
|
uid: foreignUid,
|
|
state: "S",
|
|
command: "unverifiable-process",
|
|
},
|
|
]),
|
|
environment: () => Promise.reject(new Error("EINVAL: sysctl failed")),
|
|
signalGroup,
|
|
};
|
|
await expect(
|
|
cleanupOwnedProcessGroup(
|
|
{ ...ownership, sweepTokenOwnedProcesses: true },
|
|
adapter,
|
|
),
|
|
).resolves.toMatchObject({
|
|
cleaned: true,
|
|
signaled: false,
|
|
unverified: [{ pid: 900, diagnostic: "EINVAL: sysctl failed" }],
|
|
});
|
|
expect(signalGroup).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|