diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index a518f97572..6bc303b004 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -504,6 +504,13 @@ characters. These candidates follow [Microsoft's native library loading rules](h These filename rules compare declared and observed identities; they do not establish which library the runtime loaded. +Signatures retain their decoded fields, but a Field or MethodDef row whose +signature has the other member kind is malformed and cannot establish an +unchanged signature. Runtime-provided methods are counted separately in +`native_implementations.runtime_provided`; they have no file-backed native body +for this workflow to map and do not increase `native_body_unresolved`. Historical +verification Evidence without this separate count leaves it unknown (`null`). + An empty declared import name remains unchanged in verification Evidence and produces no native-name candidates. With a supplied PE native context and `no_mangle: false`, an unspecified charset includes the ANSI `A` candidate; diff --git a/src/domain/managed/managedNativeVerificationMatch.ts b/src/domain/managed/managedNativeVerificationMatch.ts index d87e376579..240e8a6456 100644 --- a/src/domain/managed/managedNativeVerificationMatch.ts +++ b/src/domain/managed/managedNativeVerificationMatch.ts @@ -542,7 +542,8 @@ export const buildVerificationResult = ({ > => { const counts = countStatuses(pinvokeImports); const nativeBodyUnresolved = managed.native_implementations.filter( - ({ boundary_kind: kind }) => kind !== "pinvoke", + ({ boundary_kind: kind }) => + kind !== "pinvoke" && kind !== "runtime-provided", ).length; const coverage: ManagedNativeVerificationResult["coverage"] = { status: @@ -578,6 +579,9 @@ export const buildVerificationResult = ({ }, pinvoke_imports: [...pinvokeImports], native_implementations: { + runtime_provided: managed.native_implementations.filter( + ({ boundary_kind }) => boundary_kind === "runtime-provided", + ).length, unresolved: nativeBodyUnresolved, reason: "Managed metadata tokens and RVAs are not translated to native provider addresses by this workflow; C++/CLI, ReadyToRun, and native-body mappings require explicit provider-supported bridge evidence.", diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts index 266aefb5b4..8d44f8e924 100644 --- a/src/domain/managed/managedNativeVerificationSchemas.ts +++ b/src/domain/managed/managedNativeVerificationSchemas.ts @@ -169,6 +169,7 @@ export const managedNativeVerificationResultSchema = z }), pinvoke_imports: z.array(pinvokeVerificationSchema), native_implementations: z.strictObject({ + runtime_provided: z.int().nonnegative().nullable().default(null), unresolved: z.int().min(0), reason: textSchema, }), diff --git a/src/dotnet/ManagedArtifactFailures.test.ts b/src/dotnet/ManagedArtifactFailures.test.ts index 3a763bc861..68a6247904 100644 --- a/src/dotnet/ManagedArtifactFailures.test.ts +++ b/src/dotnet/ManagedArtifactFailures.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "vitest"; +import * as inventoryProducer from "./ManagedMetadataInventory.js"; +import { describe, expect, it, vi } from "vitest"; import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; @@ -10,6 +11,39 @@ import { managedPeFixtureTarget, } from "./ManagedPe.fixture.js"; +it("preserves a readable native declaration when inventory issue count exceeds call-argument capacity", () => { + const original = inventoryProducer.readManagedMetadataInventory; + const issue = { + code: "invalid-row" as const, + scope: "metadata.CustomAttribute:fixture", + offset: 0, + detail: "Controlled invalid constructor", + }; + const producer = vi + .spyOn(inventoryProducer, "readManagedMetadataInventory") + .mockImplementation((...args) => ({ + ...original(...args), + issues: Array.from({ length: 150_000 }, () => issue), + })); + try { + const bytes = buildManagedPeFixture({ + targetFramework: null, + pinvoke: { importName: "Observe" }, + }); + const result = inspectManagedNativeBoundariesBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.pinvoke_imports[0]?.import_name).toBe("Observe"); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [issue], + }); + } finally { + producer.mockRestore(); + } +}); + describe("managed artifact failure classification", () => { it("distinguishes native, malformed, and unsupported metadata", () => { const nativeBytes = buildNativePeFixture(); diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index 0f3de31744..031205ae46 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -46,6 +46,42 @@ const ldstrBody = (tokens: readonly string[]): Buffer => { return Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]); }; +it.each([ + { + options: { methodSignature: Buffer.from([0x06, 0x08]) }, + collection: "methods", + expected: "method", + }, + { + options: { fieldSignature: Buffer.from([0x00, 0x00, 0x01]) }, + collection: "fields", + expected: "field", + }, +] as const)( + "rejects a wrong-kind $expected signature at its metadata owner", + ({ options, collection, expected }) => { + const bytes = buildManagedPeFixture(options); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result[collection][0]?.signature).toMatchObject({ + parse_status: "malformed", + resolved_sha256: null, + issue: expect.stringContaining(`Expected ${expected} signature`), + }); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: expect.arrayContaining([ + expect.objectContaining({ + code: "invalid-blob", + detail: expect.stringContaining(`Expected ${expected} signature`), + }), + ]), + }); + }, +); + describe("managed compressed integer framing", () => { it.each([[0x80], [0xc0], [0xc0, 1], [0xc0, 1, 2]])( "keeps an incomplete width %j distinct from a reserved prefix", diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 8c26db2219..a536010032 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { + appendManagedParseIssues, managedFailure, ManagedReaderFailure, readManagedValue, @@ -190,7 +191,7 @@ export const readTypeDefinitions = ( issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, ): readonly TypeDefinition[] => { - issues.push(...memberLists.issues); + appendManagedParseIssues(issues, memberLists.issues); const typeTable = layout.table(2); const definitions: TypeDefinition[] = []; for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) @@ -1119,21 +1120,32 @@ export const metadataSignature = ( layout, issues, scope, + expectedKind, }: { readonly rawSha256: string; readonly typeName: SignatureTypeName; readonly layout: ManagedMetadataLayout; readonly issues: ManagedParseIssue[]; readonly scope: string; + readonly expectedKind?: "field" | "method"; }, ): ManagedSignature => { const operands: TypeOperand[] = []; - const decoded = decodeSignature( + let decoded = decodeSignature( blob, layout, operands, (issue) => issues.push(issue), scope, ); + if ( + expectedKind !== undefined && + decoded.kind !== "unknown" && + decoded.kind !== expectedKind + ) { + const detail = `Expected ${expectedKind} signature; decoded ${decoded.kind}${decoded.issue === null ? "" : `: ${decoded.issue}`}`; + issues.push({ code: "invalid-blob", scope, offset: null, detail }); + decoded = { ...decoded, parse_status: "malformed", issue: detail }; + } return projectSignature(blob, rawSha256, typeName, decoded, operands); }; diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index 14ecb1e0bb..b9ba377643 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -109,7 +109,7 @@ export const parseFields = ( layout, signatureTypeName, issues, - `Field:${token}.signature`, + { scope: `Field:${token}.signature`, kind: "field" }, ); core.set(token, { token, @@ -269,7 +269,7 @@ export const parseMemberRefs = ( layout, signatureTypeName, issues, - `MemberRef:${token}.signature`, + { scope: `MemberRef:${token}.signature` }, ); refs.push({ token, @@ -391,7 +391,7 @@ export const parseMethods = ({ layout, signatureTypeName, issues, - `MethodDef:${token}.signature`, + { scope: `MethodDef:${token}.signature`, kind: "method" }, ); core.set(token, { ...member, @@ -543,7 +543,7 @@ const budgetedSignature = ( layout: ManagedMetadataLayout, signatureTypeName: SignatureTypeName, issues: ManagedParseIssue[], - scope: string, + owner: { readonly scope: string; readonly kind?: "field" | "method" }, ): ReturnType => { const budget = managedDecodeBudget(layout); budget.admitTransient( @@ -557,7 +557,8 @@ const budgetedSignature = ( typeName: signatureTypeName, layout, issues, - scope, + scope: owner.scope, + ...(owner.kind === undefined ? {} : { expectedKind: owner.kind }), }), budget, ); diff --git a/src/dotnet/ManagedMetadataInventory.ts b/src/dotnet/ManagedMetadataInventory.ts index 3d55371cb1..5ce92e8e10 100644 --- a/src/dotnet/ManagedMetadataInventory.ts +++ b/src/dotnet/ManagedMetadataInventory.ts @@ -19,6 +19,7 @@ import { import { metadataToken } from "./ManagedMetadataHeaps.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; import { + appendManagedParseIssues, readManagedValue, uniqueManagedParseIssues, } from "./ManagedReaderFailure.js"; @@ -214,7 +215,7 @@ export const readManagedMetadataInventory = ( typeNames?: ManagedMetadataTypeNameLookup, ): ManagedMetadataInventory => { const issues: ManagedParseIssue[] = [...memberLists.issues]; - issues.push(...identity.issues); + appendManagedParseIssues(issues, identity.issues); const { module } = identity; const assembly = readManagedValue( diff --git a/src/dotnet/ManagedNativeBoundaryInspector.ts b/src/dotnet/ManagedNativeBoundaryInspector.ts index 9d9c7521e9..941c9f0112 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.ts @@ -19,7 +19,10 @@ import { type ManagedCliHeader, type ManagedPeLayout, } from "./ManagedPeReader.js"; -import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; +import { + appendManagedParseIssues, + ManagedReaderFailure, +} from "./ManagedReaderFailure.js"; import { buildNativeBoundaryInspection, cliNative, @@ -148,7 +151,7 @@ export const inspectManagedNativeBoundariesBytes = ( } try { const heapExtent = Math.max(layout.strings.size, layout.blob?.size ?? 0); - issues.push(...inventory.issues); + appendManagedParseIssues(issues, inventory.issues); const moduleRefs = parseModuleRefs(bytes, layout, heapExtent, issues); const members = new Map([ ...parseFields(bytes, layout, heapExtent, issues), diff --git a/tests/conformance/providers/managed/managedNativeVerification.test.ts b/tests/conformance/providers/managed/managedNativeVerification.test.ts index 43f1f93047..defecae410 100644 --- a/tests/conformance/providers/managed/managedNativeVerification.test.ts +++ b/tests/conformance/providers/managed/managedNativeVerification.test.ts @@ -1,3 +1,8 @@ +import { readManagedPeLayout } from "../../../../src/dotnet/ManagedPeReader.js"; +import { + metadataRowOffset, + readManagedMetadataLayout, +} from "../../../../src/dotnet/ManagedMetadataLayout.js"; import { describe, expect, it } from "vitest"; import { buildManagedPeFixture, @@ -142,6 +147,40 @@ const inputWithImportScope = (scope: string | null) => { }; }; +it("separates CLR runtime-provided methods from unresolved file-backed native bodies", () => { + const bytes = buildManagedPeFixture({ targetFramework: null }); + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("Missing fixture CLI"); + const layout = readManagedMetadataLayout( + bytes, + pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "fixture"), + pe.cli.metadata.size, + ); + bytes.writeUInt16LE(3, metadataRowOffset(layout, 6, 1) + 4); + const target = managedPeFixtureTarget(bytes); + const inspection = inspectManagedNativeBoundariesBytes(bytes, target); + expect(inspection.native_implementations[0]?.boundary_kind).toBe( + "runtime-provided", + ); + const original = exampleInput().managed_boundaries; + const managed = createEvidence(target, original.provider, { + operation: "inspect_managed_native_boundaries", + parameters: {}, + result: inspection, + }); + const result = verifyManagedNativeBoundaries({ + managed_boundaries: managed, + native_observations: [ + nativeFunctionEvidence("/fixtures/nativehelper.dll", "pe"), + ], + }); + expect(result.summary.native_body_unresolved).toBe(0); + expect(result.native_implementations).toMatchObject({ + unresolved: 0, + runtime_provided: 1, + }); +}); + it.each([ { flags: 0x200, symbol: "ExampleA", format: "pe", status: "inferred" }, { flags: 0x202, symbol: "ExampleA", format: "pe", status: "inferred" },