Files
reverse-engineering-anything/src/process/ProcessOwnership.part2.test.ts
T
muhammad-luayandN0zoM1z0 5547c1e697 fix(process): stop failing captures on tokens macOS cannot expose (#1057)
* 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>
2026-10-08 22:01:05 +08:00

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();
},
);
});