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 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-14 17:11:12 -07:00
committed by GitHub
parent 2b2a5d0f26
commit 1851f1fcfd
5 changed files with 68 additions and 31 deletions
+2 -3
View File
@@ -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({
+21 -19
View File
@@ -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,
@@ -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", () => {
+9 -9
View File
@@ -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<typeof readManagedMetadataInventory>;
/**
* 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],
});
}
+7
View File
@@ -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";