From 6336fe5bb5ca19754d76017458198d1140cd0476 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:44:21 +0800 Subject: [PATCH] refactor: consolidate binary analysis admission and format ownership Reuse the existing provider input admission helper across twelve services, preserving cancellation, schema errors, path policies, and Evidence handling. Share ELF dynamic records across Dart, MIPS, and IL2CPP readers, and reuse Mach-O segment and section records for Node SEA location. Require canonical DOS ownership before GPU PE section discovery so relocation bytes cannot masquerade as a PE pointer. Cover valid short DOS headers and the malformed case through both inventory and the public CLI. --- src/application/MipsElfSharedLoad.ts | 11 +++- .../apkSignature/ApkSignatureService.ts | 33 +++++------ .../binaryDiagnostics/AppleIpsService.ts | 35 ++++++------ .../binaryDiagnostics/ByteEntropyService.ts | 26 ++++----- .../binaryDiagnostics/DevicetreeService.ts | 39 ++++++------- .../binaryDiagnostics/ElfNotesService.ts | 37 ++++++------ .../binaryDiagnostics/KernelModuleService.ts | 37 ++++++------ .../binaryDiagnostics/NativeExportsService.ts | 37 ++++++------ .../PeCertificatesService.ts | 37 ++++++------ .../binaryDiagnostics/PeImportsService.ts | 35 ++++++------ .../binaryDiagnostics/PeLoadConfigService.ts | 35 ++++++------ .../binaryDiagnostics/PeResourcesService.ts | 39 ++++++------- src/application/python/PythonPycService.ts | 39 ++++++------- src/artifacts/NodeSeaArtifactReader.test.ts | 51 +++++++++++++++++ src/artifacts/nodeSea/NodeSeaLocation.ts | 45 +++++++-------- src/domain/elfDynamicEntry.test.ts | 57 +++++++++++++++++++ src/domain/elfDynamicEntry.ts | 24 ++++++++ src/flutter/DartElf.test.ts | 19 +++++++ src/flutter/DartElf.ts | 19 +++++-- src/il2cpp/ElfRelocations.ts | 11 +++- src/native/metadata/NativeSectionDirectory.ts | 13 ++++- tests/acceptance/gpuCodeObjectsCli.test.ts | 37 ++++++++++++ .../conformance/formats/mzPeAdmission.test.ts | 36 ++++++++++++ 23 files changed, 482 insertions(+), 270 deletions(-) create mode 100644 src/domain/elfDynamicEntry.test.ts create mode 100644 src/domain/elfDynamicEntry.ts diff --git a/src/application/MipsElfSharedLoad.ts b/src/application/MipsElfSharedLoad.ts index 2fe8b68ff9..5e82e95f0a 100644 --- a/src/application/MipsElfSharedLoad.ts +++ b/src/application/MipsElfSharedLoad.ts @@ -1,3 +1,4 @@ +import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js"; import { readElfProgramHeader } from "../domain/elfProgramHeader.js"; import type { MipsElfMetadata } from "../domain/binaryTargetTypes.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -127,14 +128,20 @@ export const readMipsElfSharedLoad = async ( if (!chunk.ok) return chunk; for (let position = 0; position < chunk.value.length; position += 8) { checkCancelled(); - const tag = u32(chunk.value, position); + const parsedEntry = readElfDynamicEntry( + chunk.value.subarray(position, position + 8), + 32, + little, + ); + if (!parsedEntry.ok) return err(parsedEntry.error.message); + const tag = Number(BigInt.asUintN(32, parsedEntry.value.tag)); if (tag === 0) { terminated = true; break; } if (!relevant.has(tag)) continue; if (tags.has(tag)) return err(`duplicate MIPS dynamic tag ${tag}`); - tags.set(tag, u32(chunk.value, position + 4)); + tags.set(tag, Number(parsedEntry.value.value)); } } if (!terminated) return err("MIPS dynamic table has no DT_NULL terminator"); diff --git a/src/application/apkSignature/ApkSignatureService.ts b/src/application/apkSignature/ApkSignatureService.ts index bc6bbefac4..bb98c31227 100644 --- a/src/application/apkSignature/ApkSignatureService.ts +++ b/src/application/apkSignature/ApkSignatureService.ts @@ -10,7 +10,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ApkSignaturePort } from "./ApkSignaturePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -19,9 +18,11 @@ import { apkSignatureResultSchema, } from "../../domain/apkSignature/apkSignatureReport.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_apk_signature"; @@ -42,18 +43,14 @@ export class ApkSignatureService { recordEvidence: undefined, }, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectApkSignatureInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectApkSignatureInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -63,7 +60,7 @@ export class ApkSignatureService { }, ]), ); - const { detail, view, ...parameters } = input.data; + const { detail, view, ...parameters } = input.value; if (detail === "summary") { const ready = requireAnalysisViewRetention(OPERATION, retention); if (!ready.ok) return ready; @@ -74,7 +71,7 @@ export class ApkSignatureService { const report = apkSignatureResultSchema.safeParse(execution.result); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !apkSignatureMatchesSubject(report.data.artifact, execution.subject) ) return err( @@ -86,7 +83,7 @@ export class ApkSignatureService { const completed = completeProviderEvidence( createEvidence(execution.subject, execution.provider, { operation: OPERATION, - parameters: { path: input.data.path }, + parameters: { path: input.value.path }, result: report.data, ...(execution.rawResult === null ? {} diff --git a/src/application/binaryDiagnostics/AppleIpsService.ts b/src/application/binaryDiagnostics/AppleIpsService.ts index de4dfb31ba..3581712eeb 100644 --- a/src/application/binaryDiagnostics/AppleIpsService.ts +++ b/src/application/binaryDiagnostics/AppleIpsService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AppleIpsPort } from "./AppleIpsPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -14,9 +13,11 @@ import { inspectAppleIpsInputSchema, type AppleIps, } from "../../domain/native/appleIps.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_apple_ips"; @@ -32,18 +33,14 @@ export class AppleIpsService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectAppleIpsInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectAppleIpsInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -53,13 +50,13 @@ export class AppleIpsService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = appleIpsSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - !validReport(report.data, input.data) + report.data.artifact.path !== input.value.path || + !validReport(report.data, input.value) ) return err( new AnalysisOutputError( @@ -78,7 +75,7 @@ export class AppleIpsService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/ByteEntropyService.ts b/src/application/binaryDiagnostics/ByteEntropyService.ts index b565feb4b9..c3cd75dc1a 100644 --- a/src/application/binaryDiagnostics/ByteEntropyService.ts +++ b/src/application/binaryDiagnostics/ByteEntropyService.ts @@ -14,7 +14,6 @@ import { AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { byteEntropySchema, computeBlockEntropy, @@ -37,7 +36,10 @@ import { import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { sameRegularFileState } from "../../filesystem/RegularFile.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; /** Bound retained result rows before constructing the Evidence representation. */ export const MAX_BYTE_ENTROPY_BLOCKS = 32_768; @@ -97,18 +99,14 @@ export class ByteEntropyService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const parsed = inspectByteEntropyInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput, { - cause: parsed.error, - }), - ); - const input = parsed.data; + const parsed = parseProviderOperationInput( + OPERATION, + rawInput, + inspectByteEntropyInputSchema, + options?.signal, + ); + if (!parsed.ok) return parsed; + const input = parsed.value; if (!isAbsolute(input.path)) return err( invalid("path", "Expected an absolute filesystem path on this host."), diff --git a/src/application/binaryDiagnostics/DevicetreeService.ts b/src/application/binaryDiagnostics/DevicetreeService.ts index 424719b6f3..af2f76aff0 100644 --- a/src/application/binaryDiagnostics/DevicetreeService.ts +++ b/src/application/binaryDiagnostics/DevicetreeService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { DevicetreePort } from "./DevicetreePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,9 +12,11 @@ import { devicetreeSchema, type Devicetree, } from "../../domain/firmware/devicetree.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_devicetree"; @@ -31,18 +32,14 @@ export class DevicetreeService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectDevicetreeInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectDevicetreeInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -52,15 +49,15 @@ export class DevicetreeService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = devicetreeSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - report.data.artifact.bytes > input.data.max_file_bytes || - report.data.nodes.length > input.data.max_nodes || - report.data.coverage.examined_properties > input.data.max_properties || + report.data.artifact.path !== input.value.path || + report.data.artifact.bytes > input.value.max_file_bytes || + report.data.nodes.length > input.value.max_nodes || + report.data.coverage.examined_properties > input.value.max_properties || !validReport(report.data) ) return err( @@ -83,7 +80,7 @@ export class DevicetreeService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/ElfNotesService.ts b/src/application/binaryDiagnostics/ElfNotesService.ts index 4bb19dbcf1..b33ac1cf4d 100644 --- a/src/application/binaryDiagnostics/ElfNotesService.ts +++ b/src/application/binaryDiagnostics/ElfNotesService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ElfNotesPort } from "./ElfNotesPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,11 +12,13 @@ import { elfNotesSchema, type ElfNotes, } from "../../domain/native/elfNotes.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js"; import { elfArchitecture } from "../../domain/elfFixedHeader.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_elf_notes"; @@ -33,18 +34,14 @@ export class ElfNotesService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectElfNotesInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectElfNotesInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -54,16 +51,16 @@ export class ElfNotesService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = elfNotesSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !validReport( report.data, - input.data.max_entries, - input.data.max_file_bytes, + input.value.max_entries, + input.value.max_file_bytes, ) ) return err( @@ -95,7 +92,7 @@ export class ElfNotesService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/KernelModuleService.ts b/src/application/binaryDiagnostics/KernelModuleService.ts index 371debbfdc..0122071e0c 100644 --- a/src/application/binaryDiagnostics/KernelModuleService.ts +++ b/src/application/binaryDiagnostics/KernelModuleService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { KernelModulePort } from "./KernelModulePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -19,11 +18,13 @@ import { MODULE_SIGNATURE_PKCS7_ID_TYPE, type KernelModule, } from "../../domain/native/kernelModule.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js"; import { elfArchitecture } from "../../domain/elfFixedHeader.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_kernel_module"; @@ -41,18 +42,14 @@ export class KernelModuleService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectKernelModuleInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectKernelModuleInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -62,16 +59,16 @@ export class KernelModuleService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = kernelModuleSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !validReport( report.data, - input.data.max_entries, - input.data.max_file_bytes, + input.value.max_entries, + input.value.max_file_bytes, ) ) return err( @@ -105,7 +102,7 @@ export class KernelModuleService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/NativeExportsService.ts b/src/application/binaryDiagnostics/NativeExportsService.ts index 19edc83001..4ac52072aa 100644 --- a/src/application/binaryDiagnostics/NativeExportsService.ts +++ b/src/application/binaryDiagnostics/NativeExportsService.ts @@ -1,18 +1,17 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { NativeExportsPort } from "./NativeExportsPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { inspectNativeExportsInputSchema, nativeExportsSchema, } from "../../domain/native/nativeExports.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_native_exports"; @@ -29,24 +28,20 @@ export class NativeExportsService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectNativeExportsInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - const result = await this.provider.inspect(input.data, options); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectNativeExportsInputSchema, + options?.signal, + ); + if (!input.ok) return input; + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = nativeExportsSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - report.data.artifact.bytes > input.data.max_file_bytes + report.data.artifact.path !== input.value.path || + report.data.artifact.bytes > input.value.max_file_bytes ) return err( new AnalysisOutputError( @@ -65,7 +60,7 @@ export class NativeExportsService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/PeCertificatesService.ts b/src/application/binaryDiagnostics/PeCertificatesService.ts index d39c80afca..9faf5f53bd 100644 --- a/src/application/binaryDiagnostics/PeCertificatesService.ts +++ b/src/application/binaryDiagnostics/PeCertificatesService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PeCertificatesPort } from "./PeCertificatesPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,10 +12,12 @@ import { peCertificatesSchema, type PeCertificates, } from "../../domain/native/peCertificates.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { peSubjectArchitecture } from "../../domain/peMachine.js"; import { err, ok, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_pe_certificates"; @@ -32,18 +33,14 @@ export class PeCertificatesService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectPeCertificatesInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectPeCertificatesInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -53,16 +50,16 @@ export class PeCertificatesService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = peCertificatesSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !validReport( report.data, - input.data.max_entries, - input.data.max_file_bytes, + input.value.max_entries, + input.value.max_file_bytes, ) ) return err( @@ -96,7 +93,7 @@ export class PeCertificatesService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/PeImportsService.ts b/src/application/binaryDiagnostics/PeImportsService.ts index 2a485334e3..0cb2f55eb1 100644 --- a/src/application/binaryDiagnostics/PeImportsService.ts +++ b/src/application/binaryDiagnostics/PeImportsService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PeImportsPort } from "./PeImportsPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,10 +12,12 @@ import { peImportsSchema, type PeImports, } from "../../domain/native/peImports.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { peSubjectArchitecture } from "../../domain/peMachine.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_pe_imports"; @@ -32,18 +33,14 @@ export class PeImportsService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectPeImportsInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectPeImportsInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -53,13 +50,13 @@ export class PeImportsService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = peImportsSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - !validReport(report.data, input.data) + report.data.artifact.path !== input.value.path || + !validReport(report.data, input.value) ) return err( new AnalysisOutputError( @@ -85,7 +82,7 @@ export class PeImportsService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/PeLoadConfigService.ts b/src/application/binaryDiagnostics/PeLoadConfigService.ts index 7137a35d59..bb4e322b0d 100644 --- a/src/application/binaryDiagnostics/PeLoadConfigService.ts +++ b/src/application/binaryDiagnostics/PeLoadConfigService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PeLoadConfigPort } from "./PeLoadConfigPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,10 +12,12 @@ import { peLoadConfigSchema, type PeLoadConfig, } from "../../domain/native/peLoadConfig.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { peSubjectArchitecture } from "../../domain/peMachine.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_pe_load_config"; @@ -32,18 +33,14 @@ export class PeLoadConfigService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectPeLoadConfigInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectPeLoadConfigInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -53,13 +50,13 @@ export class PeLoadConfigService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = peLoadConfigSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - !validReport(report.data, input.data) + report.data.artifact.path !== input.value.path || + !validReport(report.data, input.value) ) return err( new AnalysisOutputError( @@ -93,7 +90,7 @@ export class PeLoadConfigService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/binaryDiagnostics/PeResourcesService.ts b/src/application/binaryDiagnostics/PeResourcesService.ts index c3f45e05b9..c0b0f6a2f9 100644 --- a/src/application/binaryDiagnostics/PeResourcesService.ts +++ b/src/application/binaryDiagnostics/PeResourcesService.ts @@ -1,10 +1,7 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PeResourcesPort } from "./PeResourcesPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { inspectPeResourcesInputSchema, @@ -12,10 +9,12 @@ import { type PeResources, type PeResourceIdentity, } from "../../domain/native/peResources.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { peSubjectArchitecture } from "../../domain/peMachine.js"; import { err, ok, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; /** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */ export class PeResourcesService { @@ -30,27 +29,23 @@ export class PeResourcesService { options?: ExecutionOptions, ): Promise> { const operation = "inspect_pe_resources"; - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(operation, { cause: options.signal.reason }), - ); - const input = inspectPeResourcesInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(operation, input.error.issues, rawInput, { - cause: input.error, - }), - ); - const result = await this.provider.inspect(input.data, options); + const input = parseProviderOperationInput( + operation, + rawInput, + inspectPeResourcesInputSchema, + options?.signal, + ); + if (!input.ok) return input; + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = peResourcesSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !validReport( report.data, - input.data.max_entries, - input.data.max_file_bytes, + input.value.max_entries, + input.value.max_file_bytes, ) ) return err( @@ -72,7 +67,7 @@ export class PeResourcesService { this.provider.identity, { operation, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/python/PythonPycService.ts b/src/application/python/PythonPycService.ts index ef04a9de44..4cc8e2e57f 100644 --- a/src/application/python/PythonPycService.ts +++ b/src/application/python/PythonPycService.ts @@ -3,7 +3,6 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PythonPycPort } from "./PythonPycPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; @@ -13,9 +12,11 @@ import { PYTHON_PYC_HEADER_BYTES, pythonPycSchema, } from "../../domain/python/pythonPyc.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_python_pyc"; @@ -31,18 +32,14 @@ export class PythonPycService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectPythonPycInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - if (!isAbsolute(input.data.path)) + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectPythonPycInputSchema, + options?.signal, + ); + if (!input.ok) return input; + if (!isAbsolute(input.value.path)) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -52,15 +49,15 @@ export class PythonPycService { }, ]), ); - const result = await this.provider.inspect(input.data, options); + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = pythonPycSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || - report.data.artifact.bytes > input.data.max_file_bytes || - report.data.coverage.objects > input.data.max_objects || - report.data.coverage.marshal_depth > input.data.max_depth + report.data.artifact.path !== input.value.path || + report.data.artifact.bytes > input.value.max_file_bytes || + report.data.coverage.objects > input.value.max_objects || + report.data.coverage.marshal_depth > input.value.max_depth ) return err( new AnalysisOutputError( @@ -79,7 +76,7 @@ export class PythonPycService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/artifacts/NodeSeaArtifactReader.test.ts b/src/artifacts/NodeSeaArtifactReader.test.ts index cdf651242d..fe8a455baa 100644 --- a/src/artifacts/NodeSeaArtifactReader.test.ts +++ b/src/artifacts/NodeSeaArtifactReader.test.ts @@ -677,3 +677,54 @@ it.each(["cancel", "close"] as const)( } }, ); + +it.each(["__TEXT", "NODE_SEA"])( + "keeps an incomplete %s segment unrecognized before SEA admission", + async (name) => { + const bytes = Buffer.alloc(32 + 24); + bytes.writeUInt32LE(0xfeedfacf, 0); + bytes.writeUInt32LE(0x0100000c, 4); + bytes.writeUInt32LE(1, 16); + bytes.writeUInt32LE(24, 20); + bytes.writeUInt32LE(0x19, 32); + bytes.writeUInt32LE(24, 36); + bytes.write(name, 40, "latin1"); + const handle = await open( + await writeCarrier("incomplete-segment", bytes), + "r", + ); + try { + expect(await locateNodeSea(handle, bytes.length)).toBeUndefined(); + } finally { + await handle.close(); + } + }, +); + +it("retains SEA admission for a complete segment with a nonaligned command size", async () => { + const original = nodeSeaMachO(); + const commandSize = original.readUInt32LE(20); + const payloadAt = 32 + commandSize; + const bytes = Buffer.concat([ + original.subarray(0, payloadAt), + Buffer.alloc(1), + original.subarray(payloadAt), + ]); + bytes.writeUInt32LE(commandSize + 1, 20); + bytes.writeUInt32LE(commandSize + 1, 32 + 4); + bytes.writeBigUInt64LE(BigInt(payloadAt + 1), 32 + 40); + bytes.writeUInt32LE(payloadAt + 1, 32 + 72 + 48); + const handle = await open( + await writeCarrier("unaligned-command", bytes), + "r", + ); + try { + expect(await locateNodeSea(handle, bytes.length)).toEqual({ + carrier: "mach-o", + blobOffset: payloadAt + 1, + blobLength: Number(original.readBigUInt64LE(32 + 72 + 40)), + }); + } finally { + await handle.close(); + } +}); diff --git a/src/artifacts/nodeSea/NodeSeaLocation.ts b/src/artifacts/nodeSea/NodeSeaLocation.ts index 4be4ce3638..2b5a5b1306 100644 --- a/src/artifacts/nodeSea/NodeSeaLocation.ts +++ b/src/artifacts/nodeSea/NodeSeaLocation.ts @@ -1,3 +1,5 @@ +import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js"; +import { readMachoSectionHeader } from "../../domain/apple/machoSectionHeader.js"; import { readElfSectionHeader } from "../../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../../domain/elfProgramHeader.js"; import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js"; @@ -273,12 +275,15 @@ const locateThinMachO = async ( (name === "NODE_SEA" || name === "__NODE_SEA") && cmdsize >= segmentHeader ) { - const nsects = readU32( - commands, - offset + (segment64 ? 64 : 48), + const parsedSegment = readMachoSegmentHeader( + commands.subarray(offset, offset + cmdsize), + segment64, little, ); - const table = offset + segmentHeader; + if (!parsedSegment.ok) + throw formatFailure(NODE_SEA_MACHO_SEGMENT_TRUNCATED); + const nsects = parsedSegment.value.sectionCount; + const table = offset + parsedSegment.value.headerSize; if ( nsects > Math.floor((cmdsize - segmentHeader) / sectionSize) || table + nsects * sectionSize > offset + cmdsize @@ -286,17 +291,18 @@ const locateThinMachO = async ( throw formatFailure(NODE_SEA_MACHO_SEGMENT_TRUNCATED); for (let index = 0; index < nsects; index += 1) { const section = table + index * sectionSize; - if (cString(commands, section, 16) !== SEA_SECTION) continue; - const length = segment64 - ? readU64(commands, section + 40, little) - : readU32(commands, section + 36, little); - const fileOffset = readU32( - commands, - section + (segment64 ? 48 : 40), + const parsedSection = readMachoSectionHeader( + commands.subarray(section, section + sectionSize), + segment64, little, ); - if (length === undefined) throw limitFailure(NODE_SEA_BLOB_TOO_LARGE); - if (length > NODE_SEA_MAX_BLOB_BYTES) + if (!parsedSection.ok) + throw formatFailure(NODE_SEA_MACHO_SEGMENT_TRUNCATED); + const record = parsedSection.value; + if (cString(record.nameBytes, 0, 16) !== SEA_SECTION) continue; + const length = Number(record.size); + const fileOffset = record.offset; + if (!Number.isSafeInteger(length) || length > NODE_SEA_MAX_BLOB_BYTES) throw limitFailure(NODE_SEA_BLOB_TOO_LARGE); if (fileOffset > sliceSize || length > sliceSize - fileOffset) throw formatFailure(NODE_SEA_TRUNCATED); @@ -676,16 +682,3 @@ const cString = (bytes: Buffer, offset: number, length: number): string => { const readU32 = (bytes: Buffer, offset: number, little: boolean): number => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); - -const readU64 = ( - bytes: Buffer, - offset: number, - little: boolean, -): number | undefined => { - if (offset < 0 || offset > bytes.length - 8) return undefined; - const value = little - ? bytes.readBigUInt64LE(offset) - : bytes.readBigUInt64BE(offset); - if (value > BigInt(Number.MAX_SAFE_INTEGER)) return undefined; - return Number(value); -}; diff --git a/src/domain/elfDynamicEntry.test.ts b/src/domain/elfDynamicEntry.test.ts new file mode 100644 index 0000000000..8b194bdf65 --- /dev/null +++ b/src/domain/elfDynamicEntry.test.ts @@ -0,0 +1,57 @@ +import { expect, it } from "vitest"; +import { readElfDynamicEntry } from "./elfDynamicEntry.js"; + +const formats = [ + { bits: 32, littleEndian: false }, + { bits: 32, littleEndian: true }, + { bits: 64, littleEndian: false }, + { bits: 64, littleEndian: true }, +] as const; + +it.each(formats)( + "preserves signed ELF$bits tags and unsigned values (littleEndian=$littleEndian)", + ({ bits, littleEndian }) => { + const wide = bits === 64; + const length = wide ? 16 : 8; + const backing = Buffer.alloc(length + 19, 0xa5); + const bytes = backing.subarray(7, 7 + length); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const tag = wide ? -0x8000000000000000n : -0x80000000n; + const value = wide ? 0xffffffffffffffffn : 0xffffffffn; + if (wide) { + view.setBigInt64(0, tag, littleEndian); + view.setBigUint64(8, value, littleEndian); + } else { + view.setInt32(0, Number(tag), littleEndian); + view.setUint32(4, Number(value), littleEndian); + } + const before = Buffer.from(backing); + const parsed = readElfDynamicEntry(backing.subarray(7), bits, littleEndian); + expect(backing.equals(before)).toBe(true); + backing.fill(0); + expect(parsed).toEqual({ ok: true, value: { tag, value } }); + expect(readElfDynamicEntry(bytes, bits, littleEndian)).toEqual({ + ok: true, + value: { tag: 0n, value: 0n }, + }); + }, +); + +it.each(formats)( + "refuses incomplete ELF$bits dynamic entries (littleEndian=$littleEndian)", + ({ bits, littleEndian }) => { + const length = bits === 64 ? 16 : 8; + const backing = Buffer.alloc(length + 7); + for (let observed = 0; observed < length; observed++) + expect( + readElfDynamicEntry( + backing.subarray(7, 7 + observed), + bits, + littleEndian, + ), + ).toEqual({ + ok: false, + error: { kind: "malformed", message: "ELF dynamic entry is truncated" }, + }); + }, +); diff --git a/src/domain/elfDynamicEntry.ts b/src/domain/elfDynamicEntry.ts new file mode 100644 index 0000000000..48831c3690 --- /dev/null +++ b/src/domain/elfDynamicEntry.ts @@ -0,0 +1,24 @@ +import { err, ok } from "./result.js"; + +/** Fixed Elf32_Dyn/Elf64_Dyn: signed d_tag and unsigned d_un, without tag or value admission. */ +export const readElfDynamicEntry = ( + bytes: Uint8Array, + bits: 32 | 64, + littleEndian: boolean, +) => { + const wide = bits === 64; + if (bytes.byteLength < (wide ? 16 : 8)) + return err({ + kind: "malformed" as const, + message: "ELF dynamic entry is truncated", + }); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + return ok({ + tag: wide + ? view.getBigInt64(0, littleEndian) + : BigInt(view.getInt32(0, littleEndian)), + value: wide + ? view.getBigUint64(8, littleEndian) + : BigInt(view.getUint32(4, littleEndian)), + }); +}; diff --git a/src/flutter/DartElf.test.ts b/src/flutter/DartElf.test.ts index 23940d0889..d1d00b0065 100644 --- a/src/flutter/DartElf.test.ts +++ b/src/flutter/DartElf.test.ts @@ -585,3 +585,22 @@ it("keeps section precision checks on consumed declarations and mapping fields", ], }); }); + +it("keeps ignored DT_NULL payloads separate from unsigned tag precision admission", async () => { + const names = ["_kDartVmSnapshotData"]; + const control = await readElfSymbols(strippedImageWithSections(), names); + expect(control.failures).toEqual([]); + expect( + await readElfSymbols( + strippedImageWithSections([[0, 0xffffffffffffffffn]]), + names, + ), + ).toEqual(control); + const bytes = strippedImageWithSections([[16, 0n]]); + const table = Number(bytes.readBigUInt64LE(64 + 56 + 8)); + bytes.writeBigInt64LE(-1n, table + 5 * 16); + expect(await readElfSymbols(bytes, names)).toEqual({ + symbols: [], + failures: [{ reason: "Dynamic entry is not representable" }], + }); +}); diff --git a/src/flutter/DartElf.ts b/src/flutter/DartElf.ts index 96113e2996..7da95184c3 100644 --- a/src/flutter/DartElf.ts +++ b/src/flutter/DartElf.ts @@ -1,3 +1,4 @@ +import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js"; import { readElfSectionHeader } from "../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../domain/elfProgramHeader.js"; import { readElfSymbol } from "../domain/elfSymbol.js"; @@ -344,8 +345,17 @@ export const readElfSymbols = async ( await yieldToEventLoop(); signal?.throwIfAborted(); } - const tag = is64 ? reader.u64(base) : reader.u32(base); - if (tag === null) return failure("Dynamic entry is not representable"); + const parsedEntry = readElfDynamicEntry( + bytes.subarray(base, base + dynamicEntrySize), + is64 ? 64 : 32, + true, + ); + if (!parsedEntry.ok) return failure(parsedEntry.error.message); + // This reader historically admits tag identities as unsigned, safe numbers. + const tagBits = BigInt.asUintN(is64 ? 64 : 32, parsedEntry.value.tag); + if (tagBits > BigInt(Number.MAX_SAFE_INTEGER)) + return failure("Dynamic entry is not representable"); + const tag = Number(tagBits); // ELF defines d_un as ignored for DT_NULL; validate the complete entry // above, but do not interpret a value that has no meaning. if (tag === DT_NULL) { @@ -366,8 +376,9 @@ export const readElfSymbols = async ( ].includes(tag) ) continue; - const value = is64 ? reader.u64(base + 8) : reader.u32(base + 4); - if (value === null) return failure("Dynamic entry is not representable"); + if (parsedEntry.value.value > BigInt(Number.MAX_SAFE_INTEGER)) + return failure("Dynamic entry is not representable"); + const value = Number(parsedEntry.value.value); if (tags.has(tag) && tags.get(tag) !== value) return failure("Conflicting dynamic table tags"); tags.set(tag, value); diff --git a/src/il2cpp/ElfRelocations.ts b/src/il2cpp/ElfRelocations.ts index 59174ea338..b36e8eed8a 100644 --- a/src/il2cpp/ElfRelocations.ts +++ b/src/il2cpp/ElfRelocations.ts @@ -1,3 +1,4 @@ +import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js"; import { BinaryMappingFailure, mappedFileOffset, @@ -113,11 +114,17 @@ function* dynamicTags( ]); for (let at = dynamic.offset; at < dynamic.offset + dynamic.size; at += 16) { if ((at - dynamic.offset) % 65536 === 0) yield; - const tag = image.readBigInt64LE(at); + const parsedEntry = readElfDynamicEntry( + image.subarray(at, at + 16), + 64, + true, + ); + if (!parsedEntry.ok) throw malformed(parsedEntry.error.message); + const { tag, value } = parsedEntry.value; if (tag === 0n) return tags; if (tags.has(tag) && unique.has(tag)) throw malformed(`duplicate dynamic tag ${tag}`); - tags.set(tag, image.readBigUInt64LE(at + 8)); + tags.set(tag, value); } throw malformed("PT_DYNAMIC has no DT_NULL terminator"); } diff --git a/src/native/metadata/NativeSectionDirectory.ts b/src/native/metadata/NativeSectionDirectory.ts index 5bb75c67d6..13a1e2f409 100644 --- a/src/native/metadata/NativeSectionDirectory.ts +++ b/src/native/metadata/NativeSectionDirectory.ts @@ -1,3 +1,4 @@ +import { observeMzWindowsHeader } from "../../domain/dosMz.js"; import { readElfSectionHeader } from "../../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../../domain/elfProgramHeader.js"; import { readPeCoffHeader } from "../../domain/peCoffHeader.js"; @@ -285,10 +286,18 @@ export function* readPeSectionDirectory( ): Generator { const dos = new BinaryReader(yield request(fileSize, 0n, 64n), true); if (dos.u16(0) !== 0x5a4d) reject("Missing DOS MZ header"); - const pe = dos.u32(60); + const observed = observeMzWindowsHeader(dos.bytes, fileSize); + if (observed.state === "absent" || observed.state === "header-needed") + return reject("MZ header does not declare a PE image"); + const pe = observed.offset; if (pe < 64) reject("PE signature overlaps the DOS header"); const coff = new BinaryReader(yield request(fileSize, BigInt(pe), 24n), true); - if (coff.u32(0) !== 0x4550) reject("PE signature is invalid"); + const confirmed = observeMzWindowsHeader(dos.bytes, fileSize, { + offset: pe, + bytes: coff.bytes, + }); + if (confirmed.state !== "declared" || confirmed.signature !== "PE") + reject("PE signature is invalid"); const parsedCoff = readPeCoffHeader(coff.bytes.subarray(4, 24)); if (!parsedCoff.ok) return reject(parsedCoff.error.message); const { diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts index 7d9c3e73ad..652219d235 100644 --- a/tests/acceptance/gpuCodeObjectsCli.test.ts +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -16,6 +16,7 @@ import { parseMcpToolError } from "../fixtures/mcpToolError.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { cudaFixture, + peGpuFixture, ccobFixture, ptxFixture, amdFixture, @@ -1149,3 +1150,39 @@ it("rejects forged duplicate object IDs and mismatched Evidence subjects through ); expect(identityFailure.error).toMatchObject({ code: "invalid_request" }); }); + +cliTest( + "uses DOS relocation ownership before GPU section discovery through the CLI", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-gpu-dos-relocations-cli-"); + const path = join(root, "selected.exe"); + const bytes = peGpuFixture(); + bytes.writeUInt16LE(bytes.length % 512, 2); + bytes.writeUInt16LE(Math.ceil(bytes.length / 512), 4); + bytes.writeUInt16LE(2, 8); + await writeFile(path, bytes); + const args = [ + "inspect-gpu-code-objects", + path, + "--analysis", + "inventory", + "--json", + ]; + const valid = await cli.run({ arguments: args }); + expect(valid.exitCode, valid.stderr).toBe(0); + bytes.writeUInt16LE(1, 6); + bytes.writeUInt16LE(4, 8); + bytes.writeUInt16LE(60, 24); + await writeFile(path, bytes); + const refused = await cli.run({ arguments: args }); + expect(refused.exitCode).toBe(1); + expect(refused.json).toMatchObject({ + code: "target_unavailable", + details: { + path, + constraint: "file_format_rejected", + reason: "MZ header does not declare a PE image", + }, + }); + }, +); diff --git a/tests/conformance/formats/mzPeAdmission.test.ts b/tests/conformance/formats/mzPeAdmission.test.ts index b7e2758ac4..110ea9616c 100644 --- a/tests/conformance/formats/mzPeAdmission.test.ts +++ b/tests/conformance/formats/mzPeAdmission.test.ts @@ -1,3 +1,5 @@ +import { GpuInventory } from "../../../src/gpu/GpuInventory.js"; +import { GpuBufferSource } from "../../../src/gpu/GpuSource.js"; import { open, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; @@ -216,3 +218,37 @@ it("keeps a PE signature outside an inspected prefix unresolved until its requir "PE range 128 + 24 leaves the artifact.", ); }); + +it("does not inventory PE GPU sections from relocation-owned DOS bytes", async () => { + const bytes = relocationOwnedPointer(peResourceFixture().bytes); + const buffer = new GpuBufferSource(bytes, null); + const reads: { offset: number; size: number }[] = []; + const source = { + size: buffer.size, + containerId: buffer.containerId, + read: (offset: number, size: number) => { + reads.push({ offset, size }); + return buffer.read(offset, size); + }, + }; + const inventory = new GpuInventory({ + path: "/fixture", + analysis: "inventory", + }); + await expect(inventory.inspect(source)).rejects.toThrow(/PE/u); + expect(reads).toEqual([ + { offset: 0, size: 32 }, + { offset: 0, size: 64 }, + ]); +}); + +it("preserves PE GPU section inventory after a short DOS header with a confirmed signature", async () => { + const bytes = shortDosHeader(peResourceFixture().bytes); + const inventory = new GpuInventory({ + path: "/fixture", + analysis: "inventory", + }); + await expect( + inventory.inspect(new GpuBufferSource(bytes, null)), + ).resolves.toBe("pe"); +});