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:
parent
d98dcfa8c2
commit
e5ba6934e4
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue