fix(dotnet): keep admitted CLI header facts when metadata is unreadable

When the CLI header parsed but its metadata could not be admitted, the
native-boundary fallback reported hard-coded false flags and zero header
values. A corrupt-metadata ReadyToRun image therefore appeared IL-free
and non-native. The fallback now reports the parsed CLI header and
derives the summary from it as the full inspection does. When the CLI
header itself is unreadable, a limitation says the values are defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-07 07:47:44 +00:00
parent d98dcfa8c2
commit e5ba6934e4
3 changed files with 93 additions and 38 deletions

View File

@ -320,19 +320,21 @@ const boundaryKind = (
return "mixed-or-unknown";
};
/** CLI header facets of a PE without an admitted CLI header. */
export const NO_CLI_NATIVE: ManagedNativeBoundaryInspection["cli_native"] = {
il_only: false,
requires_32bit: false,
strong_name_signed: false,
native_entry_point: false,
ready_to_run_signature: false,
managed_native_header_rva: 0,
managed_native_header_size: 0,
};
export const cliNative = (
pe: ManagedPeLayout,
): ManagedNativeBoundaryInspection["cli_native"] => {
if (pe.cli === null)
return {
il_only: false,
requires_32bit: false,
strong_name_signed: false,
native_entry_point: false,
ready_to_run_signature: false,
managed_native_header_rva: 0,
managed_native_header_size: 0,
};
if (pe.cli === null) return NO_CLI_NATIVE;
return {
il_only: (pe.cli.flags & 0x0000_0001) !== 0,
requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0,
@ -344,6 +346,22 @@ export const cliNative = (
};
};
/** Summarize declaration counts with the native facets of the CLI header. */
export const nativeBoundarySummary = (
native: ManagedNativeBoundaryInspection["cli_native"],
counts: Pick<
ManagedNativeBoundaryInspection["summary"],
"module_ref_count" | "pinvoke_import_count" | "native_implementation_count"
>,
): ManagedNativeBoundaryInspection["summary"] => ({
...counts,
ready_to_run: native.ready_to_run_signature,
mixed_mode_or_native_header:
native.managed_native_header_rva !== 0 ||
native.managed_native_header_size !== 0 ||
native.native_entry_point,
});
interface BoundaryInspectionContext {
readonly target: BinaryTarget;
readonly bytes: Buffer;
@ -390,16 +408,11 @@ export const buildNativeBoundaryInspection = ({
module_refs: moduleRefs,
pinvoke_imports: imports,
native_implementations: implementations,
summary: {
summary: nativeBoundarySummary(native, {
module_ref_count: moduleRefs.length,
pinvoke_import_count: imports.length,
native_implementation_count: implementations.length,
ready_to_run: native.ready_to_run_signature,
mixed_mode_or_native_header:
native.managed_native_header_rva !== 0 ||
native.managed_native_header_size !== 0 ||
native.native_entry_point,
},
}),
coverage: {
state: issues.length === 0 ? "complete" : "partial",
issues,

View File

@ -116,6 +116,7 @@ describe("managed native boundaries", () => {
const malformedBytes = buildManagedPeFixture({
corruptMetadataSignature: true,
readyToRun: true,
});
const malformed = inspectManagedNativeBoundariesBytes(
malformedBytes,
@ -123,10 +124,37 @@ describe("managed native boundaries", () => {
);
expect(malformed).toMatchObject({
metadata: { status: "malformed" },
cli_native: {
il_only: true,
ready_to_run_signature: true,
managed_native_header_rva: 0x2700,
managed_native_header_size: 4,
},
summary: { ready_to_run: true, mixed_mode_or_native_header: true },
coverage: {
state: "unavailable",
issues: [expect.objectContaining({ code: "invalid-metadata-root" })],
},
});
expect(malformed.limitations).not.toContainEqual(
expect.stringContaining("CLI header was not admitted"),
);
});
it("marks CLI header facets as defaults when the CLI header is unreadable", () => {
const bytes = buildManagedPeFixture();
bytes.writeUInt32LE(0x7fff_0000, 0x84 + 20 + 96 + 14 * 8);
const result = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result).toMatchObject({
metadata: { status: "malformed" },
cli_native: { il_only: false },
coverage: { state: "unavailable", issues: [expect.anything()] },
});
expect(result.limitations).toContainEqual(
expect.stringContaining("CLI header was not admitted"),
);
});
});

View File

@ -20,6 +20,8 @@ import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import {
buildNativeBoundaryInspection,
cliNative,
NO_CLI_NATIVE,
nativeBoundarySummary,
nativeImplementations,
parseFields,
parseImplMaps,
@ -29,14 +31,25 @@ import {
type Inventory = ReturnType<typeof readManagedMetadataInventory>;
/** Report a PE whose CLI data is absent or cannot be admitted. */
/**
* 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
* the header is absent (`not-managed`) or unreadable (`malformed`).
*/
const emptyInspection = (
target: BinaryTarget,
bytes: Buffer,
classification: "not-managed" | "malformed",
issues: readonly ManagedParseIssue[] = [],
): ManagedNativeBoundaryInspection =>
managedNativeBoundaryInspectionSchema.parse({
{
native = null,
issues = [],
}: {
readonly native?: ManagedNativeBoundaryInspection["cli_native"] | null;
readonly issues?: readonly ManagedParseIssue[];
} = {},
): ManagedNativeBoundaryInspection => {
const header = native ?? NO_CLI_NATIVE;
return managedNativeBoundaryInspectionSchema.parse({
artifact: {
path: target.path,
sha256: target.sha256,
@ -54,31 +67,27 @@ const emptyInspection = (
requires_artifact_sha256: target.sha256,
requires_mvid: null,
},
cli_native: {
il_only: false,
requires_32bit: false,
strong_name_signed: false,
native_entry_point: false,
ready_to_run_signature: false,
managed_native_header_rva: 0,
managed_native_header_size: 0,
},
cli_native: header,
module_refs: [],
pinvoke_imports: [],
native_implementations: [],
summary: {
summary: nativeBoundarySummary(header, {
module_ref_count: 0,
pinvoke_import_count: 0,
native_implementation_count: 0,
ready_to_run: false,
mixed_mode_or_native_header: false,
},
}),
coverage: { state: "unavailable", issues },
limitations: [
"No CLI data was admitted; native boundary declarations are unavailable.",
"No CLI metadata was admitted; native boundary declarations are unavailable.",
...(native === null && classification === "malformed"
? [
"The CLI header was not admitted, so cli_native and the summary's ready_to_run and mixed_mode_or_native_header values are defaults, not observations.",
]
: []),
"Static inspection does not load or execute target code, so native export resolution is not performed.",
],
});
};
const readBoundaryInventory = (
bytes: Buffer,
@ -124,14 +133,16 @@ export const inspectManagedNativeBoundariesBytes = (
pe = readManagedPeLayout(bytes);
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return emptyInspection(target, bytes, "malformed", [cause.issue]);
return emptyInspection(target, bytes, "malformed", {
issues: [cause.issue],
});
}
if (pe.cli === null)
return emptyInspection(
target,
bytes,
pe.cliDirectoryPresent ? "malformed" : "not-managed",
pe.cliIssue === null ? [] : [pe.cliIssue],
{ issues: pe.cliIssue === null ? [] : [pe.cliIssue] },
);
let layout: ManagedMetadataLayout;
let inventory: Inventory;
@ -139,7 +150,10 @@ export const inspectManagedNativeBoundariesBytes = (
({ layout, inventory } = readBoundaryInventory(bytes, pe, pe.cli));
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return emptyInspection(target, bytes, "malformed", [cause.issue]);
return emptyInspection(target, bytes, "malformed", {
native: cliNative(pe),
issues: [cause.issue],
});
}
const heapExtent = Math.max(layout.strings.size, layout.blob.size);
const moduleRefs = parseModuleRefs(bytes, layout, heapExtent);