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"); +});