From 1851f1fcfd70879b9ca58a8536727fa476abbecc Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Wed, 14 Oct 2026 17:11:12 -0700 Subject: [PATCH] fix(managed): report ILONLY-clear assemblies as mixed in boundary summaries (#3516) inspect_managed_native_boundaries derived summary.mixed_mode_or_native_header only from the managed native header and the native entry-point flag. A C++/CLI assembly with ILONLY clear and no ReadyToRun signature was classified as mixed-clr-native by inspect_managed_artifact but summarized as not mixed. Give the rule one owner, managedCliIsMixedClrNative in ManagedPeReader, and use it for both the artifact runtime family and the boundary summary. The summary now reads the CLI header directly instead of the cli_native projection. Fixes #3366 Co-authored-by: Claude Opus 5.5 --- src/dotnet/ManagedArtifactInspector.ts | 5 +-- src/dotnet/ManagedNativeBoundaryHelpers.ts | 40 ++++++++++--------- .../ManagedNativeBoundaryInspector.test.ts | 29 ++++++++++++++ src/dotnet/ManagedNativeBoundaryInspector.ts | 18 ++++----- src/dotnet/ManagedPeReader.ts | 7 ++++ 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/dotnet/ManagedArtifactInspector.ts b/src/dotnet/ManagedArtifactInspector.ts index ab40afa9f8..e5e06f678c 100644 --- a/src/dotnet/ManagedArtifactInspector.ts +++ b/src/dotnet/ManagedArtifactInspector.ts @@ -10,6 +10,7 @@ import { } from "../domain/managed/managedArtifact.js"; import { managedCli32BitRequirement, + managedCliIsMixedClrNative, readManagedPeLayout, type ManagedCliHeader, type ManagedPeLayout, @@ -164,7 +165,7 @@ const runtimeFamilyFromCliHeader = ( ): ManagedArtifactInspection["classification"]["runtime_family"] => cli?.readyToRunSignature === true ? "modern-dotnet" - : cli !== null && (cli.flags & 1) === 0 + : cli !== null && managedCliIsMixedClrNative(cli) ? "mixed-clr-native" : "unknown"; @@ -190,8 +191,6 @@ const runtimeFamilyFor = ( ); let runtimeFamily: ManagedArtifactInspection["classification"]["runtime_family"] = "unknown"; - // crossgen2 clears ILONLY on ReadyToRun output, so only a clear flag - // without the ReadyToRun signature indicates mixed CLR/native code. if (runtimeFamilyFromCliHeader(cli) === "mixed-clr-native") { runtimeFamily = "mixed-clr-native"; evidence.push({ diff --git a/src/dotnet/ManagedNativeBoundaryHelpers.ts b/src/dotnet/ManagedNativeBoundaryHelpers.ts index e4b1a59600..d0d3dd768b 100644 --- a/src/dotnet/ManagedNativeBoundaryHelpers.ts +++ b/src/dotnet/ManagedNativeBoundaryHelpers.ts @@ -20,6 +20,8 @@ import { managedTableRowCounts } from "./ManagedMetadataInventory.js"; import type { ManagedMetadataInventory } from "./ManagedMetadataInventory.js"; import { managedCli32BitRequirement, + managedCliIsMixedClrNative, + type ManagedCliHeader, type ManagedPeLayout, } from "./ManagedPeReader.js"; import { readManagedValue } from "./ManagedReaderFailure.js"; @@ -378,36 +380,37 @@ const boundaryKind = ( }; export const cliNative = ( - pe: ManagedPeLayout, + cli: ManagedCliHeader | null, ): ManagedNativeBoundaryInspection["cli_native"] => { - if (pe.cli === null) return null; + if (cli === null) return null; return { - il_only: (pe.cli.flags & 0x0000_0001) !== 0, - requires_32bit: managedCli32BitRequirement(pe.cli.flags) === "required", - strong_name_signed: (pe.cli.flags & 0x0000_0008) !== 0, - native_entry_point: (pe.cli.flags & 0x0000_0010) !== 0, - ready_to_run_signature: pe.cli.readyToRunSignature, - managed_native_header_rva: pe.cli.managedNativeHeader.rva, - managed_native_header_size: pe.cli.managedNativeHeader.size, + il_only: (cli.flags & 0x0000_0001) !== 0, + requires_32bit: managedCli32BitRequirement(cli.flags) === "required", + strong_name_signed: (cli.flags & 0x0000_0008) !== 0, + native_entry_point: (cli.flags & 0x0000_0010) !== 0, + ready_to_run_signature: cli.readyToRunSignature, + managed_native_header_rva: cli.managedNativeHeader.rva, + managed_native_header_size: cli.managedNativeHeader.size, }; }; /** Summarize declaration counts with the native facets of the CLI header. */ export const nativeBoundarySummary = ( - native: ManagedNativeBoundaryInspection["cli_native"], + cli: ManagedCliHeader | null, counts: Pick< ManagedNativeBoundaryInspection["summary"], "module_ref_count" | "pinvoke_import_count" | "native_implementation_count" >, ): ManagedNativeBoundaryInspection["summary"] => ({ ...counts, - ready_to_run: native?.ready_to_run_signature ?? null, + ready_to_run: cli?.readyToRunSignature ?? null, mixed_mode_or_native_header: - native === null + cli === null ? null - : native.managed_native_header_rva !== 0 || - native.managed_native_header_size !== 0 || - native.native_entry_point, + : managedCliIsMixedClrNative(cli) || + cli.managedNativeHeader.rva !== 0 || + cli.managedNativeHeader.size !== 0 || + (cli.flags & 0x0000_0010) !== 0, }); interface BoundaryInspectionContext { @@ -419,19 +422,18 @@ interface BoundaryInspectionContext { readonly moduleRefs: readonly ModuleRef[]; readonly imports: readonly NativeImport[]; readonly implementations: readonly NativeImplementation[]; - readonly native: ManagedNativeBoundaryInspection["cli_native"]; readonly issues: readonly ManagedParseIssue[]; } export const buildNativeBoundaryInspection = ({ target, bytes, + pe, layout, inventory, moduleRefs, imports, implementations, - native, issues, }: BoundaryInspectionContext): ManagedNativeBoundaryInspection => managedNativeBoundaryInspectionSchema.parse( @@ -454,11 +456,11 @@ export const buildNativeBoundaryInspection = ({ requires_artifact_sha256: target.sha256, requires_mvid: inventory.module?.mvid ?? null, }, - cli_native: native, + cli_native: cliNative(pe.cli), module_refs: moduleRefs, pinvoke_imports: imports, native_implementations: implementations, - summary: nativeBoundarySummary(native, { + summary: nativeBoundarySummary(pe.cli, { module_ref_count: moduleRefs.length, pinvoke_import_count: imports.length, native_implementation_count: implementations.length, diff --git a/src/dotnet/ManagedNativeBoundaryInspector.test.ts b/src/dotnet/ManagedNativeBoundaryInspector.test.ts index 18ea8d9885..8be4d882c8 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.test.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.test.ts @@ -76,6 +76,35 @@ describe("managed native boundaries", () => { "P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.", ); }); + + it.each([ + { + name: "C++/CLI with a native method body", + options: { + cliFlags: 0, + methods: [ + { name: "NativeMethod", body: Buffer.from([0xc3]), implFlags: 1 }, + ], + }, + mixed: true, + }, + { + name: "ILONLY clear with unreadable metadata", + options: { cliFlags: 0, corruptMetadataSignature: true }, + mixed: true, + }, + { name: "pure IL", options: {}, mixed: false }, + { name: "ReadyToRun", options: { readyToRun: true }, mixed: true }, + ])("agrees with artifact classification for $name", ({ options, mixed }) => { + const bytes = buildManagedPeFixture(options); + const target = managedPeFixtureTarget(bytes); + const artifact = inspectManagedArtifactBytes(bytes, target); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, target); + + expect(boundaries.summary.mixed_mode_or_native_header).toBe(mixed); + if (artifact.classification.runtime_family === "mixed-clr-native") + expect(boundaries.summary.mixed_mode_or_native_header).toBe(true); + }); }); describe("managed native boundary coded indexes", () => { diff --git a/src/dotnet/ManagedNativeBoundaryInspector.ts b/src/dotnet/ManagedNativeBoundaryInspector.ts index ef0b62cf19..42e057e74c 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.ts @@ -16,6 +16,7 @@ import { import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js"; import { readManagedPeLayout, + type ManagedCliHeader, type ManagedPeLayout, } from "./ManagedPeReader.js"; import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; @@ -33,8 +34,8 @@ import { type Inventory = ReturnType; /** - * 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 + * Report a PE whose CLI metadata is absent or cannot be admitted. `cli` + * holds the CLI header when the header itself was admitted; otherwise * the header is absent (`not-managed`) or unreadable (`malformed`). */ const emptyInspection = ( @@ -42,10 +43,10 @@ const emptyInspection = ( bytes: Buffer, classification: "not-managed" | "malformed", { - native = null, + cli = null, issues = [], }: { - readonly native?: ManagedNativeBoundaryInspection["cli_native"] | null; + readonly cli?: ManagedCliHeader | null; readonly issues?: readonly ManagedParseIssue[]; } = {}, ): ManagedNativeBoundaryInspection => { @@ -72,11 +73,11 @@ const emptyInspection = ( requires_artifact_sha256: target.sha256, requires_mvid: null, }, - cli_native: native, + cli_native: cliNative(cli), module_refs: [], pinvoke_imports: [], native_implementations: [], - summary: nativeBoundarySummary(native, { + summary: nativeBoundarySummary(cli, { module_ref_count: 0, pinvoke_import_count: 0, native_implementation_count: 0, @@ -141,7 +142,7 @@ export const inspectManagedNativeBoundariesBytes = ( } catch (cause: unknown) { if (!(cause instanceof ManagedReaderFailure)) throw cause; return emptyInspection(target, bytes, "malformed", { - native: cliNative(pe), + cli: pe.cli, issues: [cause.issue], }); } @@ -179,13 +180,12 @@ export const inspectManagedNativeBoundariesBytes = ( moduleRefs, imports, implementations, - native: cliNative(pe), issues, }); } catch (cause: unknown) { if (!(cause instanceof ManagedReaderFailure)) throw cause; return emptyInspection(target, bytes, "malformed", { - native: cliNative(pe), + cli: pe.cli, issues: [...issues, cause.issue], }); } diff --git a/src/dotnet/ManagedPeReader.ts b/src/dotnet/ManagedPeReader.ts index 9a30c007be..db5ce01d9a 100644 --- a/src/dotnet/ManagedPeReader.ts +++ b/src/dotnet/ManagedPeReader.ts @@ -45,6 +45,13 @@ export const managedCli32BitRequirement = ( return pair === 0x0002_0002 ? "preferred" : "none"; }; +/** + * crossgen2 clears ILONLY on ReadyToRun output, so only a clear ILONLY flag + * without the ReadyToRun signature marks mixed CLR/native code. + */ +export const managedCliIsMixedClrNative = (cli: ManagedCliHeader): boolean => + (cli.flags & 0x0000_0001) === 0 && !cli.readyToRunSignature; + export interface ManagedPeLayout { readonly machine: number; readonly architecture: "x86" | "x86_64" | "arm" | "arm64";