refactor(platform): give the host platform one owner and ratchet ambient process.platform reads (#10911)

* refactor(platform): give the host platform one owner and ratchet ambient process.platform reads

* refactor(process): pass the host platform to process sampling instead of reading it ambiently

#10755 added two process.platform reads (ps selection in process sampling and the
desktop build doctor's ps fact) after the host-platform ratchet was measured.
The desktop fact already receives the host platform from doctor; process sampling
now receives it from the capture harness, removing all five ambient reads there.
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 17:42:23 +08:00
1 parent c36649a674
commit 2caedcfb57
11 files changed
+217 -16

No files matched your search

+1
View File
@@ -29,6 +29,7 @@ export const UNIT_LAYERS = Object.freeze(
"compression",
"config",
"filesystem",
"platform",
"process",
"windows",
"generatedPackageMetadata.ts",
@@ -497,6 +497,148 @@
"src/sqlite/SqliteDatabaseWorkerExecution.ts": 2,
"src/wasm/WabtArtifactProvider.ts": 1
},
"ambient-host-platform": {
"src/adb/AdbConfiguration.ts": 1,
"src/adb/AdbDeviceOutput.ts": 1,
"src/adb/AdbHostFileName.ts": 1,
"src/adb/AdbProvider.ts": 1,
"src/android/JadxConfiguration.ts": 10,
"src/android/JadxWorkspaceRoot.ts": 1,
"src/apktool/ApktoolConfiguration.ts": 4,
"src/apktool/ApktoolProvider.ts": 2,
"src/application/BinaryTargetResolver.ts": 1,
"src/application/ClientConfigPath.ts": 1,
"src/application/ClientConfigurationFiles.ts": 4,
"src/application/ClientConfigurationPublication.ts": 1,
"src/application/ClientRegistrationEnvironment.ts": 4,
"src/application/ClientRegistrationIdentity.ts": 4,
"src/application/Doctor.ts": 1,
"src/application/DshCordisPatch.ts": 2,
"src/application/JsonFiles.ts": 1,
"src/application/McpClientRegistration.ts": 4,
"src/application/SetupHost.ts": 1,
"src/application/SetupSkill.ts": 7,
"src/application/SupportedClients.ts": 2,
"src/application/Uninstall.ts": 1,
"src/application/UpdateCommandRunner.ts": 2,
"src/application/UpdateMaintenance.ts": 1,
"src/application/UpdateRuntime.ts": 2,
"src/application/binary/AnalysisSnapshotCache.ts": 1,
"src/application/binaryDiagnostics/ByteEntropyService.ts": 1,
"src/application/desktop/DesktopBuildComputers.ts": 1,
"src/application/desktop/DesktopBuildSolves.ts": 1,
"src/application/desktop/DesktopOutputTree.ts": 1,
"src/application/environment/environmentBundleWrite.ts": 1,
"src/application/javascript/ElectronActiveObservationService.ts": 1,
"src/application/process/ProcessCli.ts": 3,
"src/application/process/ProcessEvidence.ts": 1,
"src/application/process/ProcessScenarioPreparation.ts": 1,
"src/application/reverse/ReverseEngineeringService.ts": 1,
"src/artifacts/ArtifactProvider.ts": 1,
"src/artifacts/ArtifactProviderMetadata.ts": 1,
"src/artifacts/AsarListing.ts": 1,
"src/artifacts/NativeDmgArtifactReader.ts": 1,
"src/artifacts/SafeOutputTree.ts": 1,
"src/artifacts/apple/AppleAssetCatalogAnalysis.ts": 1,
"src/artifacts/apple/DylibResolutionReader.ts": 1,
"src/artifacts/apple/KeyedArchiveReader.ts": 1,
"src/artifacts/hostExecutableMarking.ts": 1,
"src/artifacts/inventory/reader.ts": 1,
"src/browser/ChromiumSandbox.ts": 1,
"src/browser/ElectronCaptureCleanup.ts": 1,
"src/browser/ElectronProcessCleanup.ts": 1,
"src/browser/PlaywrightElectronActiveProvider.ts": 1,
"src/browser/PlaywrightScenarioBrowser.ts": 2,
"src/browser/assets/LocalWebScriptArtifacts.ts": 1,
"src/browser/history/MitmproxyCaptureAdapter.ts": 1,
"src/captures/TsharkCaptureProvider.ts": 1,
"src/cli/desktopCaptureCommands.ts": 1,
"src/cli/mcpRegistration.ts": 2,
"src/cli/setupCommands.ts": 1,
"src/composition/auxiliaryAnalysisProviders.ts": 1,
"src/composition/binary.ts": 2,
"src/composition/reverseEngineering.ts": 1,
"src/config/parseConfig.ts": 2,
"src/cutter/CutterBridgeClient.ts": 1,
"src/desktop/BuildComputerEnvironment.ts": 3,
"src/desktop/BuildSecretFiles.ts": 1,
"src/doctorRuntime.ts": 9,
"src/domain/localPath.ts": 1,
"src/electron/ElectronFusePath.ts": 1,
"src/evm/EvmoleInterfaceProvider.ts": 2,
"src/filesystem/ChangeTimeBaseline.ts": 2,
"src/filesystem/callerPath.ts": 2,
"src/firmware/FirmwareCommand.ts": 1,
"src/gdb/GdbSessionManager.ts": 2,
"src/ghidra/GhidraClient.ts": 1,
"src/ghidra/GhidraInstallation.ts": 4,
"src/ghidra/GhidraJavaProbe.ts": 2,
"src/ghidra/GhidraLauncher.ts": 4,
"src/ghidra/GhidraProjectSnapshot.ts": 1,
"src/ghidra/GhidraProvider.ts": 3,
"src/ghidra/GhidraSessionRoot.ts": 1,
"src/ghidra/GhidraTargetSnapshot.ts": 1,
"src/ghidra/GhidraTransport.ts": 2,
"src/hopper/BridgeLauncher.ts": 4,
"src/hopper/HopperClient.ts": 1,
"src/hopper/HopperProvider.ts": 1,
"src/ida/IdaWorkspace.ts": 1,
"src/inspector/JavaScriptRuntimeScope.ts": 1,
"src/javascript/analysis/JavaScriptAnalysisSourceTransfer.ts": 1,
"src/javascript/recovery/WakaruCommand.ts": 1,
"src/kuna/KunaConfiguration.ts": 1,
"src/kuna/KunaDoctor.ts": 1,
"src/kuna/KunaProvider.ts": 1,
"src/native/NativeMacOSProvider.ts": 1,
"src/native/NativeMacOSProviderMetadata.ts": 1,
"src/native/pwntools/PwntoolsDecoder.ts": 2,
"src/native/windows/WindowsShortcutProvider.ts": 1,
"src/pcap/PcapCommand.ts": 1,
"src/pcap/PcapConfiguration.ts": 1,
"src/pcap/PcapNativeBound.ts": 2,
"src/pcap/PcapProvider.ts": 1,
"src/process/DarwinHelperCache.ts": 1,
"src/process/ExecFileOutput.ts": 2,
"src/process/LinuxCgroupMemory.ts": 1,
"src/process/OwnedLease.ts": 1,
"src/process/PrivateRuntimeRoot.ts": 2,
"src/process/ProcessCommandIdentity.ts": 1,
"src/process/ProcessOwnership.ts": 1,
"src/process/ProcessOwnershipObservation.ts": 4,
"src/process/ProviderProcess.ts": 2,
"src/process/RegexMatcher.ts": 2,
"src/process/capture/ProcessCaptureCapability.ts": 1,
"src/process/capture/ProcessCaptureEnvironment.ts": 1,
"src/process/capture/ProcessCaptureLifecycle.ts": 3,
"src/process/capture/ProcessHarness.ts": 1,
"src/process/capture/ProcessScenarioRuntimeValidation.ts": 1,
"src/process/ownedDebuggerSession.ts": 2,
"src/process/snapshotEnvironment.ts": 2,
"src/rizin/RizinDebugSessionManager.ts": 1,
"src/server/createServer.ts": 1,
"src/server/registerSessionTools.ts": 3,
"src/serverIdentity.ts": 1,
"src/signals/SigrokRecordingProvider.ts": 1,
"src/wasm/WabtArtifactProvider.ts": 1,
"src/windows/WindowsNativeLoader.ts": 1
},
"ambient-host-architecture": {
"src/android/JadxConfiguration.ts": 4,
"src/application/BinaryTargetResolver.ts": 1,
"src/application/Doctor.ts": 1,
"src/application/LinuxHopper.ts": 1,
"src/browser/PlaywrightElectronActiveFuses.ts": 2,
"src/evm/EvmoleInterfaceProvider.ts": 2,
"src/ghidra/GhidraInstallation.ts": 1,
"src/ghidra/GhidraProvider.ts": 1,
"src/javascript/recovery/WakaruCommand.ts": 1,
"src/native/pwntools/PwntoolsDecoder.ts": 2,
"src/process/DarwinHelperCache.ts": 1,
"src/process/capture/ProcessCaptureLifecycle.ts": 1,
"src/process/capture/ProcessHarness.ts": 1,
"src/server/createServer.ts": 1,
"src/windows/WindowsNativeLoader.ts": 1
},
"keyed-first-match": {
"src/artifacts/fit/FitStructure.ts": 1,
"src/domain/apkSignature/apkV1SignatureBlock.ts": 1,
+19
View File
@@ -471,6 +471,25 @@ checkCounted(
["src/domain/analysisErrorBase.ts"],
);
// The running host is read once by src/platform/HostPlatform; composition
// takes it from there and passes the platform or architecture on as a
// required parameter. An ambient read elsewhere mixes the host REA runs on
// with the host a caller asked about, and cannot be selected by a test.
checkCounted(
"ambient process.platform read (take HOST_PLATFORM from src/platform/HostPlatform at composition and pass the platform as a parameter)",
"ambient-host-platform",
files,
/\bprocess\.platform\b/gu,
["src/platform/HostPlatform.ts"],
);
checkCounted(
"ambient process.arch read (take HOST_PLATFORM from src/platform/HostPlatform at composition and pass the architecture as a parameter)",
"ambient-host-architecture",
files,
/\bprocess\.arch\b/gu,
["src/platform/HostPlatform.ts"],
);
// `find` on a parsed section, program-header or load-command table silently
// keeps the first of repeated declarations. Select keyed entries through
// src/domain/keyedCollection (`uniqueKeyed`, `allKeyed`, or
+3 -2
View File
@@ -14,6 +14,7 @@ import { AnalysisProviderRegistry } from "../application/binary/AnalysisProvider
import { composeBinarySession } from "../application/binary/BinarySessionComposition.js";
import { LazyAnalysisProvider } from "../application/binary/LazyAnalysisProvider.js";
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
import { HOST_PLATFORM } from "../platform/HostPlatform.js";
import {
BUNDLE_ARTIFACT_RESOURCE_OPERATIONS,
BUNDLE_NATIVE_RESOURCE_OPERATIONS,
@@ -69,7 +70,7 @@ const composeDeepBinarySession = (
});
const auxiliaryDeclarations = auxiliaryAnalysisProviderDeclarations(
environment,
process.platform,
HOST_PLATFORM.platform,
settings,
);
const nativeInspectionAvailable =
@@ -88,7 +89,7 @@ const composeDeepBinarySession = (
auxiliaryDeclarations.map(
(declaration) => new LazyAnalysisProvider(declaration),
),
createBundleResourceFactory(environment, process.platform, settings),
createBundleResourceFactory(environment, HOST_PLATFORM.platform, settings),
);
};
+1 -1
View File
@@ -200,7 +200,7 @@ const settingCommand = async (
const psFact = async (
input: DesktopBuildFactsInput,
): Promise<DesktopBuildFacts["ps"]> => {
const selected = await resolveSystemPs(process.platform);
const selected = await resolveSystemPs(input.platform);
const ps =
selected.source === "system"
? selected.command
+23
View File
@@ -0,0 +1,23 @@
/**
* The host this REA process runs on, read once from Node's ambient process
* facts. This module is the only reader of `process.platform` and
* `process.arch`: composition takes the host from here, and every other
* consumer receives the platform (or architecture) as a required parameter.
*
* Explicit parameters let one caller describe another host's layout (a
* Windows client row inspected from macOS, a Linux sandbox's paths) without
* mixing that choice with the host it actually runs on, and let tests select
* a platform without redefining global process properties.
*/
export interface HostPlatform {
/** Node's operating system identifier for this host. */
readonly platform: NodeJS.Platform;
/** Node's CPU architecture identifier for this process. */
readonly architecture: NodeJS.Architecture;
}
/** The running host, captured when this module first loads. */
export const HOST_PLATFORM: HostPlatform = Object.freeze({
platform: process.platform,
architecture: process.arch,
});
+3
View File
@@ -259,6 +259,7 @@ const createTerminalRenderer = (
interface StartCaptureRuntimeOptions {
readonly scenario: ProcessScenario;
readonly hostEnvironment: Readonly<Record<string, string | undefined>>;
readonly hostPlatform: NodeJS.Platform;
readonly temporaryRoot: string;
readonly runId: string;
readonly ownershipBaseline: ProcessOwnershipBaseline;
@@ -384,6 +385,7 @@ const startCaptureRuntime = async (
stopSampler = startProcessSampler({
rootPid: terminal.pid,
runId: options.runId,
platform: options.hostPlatform,
started,
limit: scenario.limits.processes,
samples: options.samples,
@@ -866,6 +868,7 @@ const runProcessScenario = async (
runtime = await startCaptureRuntime({
scenario,
hostEnvironment,
hostPlatform,
temporaryRoot,
runId,
ownershipBaseline,
@@ -261,6 +261,7 @@ const start = (runId = fixture.runId) => {
const samples: ProcessSample[] = [];
const resourceScope = new ProcessCaptureResourceScope();
const stop = startProcessSampler({
platform: process.platform,
rootPid: 100,
runId,
started: 0,
@@ -735,6 +736,7 @@ it("adopts the failed run-token descriptor into the actual capture scope before
fixture.runIdReadResolve = resolve;
});
const stop = startProcessSampler({
platform: process.platform,
rootPid: 100,
runId: fixture.runId,
resourceScope,
+21 -13
View File
@@ -36,6 +36,7 @@ import {
interface SampleProcessContext {
readonly rootPid: number;
readonly platform: NodeJS.Platform;
readonly elapsedMs: number;
readonly limit: number;
readonly identities: Map<number, string>;
@@ -135,10 +136,11 @@ const sampleLinux = async (
};
const readPsRowsRaw = async (
platform: NodeJS.Platform,
signal: AbortSignal,
): Promise<readonly ProcessRow[]> => {
const { stdout } = await execFileOutput(
(await resolveSystemPs(process.platform)).command,
(await resolveSystemPs(platform)).command,
["-axo", "pid=,ppid=,pgid=,sess=,command="],
{ signal, stopSignal: "SIGTERM" },
);
@@ -166,11 +168,12 @@ const readPsRowsRaw = async (
};
const readPsRows = async (
platform: NodeJS.Platform,
signal: AbortSignal,
): Promise<SamplingObservation<readonly ProcessRow[] | undefined>> =>
samplingReadObservation(
await readSamplingSource("process table (ps)", signal, () =>
readPsRowsRaw(signal),
readPsRowsRaw(platform, signal),
),
);
@@ -179,7 +182,7 @@ const sampleLinuxFallback = async (
context: SampleProcessContext,
root: ProcessRow,
): Promise<SamplingObservation<readonly ProcessRow[]>> => {
const observed = await readPsRows(context.signal);
const observed = await readPsRows(context.platform, context.signal);
if (observed.value === undefined)
return samplingObservation([root], observed.diagnostics);
const childrenByParent = new Map<number, number[]>();
@@ -242,12 +245,12 @@ const sampleLinuxFallback = async (
};
const readRootIdentity = async (
platform: NodeJS.Platform,
rootPid: number,
signal: AbortSignal,
): Promise<SamplingObservation<ProcessRow | undefined>> => {
if (process.platform === "linux")
return readLinuxProcessStat(rootPid, signal);
const rows = await readPsRows(signal);
if (platform === "linux") return readLinuxProcessStat(rootPid, signal);
const rows = await readPsRows(platform, signal);
return samplingObservation(
rows.value?.find(({ pid }) => pid === rootPid),
rows.diagnostics,
@@ -255,13 +258,14 @@ const readRootIdentity = async (
};
const inspectInitializedPtyRoot = async (
platform: NodeJS.Platform,
rootPid: number,
runId: string,
signal: AbortSignal,
): Promise<
SamplingObservation<{ readonly startTime: string | undefined } | undefined>
> => {
const before = await readRootIdentity(rootPid, signal);
const before = await readRootIdentity(platform, rootPid, signal);
if (
before.value === undefined ||
before.value.process_group_id !== rootPid ||
@@ -269,7 +273,7 @@ const inspectInitializedPtyRoot = async (
)
return samplingObservation(undefined, before.diagnostics);
const observedRunId =
process.platform === "linux"
platform === "linux"
? await readProcSamplingSource(
`/proc/${String(rootPid)}/environ`,
signal,
@@ -282,7 +286,7 @@ const inspectInitializedPtyRoot = async (
() => readProcessRunId(rootPid, undefined, signal),
),
);
const after = await readRootIdentity(rootPid, signal);
const after = await readRootIdentity(platform, rootPid, signal);
const diagnostics = mergeSamplingDiagnostics(
before.diagnostics,
observedRunId.diagnostics,
@@ -308,7 +312,7 @@ const samplePs = async (
const { rootPid, limit, signal } = context;
if (limit <= 0) return samplingObservation([]);
const observed = await readPsRows(signal);
const observed = await readPsRows(context.platform, signal);
const rows = observed.value ?? [];
const rowByPid = new Map(rows.map((row) => [row.pid, row]));
const childrenByParent = new Map<number, ProcessRow[]>();
@@ -345,11 +349,11 @@ const samplePs = async (
const sampleProcesses = async (
context: SampleProcessContext,
): Promise<SamplingObservation<readonly ProcessSample[]>> => {
if (process.platform === "win32" || context.limit <= 0)
if (context.platform === "win32" || context.limit <= 0)
return samplingObservation([]);
const rows =
process.platform === "linux"
context.platform === "linux"
? await sampleLinux(context)
: await samplePs(context);
@@ -396,6 +400,8 @@ const isOwnedSamplingShutdown = (cause: unknown, reason: unknown): boolean => {
export const startProcessSampler = (options: {
readonly rootPid: number;
readonly runId: string;
/** The host whose process table is sampled: procfs on Linux, ps elsewhere. */
readonly platform: NodeJS.Platform;
readonly resourceScope: ProcessCaptureResourceScope;
readonly started: number;
readonly limit: number;
@@ -405,7 +411,7 @@ export const startProcessSampler = (options: {
readonly partial: boolean;
readonly coverage: ProcessSamplingCoverage;
}>) => {
const { rootPid, runId, started, limit, samples } = options;
const { rootPid, runId, platform, started, limit, samples } = options;
const identities = new Map<number, string>();
const lastObservations = new Map<number, string>();
let pending: Promise<void> | undefined;
@@ -429,6 +435,7 @@ export const startProcessSampler = (options: {
let diagnostics = mergeSamplingDiagnostics();
if (!rootInitialized) {
const identity = await inspectInitializedPtyRoot(
platform,
rootPid,
runId,
controller.signal,
@@ -442,6 +449,7 @@ export const startProcessSampler = (options: {
}
const observation = await sampleProcesses({
rootPid,
platform,
elapsedMs: monotonicProcessCaptureClock() - started,
limit: limit + 1,
identities,
@@ -109,6 +109,7 @@ it.each(["plain", "all-cancel", "mixed-fd", "opaque-peer"] as const)(
throw new Error("Unexpected process signal");
});
const stop = startProcessSampler({
platform: process.platform,
rootPid: 42,
runId: "authored",
started: 0,
@@ -204,6 +204,7 @@ it.skipIf(process.platform === "win32")(
};
stopSampler = startProcessSampler({
platform: "freebsd",
resourceScope: createTestProcessCaptureResourceScope(),
rootPid: 100,
runId: "fixture-run-token",