diff --git a/src/dotnet/ManagedNativeBoundaryHelpers.ts b/src/dotnet/ManagedNativeBoundaryHelpers.ts index 3dafe5dac..1e9908f42 100644 --- a/src/dotnet/ManagedNativeBoundaryHelpers.ts +++ b/src/dotnet/ManagedNativeBoundaryHelpers.ts @@ -320,19 +320,21 @@ const boundaryKind = ( return "mixed-or-unknown"; }; +/** CLI header facets of a PE without an admitted CLI header. */ +export const NO_CLI_NATIVE: ManagedNativeBoundaryInspection["cli_native"] = { + il_only: false, + requires_32bit: false, + strong_name_signed: false, + native_entry_point: false, + ready_to_run_signature: false, + managed_native_header_rva: 0, + managed_native_header_size: 0, +}; + export const cliNative = ( pe: ManagedPeLayout, ): ManagedNativeBoundaryInspection["cli_native"] => { - if (pe.cli === null) - return { - il_only: false, - requires_32bit: false, - strong_name_signed: false, - native_entry_point: false, - ready_to_run_signature: false, - managed_native_header_rva: 0, - managed_native_header_size: 0, - }; + if (pe.cli === null) return NO_CLI_NATIVE; return { il_only: (pe.cli.flags & 0x0000_0001) !== 0, requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0, @@ -344,6 +346,22 @@ export const cliNative = ( }; }; +/** Summarize declaration counts with the native facets of the CLI header. */ +export const nativeBoundarySummary = ( + native: ManagedNativeBoundaryInspection["cli_native"], + counts: Pick< + ManagedNativeBoundaryInspection["summary"], + "module_ref_count" | "pinvoke_import_count" | "native_implementation_count" + >, +): ManagedNativeBoundaryInspection["summary"] => ({ + ...counts, + ready_to_run: native.ready_to_run_signature, + mixed_mode_or_native_header: + native.managed_native_header_rva !== 0 || + native.managed_native_header_size !== 0 || + native.native_entry_point, +}); + interface BoundaryInspectionContext { readonly target: BinaryTarget; readonly bytes: Buffer; @@ -390,16 +408,11 @@ export const buildNativeBoundaryInspection = ({ module_refs: moduleRefs, pinvoke_imports: imports, native_implementations: implementations, - summary: { + summary: nativeBoundarySummary(native, { module_ref_count: moduleRefs.length, pinvoke_import_count: imports.length, native_implementation_count: implementations.length, - ready_to_run: native.ready_to_run_signature, - mixed_mode_or_native_header: - native.managed_native_header_rva !== 0 || - native.managed_native_header_size !== 0 || - native.native_entry_point, - }, + }), coverage: { state: issues.length === 0 ? "complete" : "partial", issues, diff --git a/src/dotnet/ManagedNativeBoundaryInspector.test.ts b/src/dotnet/ManagedNativeBoundaryInspector.test.ts index 978110590..70c49248d 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.test.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.test.ts @@ -116,6 +116,7 @@ describe("managed native boundaries", () => { const malformedBytes = buildManagedPeFixture({ corruptMetadataSignature: true, + readyToRun: true, }); const malformed = inspectManagedNativeBoundariesBytes( malformedBytes, @@ -123,10 +124,37 @@ describe("managed native boundaries", () => { ); expect(malformed).toMatchObject({ metadata: { status: "malformed" }, + cli_native: { + il_only: true, + ready_to_run_signature: true, + managed_native_header_rva: 0x2700, + managed_native_header_size: 4, + }, + summary: { ready_to_run: true, mixed_mode_or_native_header: true }, coverage: { state: "unavailable", issues: [expect.objectContaining({ code: "invalid-metadata-root" })], }, }); + expect(malformed.limitations).not.toContainEqual( + expect.stringContaining("CLI header was not admitted"), + ); + }); + + it("marks CLI header facets as defaults when the CLI header is unreadable", () => { + const bytes = buildManagedPeFixture(); + bytes.writeUInt32LE(0x7fff_0000, 0x84 + 20 + 96 + 14 * 8); + const result = inspectManagedNativeBoundariesBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result).toMatchObject({ + metadata: { status: "malformed" }, + cli_native: { il_only: false }, + coverage: { state: "unavailable", issues: [expect.anything()] }, + }); + expect(result.limitations).toContainEqual( + expect.stringContaining("CLI header was not admitted"), + ); }); }); diff --git a/src/dotnet/ManagedNativeBoundaryInspector.ts b/src/dotnet/ManagedNativeBoundaryInspector.ts index e3f82c7cb..b9d519ee7 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.ts @@ -20,6 +20,8 @@ import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; import { buildNativeBoundaryInspection, cliNative, + NO_CLI_NATIVE, + nativeBoundarySummary, nativeImplementations, parseFields, parseImplMaps, @@ -29,14 +31,25 @@ import { type Inventory = ReturnType; -/** Report a PE whose CLI data is absent or cannot be admitted. */ +/** + * Report a PE whose CLI metadata is absent or cannot be admitted. `native` + * holds the CLI header facts when the header itself was admitted; otherwise + * the header is absent (`not-managed`) or unreadable (`malformed`). + */ const emptyInspection = ( target: BinaryTarget, bytes: Buffer, classification: "not-managed" | "malformed", - issues: readonly ManagedParseIssue[] = [], -): ManagedNativeBoundaryInspection => - managedNativeBoundaryInspectionSchema.parse({ + { + native = null, + issues = [], + }: { + readonly native?: ManagedNativeBoundaryInspection["cli_native"] | null; + readonly issues?: readonly ManagedParseIssue[]; + } = {}, +): ManagedNativeBoundaryInspection => { + const header = native ?? NO_CLI_NATIVE; + return managedNativeBoundaryInspectionSchema.parse({ artifact: { path: target.path, sha256: target.sha256, @@ -54,31 +67,27 @@ const emptyInspection = ( requires_artifact_sha256: target.sha256, requires_mvid: null, }, - cli_native: { - il_only: false, - requires_32bit: false, - strong_name_signed: false, - native_entry_point: false, - ready_to_run_signature: false, - managed_native_header_rva: 0, - managed_native_header_size: 0, - }, + cli_native: header, module_refs: [], pinvoke_imports: [], native_implementations: [], - summary: { + summary: nativeBoundarySummary(header, { module_ref_count: 0, pinvoke_import_count: 0, native_implementation_count: 0, - ready_to_run: false, - mixed_mode_or_native_header: false, - }, + }), coverage: { state: "unavailable", issues }, limitations: [ - "No CLI data was admitted; native boundary declarations are unavailable.", + "No CLI metadata was admitted; native boundary declarations are unavailable.", + ...(native === null && classification === "malformed" + ? [ + "The CLI header was not admitted, so cli_native and the summary's ready_to_run and mixed_mode_or_native_header values are defaults, not observations.", + ] + : []), "Static inspection does not load or execute target code, so native export resolution is not performed.", ], }); +}; const readBoundaryInventory = ( bytes: Buffer, @@ -124,14 +133,16 @@ export const inspectManagedNativeBoundariesBytes = ( pe = readManagedPeLayout(bytes); } catch (cause: unknown) { if (!(cause instanceof ManagedReaderFailure)) throw cause; - return emptyInspection(target, bytes, "malformed", [cause.issue]); + return emptyInspection(target, bytes, "malformed", { + issues: [cause.issue], + }); } if (pe.cli === null) return emptyInspection( target, bytes, pe.cliDirectoryPresent ? "malformed" : "not-managed", - pe.cliIssue === null ? [] : [pe.cliIssue], + { issues: pe.cliIssue === null ? [] : [pe.cliIssue] }, ); let layout: ManagedMetadataLayout; let inventory: Inventory; @@ -139,7 +150,10 @@ export const inspectManagedNativeBoundariesBytes = ( ({ layout, inventory } = readBoundaryInventory(bytes, pe, pe.cli)); } catch (cause: unknown) { if (!(cause instanceof ManagedReaderFailure)) throw cause; - return emptyInspection(target, bytes, "malformed", [cause.issue]); + return emptyInspection(target, bytes, "malformed", { + native: cliNative(pe), + issues: [cause.issue], + }); } const heapExtent = Math.max(layout.strings.size, layout.blob.size); const moduleRefs = parseModuleRefs(bytes, layout, heapExtent);