fix(managed): retain declaration kind and runtime implementation facts

This commit is contained in:
morluto
2026-10-24 02:34:49 +08:00
parent 22cf9646a7
commit bc2d494030
10 changed files with 150 additions and 12 deletions
+7
View File
@@ -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;
@@ -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.",
@@ -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,
}),
+35 -1
View File
@@ -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();
+36
View File
@@ -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",
+14 -2
View File
@@ -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);
};
+6 -5
View File
@@ -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<typeof metadataSignature> => {
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,
);
+2 -1
View File
@@ -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(
+5 -2
View File
@@ -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),
@@ -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" },