From 2caedcfb57742753f010943b3459f7c1288db29f Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 17:42:23 +0800 Subject: [PATCH] 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. --- scripts/lib/module-layers.mjs | 1 + .../verify/architecture-guard-baselines.json | 142 ++++++++++++++++++ scripts/verify/architecture-guards.mjs | 19 +++ src/composition/binary.ts | 5 +- src/desktop/DesktopBuildDoctorFacts.ts | 2 +- src/platform/HostPlatform.ts | 23 +++ src/process/capture/ProcessHarness.ts | 3 + .../ProcessSampling.procReadCoverage.test.ts | 2 + src/process/capture/ProcessSampling.ts | 34 +++-- .../capture/ProcessSamplingShutdown.test.ts | 1 + ...cessObservationCommandFinalization.test.ts | 1 + 11 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 src/platform/HostPlatform.ts diff --git a/scripts/lib/module-layers.mjs b/scripts/lib/module-layers.mjs index f4c49b9bad..24bc97cbd4 100644 --- a/scripts/lib/module-layers.mjs +++ b/scripts/lib/module-layers.mjs @@ -29,6 +29,7 @@ export const UNIT_LAYERS = Object.freeze( "compression", "config", "filesystem", + "platform", "process", "windows", "generatedPackageMetadata.ts", diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 6482fc12fb..d45bbc4a88 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -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, diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index de42aef00c..b450dff5ba 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -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 diff --git a/src/composition/binary.ts b/src/composition/binary.ts index 54fd87e2b1..e5bc5fdd0c 100644 --- a/src/composition/binary.ts +++ b/src/composition/binary.ts @@ -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), ); }; diff --git a/src/desktop/DesktopBuildDoctorFacts.ts b/src/desktop/DesktopBuildDoctorFacts.ts index be0b8f396c..8b0f94a321 100644 --- a/src/desktop/DesktopBuildDoctorFacts.ts +++ b/src/desktop/DesktopBuildDoctorFacts.ts @@ -200,7 +200,7 @@ const settingCommand = async ( const psFact = async ( input: DesktopBuildFactsInput, ): Promise => { - const selected = await resolveSystemPs(process.platform); + const selected = await resolveSystemPs(input.platform); const ps = selected.source === "system" ? selected.command diff --git a/src/platform/HostPlatform.ts b/src/platform/HostPlatform.ts new file mode 100644 index 0000000000..d04b4185c7 --- /dev/null +++ b/src/platform/HostPlatform.ts @@ -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, +}); diff --git a/src/process/capture/ProcessHarness.ts b/src/process/capture/ProcessHarness.ts index 048b6174e7..863119d658 100644 --- a/src/process/capture/ProcessHarness.ts +++ b/src/process/capture/ProcessHarness.ts @@ -259,6 +259,7 @@ const createTerminalRenderer = ( interface StartCaptureRuntimeOptions { readonly scenario: ProcessScenario; readonly hostEnvironment: Readonly>; + 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, diff --git a/src/process/capture/ProcessSampling.procReadCoverage.test.ts b/src/process/capture/ProcessSampling.procReadCoverage.test.ts index 5e52f6fa7e..8c8f7fdd68 100644 --- a/src/process/capture/ProcessSampling.procReadCoverage.test.ts +++ b/src/process/capture/ProcessSampling.procReadCoverage.test.ts @@ -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, diff --git a/src/process/capture/ProcessSampling.ts b/src/process/capture/ProcessSampling.ts index 423192b1e3..62d9d73c3e 100644 --- a/src/process/capture/ProcessSampling.ts +++ b/src/process/capture/ProcessSampling.ts @@ -36,6 +36,7 @@ import { interface SampleProcessContext { readonly rootPid: number; + readonly platform: NodeJS.Platform; readonly elapsedMs: number; readonly limit: number; readonly identities: Map; @@ -135,10 +136,11 @@ const sampleLinux = async ( }; const readPsRowsRaw = async ( + platform: NodeJS.Platform, signal: AbortSignal, ): Promise => { 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> => samplingReadObservation( await readSamplingSource("process table (ps)", signal, () => - readPsRowsRaw(signal), + readPsRowsRaw(platform, signal), ), ); @@ -179,7 +182,7 @@ const sampleLinuxFallback = async ( context: SampleProcessContext, root: ProcessRow, ): Promise> => { - 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(); @@ -242,12 +245,12 @@ const sampleLinuxFallback = async ( }; const readRootIdentity = async ( + platform: NodeJS.Platform, rootPid: number, signal: AbortSignal, ): Promise> => { - 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(); @@ -345,11 +349,11 @@ const samplePs = async ( const sampleProcesses = async ( context: SampleProcessContext, ): Promise> => { - 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(); const lastObservations = new Map(); let pending: Promise | 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, diff --git a/src/process/capture/ProcessSamplingShutdown.test.ts b/src/process/capture/ProcessSamplingShutdown.test.ts index 1e298bc5a1..3c2515c4aa 100644 --- a/src/process/capture/ProcessSamplingShutdown.test.ts +++ b/src/process/capture/ProcessSamplingShutdown.test.ts @@ -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, diff --git a/tests/process-global/processObservationCommandFinalization.test.ts b/tests/process-global/processObservationCommandFinalization.test.ts index 589e18eaee..b99a242085 100644 --- a/tests/process-global/processObservationCommandFinalization.test.ts +++ b/tests/process-global/processObservationCommandFinalization.test.ts @@ -204,6 +204,7 @@ it.skipIf(process.platform === "win32")( }; stopSampler = startProcessSampler({ + platform: "freebsd", resourceScope: createTestProcessCaptureResourceScope(), rootPid: 100, runId: "fixture-run-token",