Files
rea/scripts/verify/recorded-crash/trace.mjs
T
muhammad-luay 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

176 lines
5.5 KiB
JavaScript

import assert from "node:assert/strict";
import { readdir, readFile, realpath } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { isAbsolute, join, resolve } from "node:path";
import { runOwnedCommand } from "../../../dist/process/OwnedCommand.js";
/** Check attempted syscalls, including failed attempts, on a recorded PID that collides with an owned live sentinel. */
export async function verifyCoreInspectionTrace({
root,
environment,
entrypoint,
core,
sentinel,
runId,
}) {
const gdb = await realpath(environment.REA_PWNDBG_GDB);
const python = await realpath(environment.REA_PWNTOOLS_PYTHON);
const plugin = resolve(environment.REA_PWNDBG_GDBINIT, "..");
const helperPaths = (name) =>
new Set([
join(environment.REA_PWNDBG_VENV_PATH, "bin", name),
...(environment.PATH ?? "")
.split(":")
.filter(isAbsolute)
.map((directory) => join(directory, name)),
]);
const psPaths = helperPaths("ps");
const iconvPaths = helperPaths("iconv");
const gitPaths = helperPaths("git");
const observations = [];
for (const mode of ["cli", "mcp"]) {
sentinel.assertAlive();
const prefix = join(root, `inspection-${mode}.trace`);
const invocation =
mode === "cli"
? [
entrypoint,
"inspect-recorded-crash",
core,
"--debugger-context",
"--json",
]
: [
fileURLToPath(new URL("./trace-client.mjs", import.meta.url)),
entrypoint,
core,
];
await runOwnedCommand(
{
command: environment.REA_VERIFY_STRACE_COMMAND,
arguments: [
"-ff",
"-qq",
"-s",
"8192",
"-e",
"trace=%process,%network,%file,ptrace,process_vm_readv,process_vm_writev",
"-o",
prefix,
process.execPath,
...invocation,
],
cwd: root,
hostEnvironment: environment,
expectedCommand: null,
runId,
},
{ timeoutMs: 90_000, diagnosticBytes: 1024 * 1024 },
);
const executions = [];
const ownershipReads = [];
let gdbObserved = false;
for (const file of await readdir(root)) {
if (!file.startsWith(`inspection-${mode}.trace.`)) continue;
const text = await readFile(join(root, file), "utf8");
const ownershipTrace = text
.split("\n")
.some(
(line) =>
/^execve\("\/(?:usr\/)?bin\/ps",/u.test(line) &&
line.includes(
'["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]',
) &&
line.endsWith(" = 0"),
);
const pidReads = text
.split("\n")
.filter((line) => line.includes(`/proc/${sentinel.pid}/`));
if (ownershipTrace) {
for (const line of pidReads) {
assert.ok(
new RegExp(
`/proc/${sentinel.pid}/(?:stat|status|cmdline|wchan|environ)"`,
"u",
).test(line),
`Unexpected ownership observation of the sentinel: ${line}`,
);
ownershipReads.push(line);
}
} else
assert.deepEqual(
pidReads,
[],
`Historical PID caused a provider lookup in ${file}: ${pidReads.join("\n")}`,
);
assert.ok(
!/\bptrace\(/u.test(text),
`Inspection attempted ptrace: ${file}`,
);
assert.ok(
!/\bprocess_vm_(?:readv|writev)\(/u.test(text),
`Inspection attempted process memory access: ${file}`,
);
assert.ok(
!/\b(?:socket|connect|sendto|sendmsg)\([^\n]*(?:AF_INET|AF_INET6)/u.test(
text,
),
`Inspection attempted an Internet socket: ${file}`,
);
for (const line of text.split("\n")) {
if (!/execve(?:at)?\(/u.test(line)) continue;
const match = /^execve\("([^"]+)"/u.exec(line);
assert.notEqual(match, null, `Unresolved executable identity: ${line}`);
assert.ok(
/ = (?:0|-1 [A-Z]+)/u.test(line),
`Incomplete execution observation: ${line}`,
);
const executable = await realpath(match[1]).catch(() => match[1]);
const ownership =
psPaths.has(match[1]) &&
line.includes(
'["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]',
);
const iconv =
iconvPaths.has(match[1]) && line.includes('["iconv", "-l"]');
const version =
gitPaths.has(match[1]) &&
line.includes(
"[" +
[
"git",
"--git-dir",
join(plugin, ".git"),
"rev-parse",
"--short",
"HEAD",
]
.map((argument) => JSON.stringify(argument))
.join(", ") +
"]",
);
assert.ok(
[process.execPath, python, gdb].includes(executable) ||
ownership ||
iconv ||
version,
`Unexpected attempted host execution: ${line}`,
);
if (executable === gdb) gdbObserved = true;
executions.push(executable);
}
}
assert.ok(gdbObserved, "Trace must include the actual core-only GDB stage");
assert.ok(executions.includes(python));
sentinel.assertAlive();
observations.push({
mode,
executions,
historical_pid: sentinel.pid,
sentinel: "still-alive",
ownership_process_table_reads: ownershipReads,
});
}
return observations;
}