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.
This commit is contained in:
morluto committed 2026-10-26 16:44:21 +08:00
1 parent 7adfcc4467
commit 6336fe5bb5
23 files changed
+482 -270

No files matched your search

+9 -2
View File
@@ -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");
@@ -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<Result<Evidence, AnalysisError>> {
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
? {}
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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."),
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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<Result<Evidence, AnalysisError>> {
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,
+18 -21
View File
@@ -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<Result<Evidence, AnalysisError>> {
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,
@@ -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();
}
});
+19 -26
View File
@@ -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);
};
+57
View File
@@ -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" },
});
},
);
+24
View File
@@ -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)),
});
};
+19
View File
@@ -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" }],
});
});
+15 -4
View File
@@ -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);
+9 -2
View File
@@ -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");
}
+11 -2
View File
@@ -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<NativeReadRequest, NativeSectionDirectory, Buffer> {
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 {
@@ -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",
},
});
},
);
@@ -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");
});