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";