refactor(managed): derive inspection from PE CLI extents

This commit is contained in:
morluto
2026-09-29 17:35:41 +08:00
parent 3b64c813cf
commit dfad09e3b5
34 changed files with 192 additions and 626 deletions
@@ -203,7 +203,7 @@ artifact: canonical path + byte length + SHA-256
component: container identity + component path/name + component SHA-256, when applicable
classification: container + runtime family + implementation form + architecture
assembly/module: simple name + version + culture + public-key identity + module name + MVID
producer: provider ID + exact version + parser/profile schema + profile digest
producer: provider ID + exact version + analysis-affecting parameters + profile digest
```
An assembly-qualified name does not replace the artifact digest or MVID.
+15 -12
View File
@@ -4,9 +4,9 @@ This document turns
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md) into an
implementation and verification plan. REA currently ships read-only PE/CLI
triage and exact identity through `inspect_managed_artifact` and
`rea inspect-managed-artifact`, plus bounded metadata, signature, method-body
CIL, exception-region, call-edge, and field-access inspection through
`inspect_managed_members` and `rea inspect-managed-members`, and
`rea inspect-managed-artifact`, plus file-backed metadata, signature,
method-body CIL, exception-region, call-edge, and field-access inspection
through `inspect_managed_members` and `rea inspect-managed-members`, and
declared ModuleRef/ImplMap/PInvoke and native implementation boundary inventory
through `inspect_managed_native_boundaries` and
`rea inspect-managed-native-boundaries`, plus obfuscation-resistant member
@@ -91,7 +91,7 @@ commitment groups:
- module name, generation, and MVID;
- target framework and runtime/version strings with their exact metadata
locations;
- parser/provider version, profile schema, limits, and profile digest.
- provider identity, analysis-affecting parameters, and their profile digest.
CLI `#GUID` heap values are committed as their exact 16-byte GUID text. REA
does not impose RFC 4122 UUID version or variant bits on MVID, EncId, or
@@ -120,8 +120,8 @@ boundaries:
| `normalized_il_sha256` | UTF-8 `JSON.stringify` output for the instruction-order array of `[opcode, operand_kind, operand]` tuples | Instruction offsets except projected scalar branch targets, switch targets, resolved metadata/string identities, method header, locals, and exception regions |
The normalized field is available only when the entire CIL stream decodes with
status `present`. A partial, malformed, absent, or over-limit body reports
`null`. The tuple format is defined here.
status `present`. A malformed or absent body reports `null`. The tuple format
is defined here.
Tuple operands use these exact projections:
@@ -169,7 +169,7 @@ that implement them. The underlying capability slices are:
1. **Triage and inventory**: container/component classification, assembly and
module identity, references, files, exported types, resources, attributes,
and runtime markers.
2. **Member inventory**: bounded types, methods, fields, properties, events,
2. **Member inventory**: file-backed types, methods, fields, properties, events,
interfaces, nesting, generics, and normalized signatures.
3. **Method inspection**: headers, max stack, locals, CIL instructions,
constants, metadata operands, exception regions, and implementation flags.
@@ -190,9 +190,11 @@ that implement them. The underlying capability slices are:
Evidence-backed application graph vocabulary as JavaScript/Electron
findings while preserving managed static-analysis authority.
Every list has a deterministic order and hard limits. Search exposes scanned,
matched, returned, and dropped counts. An unresolved indirect call or a failed
signature decode is not silently omitted from a completeness claim.
Inventory lists preserve deterministic order and include every fact available
from the admitted PE/CLI streams. Malformed ranges and unsupported metadata are
reported as coverage issues instead of being silently omitted. Search exposes
scanned, matched, returned, and dropped counts. An unresolved indirect call or
a failed signature decode is not silently omitted from a completeness claim.
## Obfuscation-resistant method slices
@@ -372,7 +374,8 @@ manifest supplies those fields. It then pages each declared MethodDef token
directly by row, so selected methods do not need to appear in the first member
page of a large application. Optional `il_sha256` locks the exact raw CIL bytes
in addition to REA's decoded-instruction-tuple digest. That normalized field
has the limits documented above and is not a complete semantic CIL identity.
has the byte boundaries documented above and is not a complete semantic CIL
identity.
The optional `application_graph` block reuses those exact-build method
commitments. For each referenced MethodDef token, the verifier builds a bounded
@@ -420,7 +423,7 @@ The managed-code track advances as reviewable pull requests:
1. accepted evidence/provider boundary (this document and ADR);
2. read-only artifact triage and exact identity (shipped);
3. bounded metadata, signatures, method bodies, raw CIL hashes, and the limited
3. file-backed metadata, signatures, method bodies, raw CIL hashes, and the
decoded-instruction-tuple fingerprint (shipped; complete normalized-CIL
semantics remain planned);
4. obfuscation-resistant slices and cross-version comparison (shipped for
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_1c73a5b9ae7f6436b096a22f14eab619743ca5d79176de337354da0f51447202",
"manifest_id": "ecm_a1bee6c3a55086b881e35c2b8ce35bf0852d493d356685db7e188dbbdc61ee37",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "58478d036b495a11dcf06753cadbed1b37ddafec6d794421440886e6c117e4cb"
"sha256": "c04163b8a565e95246f22f083f4923efbc4e4499c15b67858bc5649e2d1bc98b"
}
],
"claims": [
+1 -1
View File
@@ -154,7 +154,7 @@ try {
{
id: "ghidra",
name: "Ghidra",
version: "12.1.2",
version: "12.1.4",
},
{
operation: "analyze_function",
+1 -1
View File
@@ -253,7 +253,7 @@ const verifyManagedNativeVerification = async ({
{
id: "ghidra",
name: "Ghidra",
version: "12.1.2",
version: "12.1.4",
},
{
operation: "analyze_function",
@@ -1,4 +1,4 @@
import { readFile, stat } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import { z } from "zod";
@@ -17,15 +17,11 @@ import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import { err, ok, type Result } from "../domain/result.js";
import {
type ManagedMemberInspectionLimits,
inspectManagedMembersBytes,
} from "../dotnet/ManagedMemberInspector.js";
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
import {
MANAGED_STATIC_PROVIDER,
MANAGED_WORKFLOW_PROVIDER,
} from "./InvestigationProviders.js";
import { MANAGED_INSPECTION_DEFAULTS } from "../dotnet/ManagedInspectionDefaults.js";
/** Compare managed members from input parsed by a trusted adapter. */
export const compareManagedMembersEvidenceValidated = (
@@ -52,10 +48,6 @@ export const compareManagedMemberPaths = async (input: {
}): Promise<Result<Evidence, AnalysisError>> => {
const operation = "compare_managed_members";
try {
const memberLimits: ManagedMemberPathInspectionLimits = {
...MANAGED_INSPECTION_DEFAULTS,
maxFileBytes: MANAGED_INSPECTION_DEFAULTS.maxFileBytes,
};
const [leftTarget, rightTarget] = await Promise.all([
parseBinaryTarget(input.leftPath),
parseBinaryTarget(input.rightPath),
@@ -68,21 +60,6 @@ export const compareManagedMemberPaths = async (input: {
return err(
new AnalysisInputError(operation, { cause: rightTarget.error }),
);
const [leftStat, rightStat] = await Promise.all([
stat(leftTarget.value.path),
stat(rightTarget.value.path),
]);
if (
leftStat.size > memberLimits.maxFileBytes ||
rightStat.size > memberLimits.maxFileBytes
)
return err(
new AnalysisInputError(operation, {
cause: new RangeError(
`Managed comparison input exceeds max_file_bytes ${String(memberLimits.maxFileBytes)}`,
),
}),
);
const [leftBytes, rightBytes] = await Promise.all([
readFile(leftTarget.value.path),
readFile(rightTarget.value.path),
@@ -90,19 +67,17 @@ export const compareManagedMemberPaths = async (input: {
const leftInspection = inspectManagedMembersBytes(
leftBytes,
leftTarget.value,
memberLimits,
);
const rightInspection = inspectManagedMembersBytes(
rightBytes,
rightTarget.value,
memberLimits,
);
const leftEvidence = createEvidence(
leftTarget.value,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: memberLimitParameters(memberLimits),
parameters: {},
result: jsonValueSchema.parse(leftInspection),
rawResult: null,
limitations: leftInspection.limitations,
@@ -114,7 +89,7 @@ export const compareManagedMemberPaths = async (input: {
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: memberLimitParameters(memberLimits),
parameters: {},
result: jsonValueSchema.parse(rightInspection),
rawResult: null,
limitations: rightInspection.limitations,
@@ -145,11 +120,7 @@ export const compareManagedMemberPaths = async (input: {
}
};
/** Byte-inspection limits plus the file admission bound for path comparisons. */
export interface ManagedMemberPathInspectionLimits
extends ManagedMemberInspectionLimits {
readonly maxFileBytes: number;
}
/** ECMA-335 representable limits for path-based member comparison. */
const createManagedMemberComparisonEvidence = (
parameters: Pick<CompareManagedMembersInput, "left" | "right">,
@@ -171,17 +142,6 @@ const createManagedMemberComparisonEvidence = (
evidenceLinks: result.evidence_links,
});
const memberLimitParameters = (
limits: ManagedMemberPathInspectionLimits,
): Record<string, ReturnType<typeof jsonValueSchema.parse>> => ({
max_file_bytes: limits.maxFileBytes,
max_metadata_bytes: limits.maxMetadataBytes,
max_table_rows: limits.maxTableRows,
max_heap_item_bytes: limits.maxHeapItemBytes,
max_method_body_bytes: limits.maxMethodBodyBytes,
max_method_instructions: limits.maxMethodInstructions,
});
const workflowFailure = (
operation: string,
cause: unknown,
+1 -2
View File
@@ -151,7 +151,7 @@ const managedInstructionAnchorSchema = z.object({
});
const managedMethodBodySchema = z.object({
status: z.enum(["present", "partial", "absent", "malformed", "too-large"]),
status: z.enum(["present", "partial", "absent", "malformed"]),
header_format: z.enum(["tiny", "fat", "none", "unknown"]),
rva: z.number().int().min(0).max(0xffff_ffff),
file_offset: offsetSchema.nullable(),
@@ -294,7 +294,6 @@ const managedParseIssueSchema = z.object({
"invalid-blob",
"invalid-guid",
"invalid-resource",
"limit-exceeded",
]),
scope: z.string().min(1),
offset: offsetSchema.nullable(),
+2 -36
View File
@@ -10,7 +10,6 @@ import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js"
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
} from "../dotnet/ManagedPe.fixture.js";
describe("managed member comparison", () => {
@@ -115,35 +114,6 @@ describe("managed member comparison", () => {
}).success,
).toBe(false);
});
it("does not match identical instruction-limited method prefixes", () => {
const limited = {
...MANAGED_MEMBER_FIXTURE_LIMITS,
maxMethodInstructions: 1,
};
const left = inspect(
buildManagedPeFixture(),
"/tmp/left-partial.dll",
limited,
);
const right = inspect(
buildManagedPeFixture(),
"/tmp/right-partial.dll",
limited,
);
const result = compareManagedMembers(
{ evidenceId: left.evidenceId, result: left.result },
{ evidenceId: right.evidenceId, result: right.result },
);
expect(result.matching.exact_il_signature).toBe(0);
expect(result.matching.structural_method_shape).toBe(0);
expect(result.coverage).toMatchObject({
status: "partial",
left_status: "partial",
right_status: "partial",
});
});
});
describe("managed member comparison uncertainty", () => {
@@ -244,13 +214,9 @@ describe("managed member comparison uncertainty", () => {
});
});
const inspect = (
bytes: Buffer,
path: string,
limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS,
) => {
const inspect = (bytes: Buffer, path: string) => {
const target = managedPeFixtureTarget(bytes, path);
const result = inspectManagedMembersBytes(bytes, target, limits);
const result = inspectManagedMembersBytes(bytes, target);
return {
result,
evidenceId: `ev_${hash(Buffer.from(path))}`,
+1 -16
View File
@@ -5,16 +5,14 @@ import {
buildManagedPeFixture,
buildNativePeFixture,
managedPeFixtureTarget,
MANAGED_ARTIFACT_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed artifact failure classification", () => {
it("distinguishes native, malformed, unsupported, and bounded metadata", () => {
it("distinguishes native, malformed, and unsupported metadata", () => {
const nativeBytes = buildNativePeFixture();
const native = inspectManagedArtifactBytes(
nativeBytes,
managedPeFixtureTarget(nativeBytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(native).toMatchObject({
classification: { status: "not-managed" },
@@ -28,7 +26,6 @@ describe("managed artifact failure classification", () => {
const malformed = inspectManagedArtifactBytes(
malformedBytes,
managedPeFixtureTarget(malformedBytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(malformed.classification.status).toBe("malformed");
expect(malformed.coverage.issues).toEqual([
@@ -41,21 +38,9 @@ describe("managed artifact failure classification", () => {
const unsupportedTable = inspectManagedArtifactBytes(
unsupportedTableBytes,
managedPeFixtureTarget(unsupportedTableBytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(unsupportedTable.coverage.issues).toEqual([
expect.objectContaining({ code: "invalid-tables" }),
]);
const limitedBytes = buildManagedPeFixture();
const limited = inspectManagedArtifactBytes(
limitedBytes,
managedPeFixtureTarget(limitedBytes),
{ ...MANAGED_ARTIFACT_FIXTURE_LIMITS, maxMetadataBytes: 256 },
);
expect(limited.metadata.status).toBe("partial");
expect(limited.coverage.issues).toEqual([
expect.objectContaining({ code: "limit-exceeded" }),
]);
});
});
@@ -8,9 +8,6 @@ import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInsp
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_ARTIFACT_FIXTURE_LIMITS,
MANAGED_MEMBER_FIXTURE_LIMITS,
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed artifact inventory", () => {
@@ -30,7 +27,6 @@ describe("managed artifact inventory", () => {
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(result.classification.status).toBe("managed");
@@ -44,7 +40,6 @@ describe("managed artifact inventory", () => {
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(result.classification).toMatchObject({
@@ -90,7 +85,6 @@ describe("managed artifact inventory", () => {
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
expect(result.classification.runtime_family).toBe("unity-mono");
@@ -106,17 +100,14 @@ describe("managed artifact inventory", () => {
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
const members = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
const boundaries = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
);
expect(result.module?.mvid).toBe("0ec6eb3a-4adc-4b54-1f45-8b4ed40b33b1");
@@ -135,7 +126,6 @@ describe("managed artifact inventory", () => {
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(result.fields[0]?.signature).toMatchObject({
+4 -27
View File
@@ -12,7 +12,6 @@ import {
import {
managedTableRowCounts,
readManagedMetadataInventory,
type ManagedInventoryInput,
type ManagedMetadataInventory,
type ManagedResourceDirectory,
} from "./ManagedMetadataInventory.js";
@@ -22,11 +21,6 @@ import {
} from "./ManagedMetadataLayout.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
export interface ManagedInspectionLimits extends ManagedInventoryInput {
readonly maxMetadataBytes: number;
readonly maxTableRows: number;
}
const flagNames = (flags: number): string[] => {
const names: string[] = [];
for (const [mask, name] of [
@@ -309,7 +303,7 @@ const partialMetadataResult = ({
],
},
metadata: {
status: issue.code === "limit-exceeded" ? "partial" : "malformed",
status: "malformed",
version: null,
stream_names: [],
table_row_counts: {},
@@ -322,7 +316,7 @@ const partialMetadataResult = ({
attributes: [],
coverage: { state: "partial", issues: [issue] },
limitations: [
"CLI metadata triage stopped at the reported bounded format or resource limit.",
"CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.",
],
});
@@ -350,29 +344,14 @@ const mapResources = (
}
};
/** Inspect PE/CLI identity directly from bounded bytes without CLR loading. */
/** Inspect PE/CLI identity directly from local bytes without CLR loading. */
export const inspectManagedArtifactBytes = (
bytes: Buffer,
target: BinaryTarget,
limits: ManagedInspectionLimits,
): ManagedArtifactInspection => {
const layout = readManagedPeLayout(bytes);
if (layout.cli === null) return unavailableResult(target, bytes, layout);
const cli = layout.cli;
if (cli.metadata.size > limits.maxMetadataBytes)
return managedArtifactInspectionSchema.parse(
partialMetadataResult({
target,
bytes,
layout,
issue: {
code: "limit-exceeded",
scope: "cli.metadata",
offset: cli.headerOffset + 8,
detail: `CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
},
}),
);
let metadata: ManagedMetadataLayout;
try {
const metadataOffset = layout.rvaToOffset(
@@ -384,7 +363,6 @@ export const inspectManagedArtifactBytes = (
bytes,
metadataOffset,
cli.metadata.size,
limits.maxTableRows,
);
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
@@ -402,7 +380,6 @@ export const inspectManagedArtifactBytes = (
const inventory = readManagedMetadataInventory(
bytes,
metadata,
limits,
resourceDirectory,
);
const issues = [...resourceIssues, ...inventory.issues];
@@ -410,7 +387,7 @@ export const inspectManagedArtifactBytes = (
...(issues.length === 0
? []
: [
"At least one bounded metadata or resource item was unavailable; coverage is partial.",
"At least one metadata or resource item could not be read; coverage is partial.",
]),
"Static inspection did not load the assembly, resolve dependencies through a CLR, decompile C#, or execute target code.",
];
-5
View File
@@ -6,7 +6,6 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed CIL decoding", () => {
@@ -26,7 +25,6 @@ describe("managed CIL decoding", () => {
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(result.methods[0]?.body).toMatchObject({
@@ -53,7 +51,6 @@ describe("managed CIL decoding", () => {
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(result.methods[0]?.body).toMatchObject({
@@ -81,12 +78,10 @@ describe("managed CIL decoding", () => {
const tiny = inspectManagedMembersBytes(
tinyBytes,
managedPeFixtureTarget(tinyBytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
const fat = inspectManagedMembersBytes(
fatBytes,
managedPeFixtureTarget(fatBytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(tiny.methods[0]?.body).toMatchObject({
-53
View File
@@ -1,53 +0,0 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed CIL limits", () => {
it("marks instruction-limited CIL as partial without assigning normalized identity", () => {
const bytes = buildManagedPeFixture();
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
{
...MANAGED_MEMBER_FIXTURE_LIMITS,
maxMethodInstructions: 1,
},
);
const il = bytes.subarray(0x0a01, 0x0a0d);
expect(result.metadata.status).toBe("complete");
expect(result.methods[0]?.body).toMatchObject({
status: "partial",
il_size: 12,
il_sha256: createHash("sha256").update(il).digest("hex"),
normalized_il_sha256: null,
instruction_count: 1,
decoded_instruction_count: 1,
truncated_instructions: 1,
issue:
"Instruction decode reached max_method_instructions 1 before the end of the method body",
});
expect(result.coverage).toEqual({
state: "partial",
issues: [
{
code: "limit-exceeded",
scope: "method.0x06000001.body.instructions",
offset: 0x0a00,
detail:
"Instruction decode reached max_method_instructions 1 before the end of the method body",
},
],
});
expect(result.limitations.join(" ")).toContain(
"has no normalized CIL identity",
);
});
});
-9
View File
@@ -1,9 +0,0 @@
/** Internal parser and artifact-admission safeguards for managed PE inspection. */
export const MANAGED_INSPECTION_DEFAULTS = Object.freeze({
maxFileBytes: 268_435_456,
maxMetadataBytes: 67_108_864,
maxTableRows: 100_000,
maxHeapItemBytes: 1_048_576,
maxMethodBodyBytes: 1_048_576,
maxMethodInstructions: 10_000,
});
@@ -4,7 +4,6 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed member inspection", () => {
@@ -13,7 +12,6 @@ describe("managed member inspection", () => {
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(result.identity_scope).toEqual({
+11 -64
View File
@@ -14,15 +14,8 @@ import {
readManagedMetadataInventory,
} from "./ManagedMetadataInventory.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
ManagedReaderFailure,
managedFailure,
} from "./ManagedReaderFailure.js";
import {
parseTypes,
typeRanges,
type ManagedMemberInspectionLimits,
} from "./ManagedMemberInspectorCore.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import { parseTypes, typeRanges } from "./ManagedMemberInspectorCore.js";
import {
edges,
parseFields,
@@ -30,12 +23,9 @@ import {
parseMethods,
} from "./ManagedMemberRows.js";
export type { ManagedMemberInspectionLimits } from "./ManagedMemberInspectorCore.js";
const unavailable = (
target: BinaryTarget,
bytes: Buffer,
limits: ManagedMemberInspectionLimits,
issue: ManagedParseIssue | null,
): ManagedMemberInspection =>
managedMemberInspectionSchema.parse({
@@ -92,30 +82,7 @@ const resourceDirectory = (
};
};
const collectMethodBodyIssues = (
methods: ReturnType<typeof parseMethods>,
limits: ManagedMemberInspectionLimits,
): ManagedParseIssue[] =>
methods.methods.flatMap((method) =>
method.body.status === "partial"
? [
{
code: "limit-exceeded" as const,
scope: `method.${method.token}.body.instructions`,
offset: method.body.file_offset,
detail:
method.body.issue ??
`Instruction decode reached max_method_instructions ${String(limits.maxMethodInstructions)}`,
},
]
: [],
);
const readMemberInventory = (
bytes: Buffer,
pe: ManagedPeLayout,
limits: ManagedMemberInspectionLimits,
) => {
const readMemberInventory = (bytes: Buffer, pe: ManagedPeLayout) => {
const cli = pe.cli;
if (cli === null)
throw new TypeError("Managed inventory requires CLI metadata");
@@ -124,25 +91,14 @@ const readMemberInventory = (
cli.metadata.size,
"cli.metadata",
);
if (cli.metadata.size > limits.maxMetadataBytes)
throw managedFailure(
"limit-exceeded",
"metadata.root",
`CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
rootOffset,
);
const layout = readManagedMetadataLayout(
bytes,
rootOffset,
cli.metadata.size,
limits.maxTableRows,
);
const inventory = readManagedMetadataInventory(
bytes,
layout,
{
maxHeapItemBytes: limits.maxHeapItemBytes,
},
resourceDirectory(pe),
);
return { layout, inventory };
@@ -152,28 +108,24 @@ const readMemberInventory = (
export const inspectManagedMembersBytes = (
bytes: Buffer,
target: BinaryTarget,
limits: ManagedMemberInspectionLimits,
): ManagedMemberInspection => {
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) return unavailable(target, bytes, limits, pe.cliIssue);
if (pe.cli === null) return unavailable(target, bytes, pe.cliIssue);
const issues: ManagedParseIssue[] = [];
try {
const { layout, inventory } = readMemberInventory(bytes, pe, limits);
const { layout, inventory } = readMemberInventory(bytes, pe);
issues.push(...inventory.issues);
const ranges = typeRanges(bytes, layout, limits.maxHeapItemBytes);
const types = parseTypes(bytes, layout, ranges, limits.maxHeapItemBytes);
const fields = parseFields(bytes, layout, ranges, limits.maxHeapItemBytes);
const memberRefs = parseMemberRefs(bytes, layout, limits.maxHeapItemBytes);
const ranges = typeRanges(bytes, layout);
const types = parseTypes(bytes, layout, ranges);
const fields = parseFields(bytes, layout, ranges);
const memberRefs = parseMemberRefs(bytes, layout);
const methods = parseMethods({
bytes,
layout,
pe,
ranges,
maxBytes: limits.maxHeapItemBytes,
limits,
});
const methodBodyIssues = collectMethodBodyIssues(methods, limits);
const coverageIssues = [...issues, ...methodBodyIssues];
const coverageIssues = issues;
const related = edges(
methods.methods,
methods.core,
@@ -212,16 +164,11 @@ export const inspectManagedMembersBytes = (
"Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
"Signatures are decoded for common ECMA-335 primitive, class, valuetype, pointer, byref, array, and generic variable forms; unsupported forms retain raw signature hashes.",
...(methodBodyIssues.length > 0
? [
"At least one CIL method body reached max_method_instructions; its decoded prefix is partial and has no normalized CIL identity.",
]
: []),
],
});
} catch (cause: unknown) {
if (cause instanceof ManagedReaderFailure)
return unavailable(target, bytes, limits, cause.issue);
return unavailable(target, bytes, cause.issue);
throw cause;
}
};
+5 -16
View File
@@ -24,14 +24,6 @@ export type ManagedInstructionAnchor = ManagedMethodBody["anchors"][number];
export type ManagedExceptionRegion =
ManagedMethodBody["exception_regions"][number];
export interface ManagedMemberInspectionLimits {
readonly maxMetadataBytes: number;
readonly maxTableRows: number;
readonly maxHeapItemBytes: number;
readonly maxMethodBodyBytes: number;
readonly maxMethodInstructions: number;
}
export interface TypeRange {
readonly token: string;
readonly fullName: string;
@@ -87,7 +79,6 @@ const readTypeRange = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
): TypeRange => {
const methods = layout.table(6);
const fields = layout.table(4);
@@ -97,13 +88,13 @@ const readTypeRange = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldStart = cursor.readIndex(layout.tableIndexSize(4));
@@ -133,12 +124,11 @@ const readTypeRange = (
export const typeRanges = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): readonly TypeRange[] => {
const typeTable = layout.table(2);
const ranges: TypeRange[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
ranges.push(readTypeRange(bytes, layout, row, maxBytes));
ranges.push(readTypeRange(bytes, layout, row));
return ranges;
};
@@ -160,7 +150,6 @@ export const parseTypes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
maxBytes: number,
): readonly ManagedType[] => {
const typeTable = layout.table(2);
const fields = layout.table(4);
@@ -173,13 +162,13 @@ export const parseTypes = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldStart = cursor.readIndex(layout.tableIndexSize(4));
@@ -377,7 +377,6 @@ const readOperand = (
export const decodeInstructions = (
il: Buffer,
limit: number,
): {
readonly parsed: readonly ParsedInstruction[];
readonly count: number;
@@ -388,7 +387,7 @@ export const decodeInstructions = (
let offset = 0;
let issue: string | null = null;
try {
while (offset < il.length && parsed.length < limit) {
while (offset < il.length) {
const start = offset;
const first = il.readUInt8(offset);
offset += 1;
+7 -14
View File
@@ -7,7 +7,6 @@ import {
type ManagedCallEdge,
type ManagedField,
type ManagedFieldAccess,
type ManagedMemberInspectionLimits,
type ManagedMemberRef,
type ManagedMethod,
type MemberRefCore,
@@ -27,7 +26,6 @@ export const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
maxBytes: number,
): {
readonly fields: readonly ManagedField[];
readonly core: ReadonlyMap<string, FieldCore>;
@@ -42,13 +40,13 @@ export const parseFields = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const sig = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
layout.blob.size,
);
const declared = declaringType(ranges, "field", row);
const token = metadataToken(4, row);
@@ -69,7 +67,6 @@ export const parseFields = (
export const parseMemberRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): {
readonly refs: readonly ManagedMemberRef[];
readonly core: ReadonlyMap<string, MemberRefCore>;
@@ -86,13 +83,13 @@ export const parseMemberRefs = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const sig = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
layout.blob.size,
);
const token = metadataToken(10, row);
refs.push({
@@ -112,8 +109,6 @@ interface ParseMethodsInput {
readonly layout: ManagedMetadataLayout;
readonly pe: ManagedPeLayout;
readonly ranges: readonly TypeRange[];
readonly maxBytes: number;
readonly limits: ManagedMemberInspectionLimits;
}
export const parseMethods = ({
@@ -121,8 +116,6 @@ export const parseMethods = ({
layout,
pe,
ranges,
maxBytes,
limits,
}: ParseMethodsInput): {
readonly methods: readonly ManagedMethod[];
readonly core: ReadonlyMap<string, MethodCore>;
@@ -139,13 +132,13 @@ export const parseMethods = ({
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
layout.strings.size,
);
const sig = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
layout.blob.size,
);
cursor.readIndex(layout.tableIndexSize(8));
const declared = declaringType(ranges, "method", row);
@@ -160,7 +153,7 @@ export const parseMethods = ({
impl_flags: implFlags,
flags,
signature: signature(sig),
body: methodBody(bytes, pe, rva, limits),
body: methodBody(bytes, pe, rva),
});
core.set(token, { token, name, declaringType: declared?.fullName ?? null });
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { readMetadataBlob } from "./ManagedMetadataHeaps.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
describe("managed metadata heaps", () => {
it("reads a blob item's compressed length from its ECMA-335 prefix", () => {
const itemLength = 1_048_577;
const bytes = Buffer.alloc(itemLength + 5);
bytes.set([0xc0, 0x10, 0x00, 0x01], 1);
const layout: ManagedMetadataLayout = {
rootOffset: 0,
size: bytes.length,
version: "",
streamNames: ["#Blob"],
strings: { name: "#Strings", offset: 0, size: 0 },
guid: { name: "#GUID", offset: 0, size: 0 },
blob: { name: "#Blob", offset: 0, size: bytes.length },
tables: new Map(),
rowCounts: [],
stringIndexSize: 2,
guidIndexSize: 2,
blobIndexSize: 2,
table: () => undefined,
tableIndexSize: () => 2,
codedIndexSize: () => 2,
};
expect(readMetadataBlob(bytes, layout, 1, layout.blob.size)).toHaveLength(
itemLength,
);
});
});
+10 -10
View File
@@ -101,12 +101,12 @@ const compressedLength = (
);
};
/** Read one bounded UTF-8 value from #Strings. */
/** Read one UTF-8 value from #Strings within its PE/CLI stream extent. */
export const readMetadataString = (
bytes: Buffer,
layout: ManagedMetadataLayout,
index: number,
maxBytes: number,
heapExtent: number,
): string => {
if (index === 0) return "";
if (index < 0 || index >= layout.strings.size)
@@ -119,15 +119,15 @@ export const readMetadataString = (
const start = layout.strings.offset + index;
const maximum = Math.min(
layout.strings.offset + layout.strings.size,
start + maxBytes + 1,
start + heapExtent + 1,
);
let end = start;
while (end < maximum && bytes[end] !== 0) end += 1;
if (end === maximum)
throw managedFailure(
"limit-exceeded",
"invalid-string",
"metadata.#Strings",
`String heap item exceeds max_heap_item_bytes ${String(maxBytes)}`,
"String heap item has no terminator within #Strings",
start,
);
try {
@@ -144,12 +144,12 @@ export const readMetadataString = (
}
};
/** Read one bounded item from #Blob without interpreting its contents. */
/** Read one #Blob item while validating its encoded extent against the heap. */
export const readMetadataBlob = (
bytes: Buffer,
layout: ManagedMetadataLayout,
index: number,
maxBytes: number,
heapExtent: number,
): Buffer => {
if (index === 0) return Buffer.alloc(0);
if (index < 0 || index >= layout.blob.size)
@@ -161,11 +161,11 @@ export const readMetadataBlob = (
);
const start = layout.blob.offset + index;
const decoded = compressedLength(bytes, start, "metadata.#Blob");
if (decoded.length > maxBytes)
if (decoded.length > heapExtent)
throw managedFailure(
"limit-exceeded",
"invalid-blob",
"metadata.#Blob",
`Blob heap item exceeds max_heap_item_bytes ${String(maxBytes)}`,
"Blob length exceeds the admitted heap extent",
start,
);
const content = start + decoded.prefix;
+15 -37
View File
@@ -22,10 +22,6 @@ type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
type AssemblyReference = ManagedArtifactInspection["references"][number];
type ManagedResource = ManagedArtifactInspection["resources"][number];
type CustomAttribute = ManagedArtifactInspection["attributes"][number];
export interface ManagedInventoryInput {
readonly maxHeapItemBytes: number;
}
export interface ManagedResourceDirectory {
readonly offset: number;
readonly size: number;
@@ -74,15 +70,17 @@ const uniqueIssues = (
...new Map(issues.map((issue) => [JSON.stringify(issue), issue])).values(),
];
const heapExtent = (layout: ManagedMetadataLayout): number =>
Math.max(layout.strings.size, layout.blob.size);
const readReferences = (
bytes: Buffer,
layout: ManagedMetadataLayout,
input: ManagedInventoryInput,
issues: ManagedParseIssue[],
): readonly AssemblyReference[] =>
readRows(layout.table(35), (row) =>
safeRead(
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
() => readAssemblyReference(bytes, layout, row, heapExtent(layout)),
issues,
),
);
@@ -90,13 +88,11 @@ const readReferences = (
const readResources = ({
bytes,
layout,
input,
resourceDirectory,
issues,
}: {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly input: ManagedInventoryInput;
readonly resourceDirectory: ManagedResourceDirectory | null;
readonly issues: ManagedParseIssue[];
}): readonly ManagedResource[] =>
@@ -107,7 +103,6 @@ const readResources = ({
bytes,
layout,
row,
maxBytes: input.maxHeapItemBytes,
directory: resourceDirectory,
issues,
}),
@@ -118,12 +113,11 @@ const readResources = ({
const readAttributes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
input: ManagedInventoryInput,
issues: ManagedParseIssue[],
): readonly CustomAttribute[] =>
readRows(layout.table(12), (row) =>
safeRead(
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
() => readCustomAttribute(bytes, layout, row, heapExtent(layout)),
issues,
),
);
@@ -131,14 +125,13 @@ const readAttributes = (
const collectReferenceNames = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
issues: ManagedParseIssue[],
): readonly string[] => {
const names: string[] = [];
const referenceTable = layout.table(35);
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
const reference = safeRead(
() => readAssemblyReference(bytes, layout, row, maxBytes),
() => readAssemblyReference(bytes, layout, row, heapExtent(layout)),
issues,
);
if (reference !== undefined) names.push(reference.name);
@@ -149,14 +142,13 @@ const collectReferenceNames = (
const collectTargetFrameworks = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
issues: ManagedParseIssue[],
): readonly string[] => {
const targetFrameworks: string[] = [];
const attributeTable = layout.table(12);
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
const attribute = safeRead(
() => readCustomAttribute(bytes, layout, row, maxBytes),
() => readCustomAttribute(bytes, layout, row, heapExtent(layout)),
issues,
);
if (
@@ -171,43 +163,29 @@ const collectTargetFrameworks = (
return targetFrameworks;
};
/** Inventory bounded identity tables without CLR reflection or execution. */
/** Inventory identity tables without CLR reflection or execution. */
export const readManagedMetadataInventory = (
bytes: Buffer,
layout: ManagedMetadataLayout,
input: ManagedInventoryInput,
resourceDirectory: ManagedResourceDirectory | null,
): ManagedMetadataInventory => {
const issues: ManagedParseIssue[] = [];
const module =
safeRead(() => readModule(bytes, layout, input.maxHeapItemBytes), issues) ??
safeRead(() => readModule(bytes, layout, heapExtent(layout)), issues) ??
null;
const assembly =
safeRead(
() => readAssembly(bytes, layout, input.maxHeapItemBytes),
issues,
) ?? null;
const references = readReferences(bytes, layout, input, issues);
safeRead(() => readAssembly(bytes, layout, heapExtent(layout)), issues) ??
null;
const references = readReferences(bytes, layout, issues);
const resources = readResources({
bytes,
layout,
input,
resourceDirectory,
issues,
});
const attributes = readAttributes(bytes, layout, input, issues);
const referenceNames = collectReferenceNames(
bytes,
layout,
input.maxHeapItemBytes,
issues,
);
const targetFrameworks = collectTargetFrameworks(
bytes,
layout,
input.maxHeapItemBytes,
issues,
);
const attributes = readAttributes(bytes, layout, issues);
const referenceNames = collectReferenceNames(bytes, layout, issues);
const targetFrameworks = collectTargetFrameworks(bytes, layout, issues);
return {
module,
assembly,
+39 -36
View File
@@ -45,7 +45,7 @@ const publicKeyIdentity = (
export const readModule = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
heapExtent: number,
): ModuleIdentity | null => {
const table = layout.table(0);
if (table === undefined || table.rowCount === 0) return null;
@@ -55,7 +55,7 @@ export const readModule = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const mvid = readMetadataGuid(
bytes,
@@ -86,7 +86,7 @@ export const readModule = (
export const readAssembly = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
heapExtent: number,
): AssemblyIdentity | null => {
const table = layout.table(32);
if (table === undefined || table.rowCount === 0) return null;
@@ -103,19 +103,19 @@ export const readAssembly = (
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
heapExtent,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
return {
name,
@@ -136,7 +136,7 @@ export const readAssemblyReference = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
heapExtent: number,
): AssemblyReference => {
const cursor = metadataRowCursor(bytes, layout, 35, row);
const version = [
@@ -150,25 +150,25 @@ export const readAssemblyReference = (
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
heapExtent,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const hashValue = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
heapExtent,
);
return {
name,
@@ -191,7 +191,7 @@ interface ReadTypeNameContext {
readonly layout: ManagedMetadataLayout;
readonly table: 1 | 2;
readonly row: number;
readonly maxBytes: number;
readonly heapExtent: number;
}
const readTypeName = ({
@@ -199,7 +199,7 @@ const readTypeName = ({
layout,
table,
row,
maxBytes,
heapExtent,
}: ReadTypeNameContext): string | null => {
const descriptor = layout.table(table);
if (descriptor === undefined || row < 1 || row > descriptor.rowCount)
@@ -211,13 +211,13 @@ const readTypeName = ({
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
};
@@ -226,7 +226,7 @@ const declaringTypeForMethod = (
bytes: Buffer,
layout: ManagedMetadataLayout,
methodRow: number,
maxBytes: number,
heapExtent: number,
): string | null => {
const types = layout.table(2);
const methods = layout.table(6);
@@ -244,12 +244,12 @@ const declaringTypeForMethod = (
? methods.rowCount + 1
: methodListForType(bytes, layout, row + 1);
if (methodRow < methodStart || methodRow >= methodEnd) continue;
const name = readMetadataString(bytes, layout, nameIndex, maxBytes);
const name = readMetadataString(bytes, layout, nameIndex, heapExtent);
const namespace = readMetadataString(
bytes,
layout,
namespaceIndex,
maxBytes,
heapExtent,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
}
@@ -274,12 +274,12 @@ const attributeTypeName = (
bytes: Buffer,
layout: ManagedMetadataLayout,
rawType: number,
maxBytes: number,
heapExtent: number,
): string | null => {
const tag = rawType & 7;
const row = Math.floor(rawType / 8);
if (row === 0) return null;
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, maxBytes);
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, heapExtent);
if (tag !== 3) return null;
const memberRefs = layout.table(10);
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
@@ -288,11 +288,23 @@ const attributeTypeName = (
const parentTag = parent & 7;
const parentRow = Math.floor(parent / 8);
if (parentTag === 0)
return readTypeName({ bytes, layout, table: 2, row: parentRow, maxBytes });
return readTypeName({
bytes,
layout,
table: 2,
row: parentRow,
heapExtent,
});
if (parentTag === 1)
return readTypeName({ bytes, layout, table: 1, row: parentRow, maxBytes });
return readTypeName({
bytes,
layout,
table: 1,
row: parentRow,
heapExtent,
});
if (parentTag === 3)
return declaringTypeForMethod(bytes, layout, parentRow, maxBytes);
return declaringTypeForMethod(bytes, layout, parentRow, heapExtent);
return null;
};
@@ -345,7 +357,7 @@ export const readCustomAttribute = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
heapExtent: number,
): CustomAttribute => {
const cursor = metadataRowCursor(bytes, layout, 12, row);
const parentRaw = cursor.readIndex(
@@ -358,7 +370,7 @@ export const readCustomAttribute = (
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
heapExtent,
);
const parent = metadataCodedToken(
parentRaw,
@@ -375,7 +387,7 @@ export const readCustomAttribute = (
"CustomAttribute parent coded index is invalid",
cursor.start,
);
const typeName = attributeTypeName(bytes, layout, typeRaw, maxBytes);
const typeName = attributeTypeName(bytes, layout, typeRaw, heapExtent);
return {
parent_token: parent,
constructor_token: metadataCodedToken(typeRaw, 3, [
@@ -400,7 +412,6 @@ interface ReadResourceContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly row: number;
readonly maxBytes: number;
readonly directory: ManagedResourceDirectory | null;
readonly issues: ManagedParseIssue[];
}
@@ -409,7 +420,6 @@ export const readResource = ({
bytes,
layout,
row,
maxBytes,
directory,
issues,
}: ReadResourceContext): ManagedResource => {
@@ -420,7 +430,7 @@ export const readResource = ({
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
Math.max(layout.strings.size, layout.blob.size),
);
const implementationRaw = cursor.readIndex(
layout.codedIndexSize("Implementation"),
@@ -450,13 +460,6 @@ export const readResource = ({
offset: start,
detail: "Embedded resource length leaves the CLI resources directory",
});
} else if (dataLength > maxBytes) {
issues.push({
code: "limit-exceeded",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: start + 4,
detail: `Embedded resource exceeds max_heap_item_bytes ${String(maxBytes)}; digest omitted`,
});
} else {
dataSha256 = sha256Bytes(
bytes.subarray(start + 4, start + 4 + dataLength),
+1 -16
View File
@@ -307,8 +307,6 @@ const readTablesHeader = (
const readRowCounts = (
tableBytes: Buffer,
valid: bigint,
maxTableRows: number,
tablesOffset: number,
): readonly number[] => {
const rowCounts = Array<number>(METADATA_TABLE_NAMES.length).fill(0);
let tableCursor = 24;
@@ -316,13 +314,6 @@ const readRowCounts = (
if ((valid & (1n << BigInt(index))) === 0n) continue;
requireRange(tableBytes, tableCursor, 4, "metadata.table-row-count");
const count = tableBytes.readUInt32LE(tableCursor);
if (count > maxTableRows)
throw managedFailure(
"limit-exceeded",
`metadata.${METADATA_TABLE_NAMES[index] ?? String(index)}`,
`Metadata table row count ${String(count)} exceeds max_table_rows ${String(maxTableRows)}`,
tablesOffset + tableCursor,
);
rowCounts[index] = count;
tableCursor += 4;
}
@@ -390,7 +381,6 @@ export const readManagedMetadataLayout = (
artifact: Buffer,
rootOffset: number,
size: number,
maxTableRows: number,
): ManagedMetadataLayout => {
if (size < 20 || rootOffset < 0 || rootOffset > artifact.length - size)
throw managedFailure(
@@ -405,12 +395,7 @@ export const readManagedMetadataLayout = (
artifact,
streams,
);
const rowCounts = readRowCounts(
tableBytes,
valid,
maxTableRows,
tablesOffset,
);
const rowCounts = readRowCounts(tableBytes, valid);
const tables = buildMetadataTables({
tableBytes,
rowCounts,
+3 -37
View File
@@ -1,8 +1,5 @@
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type {
ManagedMemberInspectionLimits,
ManagedMethodBody,
} from "./ManagedMemberInspectorCore.js";
import type { ManagedMethodBody } from "./ManagedMemberInspectorCore.js";
import {
decodeInstructions,
parseExceptionRegions,
@@ -56,19 +53,16 @@ export const methodBody = (
bytes: Buffer,
pe: ManagedPeLayout,
rva: number,
limits: ManagedMemberInspectionLimits,
): ManagedMethodBody => {
if (rva === 0) return emptyMethodBody(rva, "absent", null);
try {
const offset = pe.rvaToOffset(rva, 1, "method.body");
const header = readMethodBodyHeader(bytes, offset);
if (header.ilSize > limits.maxMethodBodyBytes)
return oversizedMethodBody(header, rva, offset, limits);
const ilOffset = offset + header.size;
if (ilOffset > bytes.length - header.ilSize)
throw new RangeError("Method IL bytes leave artifact");
const il = bytes.subarray(ilOffset, ilOffset + header.ilSize);
const decoded = decodeInstructions(il, limits.maxMethodInstructions);
const decoded = decodeInstructions(il);
const opcodeCounts: Record<string, number> = {};
for (const instruction of decoded.parsed)
opcodeCounts[instruction.opcode] =
@@ -126,7 +120,7 @@ export const methodBody = (
issue:
decoded.issue ??
(decoded.truncated > 0
? `Instruction decode reached max_method_instructions ${String(limits.maxMethodInstructions)} before the end of the method body`
? "Instruction decoding stopped before the end of the method body"
: null),
};
} catch (cause: unknown) {
@@ -161,31 +155,3 @@ const emptyMethodBody = (
exception_regions: [],
issue,
});
const oversizedMethodBody = (
header: MethodBodyHeader,
rva: number,
offset: number,
limits: ManagedMemberInspectionLimits,
): ManagedMethodBody => ({
status: "too-large",
header_format: header.format,
rva,
file_offset: offset,
max_stack: header.maxStack,
init_locals: (header.flags & 0x10) !== 0,
local_var_sig_token:
header.localSig === 0
? null
: `0x${header.localSig.toString(16).padStart(8, "0")}`,
il_size: header.ilSize,
il_sha256: null,
normalized_il_sha256: null,
instruction_count: 0,
decoded_instruction_count: 0,
truncated_instructions: 0,
opcode_counts: {},
anchors: [],
exception_regions: [],
issue: `Method body exceeds max_method_body_bytes ${String(limits.maxMethodBodyBytes)}`,
});
+9 -9
View File
@@ -36,7 +36,7 @@ const flagsHex = (value: number): string =>
export const parseModuleRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
heapExtent: number,
): readonly ModuleRef[] => {
const refs: ModuleRef[] = [];
const table = layout.table(26);
@@ -49,7 +49,7 @@ export const parseModuleRefs = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
),
});
}
@@ -59,7 +59,7 @@ export const parseModuleRefs = (
export const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
heapExtent: number,
): ReadonlyMap<string, MemberCore> => {
const fields = new Map<string, MemberCore>();
const table = layout.table(4);
@@ -70,7 +70,7 @@ export const parseFields = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
cursor.readIndex(layout.blobIndexSize);
const token = metadataToken(4, row);
@@ -90,7 +90,7 @@ export const parseFields = (
export const parseMethods = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
heapExtent: number,
): ReadonlyMap<string, MemberCore> => {
const methods = new Map<string, MemberCore>();
const table = layout.table(6);
@@ -103,7 +103,7 @@ export const parseMethods = (
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
cursor.readIndex(layout.blobIndexSize);
cursor.readIndex(layout.tableIndexSize(8));
@@ -124,7 +124,7 @@ export const parseMethods = (
interface ParseImplMapsContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly maxBytes: number;
readonly heapExtent: number;
readonly modules: readonly ModuleRef[];
readonly members: ReadonlyMap<string, MemberCore>;
}
@@ -132,7 +132,7 @@ interface ParseImplMapsContext {
export const parseImplMaps = ({
bytes,
layout,
maxBytes,
heapExtent,
modules,
members,
}: ParseImplMapsContext): readonly NativeImport[] => {
@@ -151,7 +151,7 @@ export const parseImplMaps = ({
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
heapExtent,
);
const importScopeRow = cursor.readIndex(layout.tableIndexSize(26));
const importScopeToken =
@@ -6,8 +6,6 @@ import {
buildManagedPeFixture,
buildNativePeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
} from "./ManagedPe.fixture.js";
describe("managed native boundaries", () => {
@@ -23,7 +21,6 @@ describe("managed native boundaries", () => {
const result = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
);
expect(result.cli_native).toMatchObject({
@@ -81,7 +78,6 @@ describe("managed native boundaries", () => {
const paged = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(paged.methods).toHaveLength(1);
@@ -89,7 +85,6 @@ describe("managed native boundaries", () => {
const native = inspectManagedMembersBytes(
nativeBytes,
managedPeFixtureTarget(nativeBytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(native.metadata.status).toBe("absent");
expect(native.coverage.state).toBe("unavailable");
@@ -100,7 +95,6 @@ describe("managed native boundaries", () => {
const malformed = inspectManagedMembersBytes(
malformedBytes,
managedPeFixtureTarget(malformedBytes),
MANAGED_MEMBER_FIXTURE_LIMITS,
);
expect(malformed.metadata.status).toBe("malformed");
expect(malformed.coverage.issues).toEqual([
+6 -31
View File
@@ -27,12 +27,6 @@ import {
parseModuleRefs,
} from "./ManagedNativeBoundaryHelpers.js";
export interface ManagedNativeBoundaryInspectionLimits {
readonly maxMetadataBytes: number;
readonly maxTableRows: number;
readonly maxHeapItemBytes: number;
}
type Inventory = ReturnType<typeof readManagedMetadataInventory>;
const emptyInspection = (
@@ -92,7 +86,6 @@ const readBoundaryInventory = (
bytes: Buffer,
pe: ManagedPeLayout,
cli: NonNullable<ManagedPeLayout["cli"]>,
limits: ManagedNativeBoundaryInspectionLimits,
): {
readonly layout: ManagedMetadataLayout;
readonly inventory: Inventory;
@@ -106,7 +99,6 @@ const readBoundaryInventory = (
bytes,
metadataOffset,
cli.metadata.size,
limits.maxTableRows,
);
const resourceDirectory: ManagedResourceDirectory = {
offset: pe.rvaToOffset(
@@ -119,29 +111,15 @@ const readBoundaryInventory = (
const inventory = readManagedMetadataInventory(
bytes,
layout,
{
maxHeapItemBytes: limits.maxHeapItemBytes,
},
resourceDirectory,
);
return { layout, inventory };
};
const metadataLimitIssue = (
cli: NonNullable<ManagedPeLayout["cli"]>,
maxBytes: number,
): ManagedParseIssue => ({
code: "limit-exceeded",
scope: "cli.metadata",
offset: cli.headerOffset + 8,
detail: `CLI metadata size exceeds max_metadata_bytes ${String(maxBytes)}`,
});
/** Inspect managed/native boundary declarations from PE metadata without execution. */
export const inspectManagedNativeBoundariesBytes = (
bytes: Buffer,
target: BinaryTarget,
limits: ManagedNativeBoundaryInspectionLimits,
): ManagedNativeBoundaryInspection => {
let pe: ManagedPeLayout;
try {
@@ -157,27 +135,24 @@ export const inspectManagedNativeBoundariesBytes = (
pe.cliDirectoryPresent ? "malformed" : "not-managed",
pe.cliIssue === null ? [] : [pe.cliIssue],
);
if (pe.cli.metadata.size > limits.maxMetadataBytes)
return emptyInspection(target, bytes, "malformed", [
metadataLimitIssue(pe.cli, limits.maxMetadataBytes),
]);
let layout: ManagedMetadataLayout;
let inventory: Inventory;
try {
({ layout, inventory } = readBoundaryInventory(bytes, pe, pe.cli, limits));
({ layout, inventory } = readBoundaryInventory(bytes, pe, pe.cli));
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return emptyInspection(target, bytes, "malformed", [cause.issue]);
}
const moduleRefs = parseModuleRefs(bytes, layout, limits.maxHeapItemBytes);
const heapExtent = Math.max(layout.strings.size, layout.blob.size);
const moduleRefs = parseModuleRefs(bytes, layout, heapExtent);
const members = new Map([
...parseFields(bytes, layout, limits.maxHeapItemBytes),
...parseMethods(bytes, layout, limits.maxHeapItemBytes),
...parseFields(bytes, layout, heapExtent),
...parseMethods(bytes, layout, heapExtent),
]);
const imports = parseImplMaps({
bytes,
layout,
maxBytes: limits.maxHeapItemBytes,
heapExtent,
modules: moduleRefs,
members,
});
-23
View File
@@ -73,29 +73,6 @@ const DEFAULT_MVID = Buffer.from([
0xdd, 0xee, 0xff,
]);
/** Generous bounds for inventory assertions over the source-owned PE fixture. */
export const MANAGED_ARTIFACT_FIXTURE_LIMITS = {
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
};
/** Generous bounds for member assertions over the source-owned PE fixture. */
export const MANAGED_MEMBER_FIXTURE_LIMITS = {
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
maxMethodBodyBytes: 1024 * 1024,
maxMethodInstructions: 1_000,
};
/** Generous native-boundary bounds for the source-owned PE fixture. */
export const MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS = {
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
};
/** Describe fixture bytes as the managed PE target consumed by static inspectors. */
export const managedPeFixtureTarget = (
bytes: Buffer,
+8 -39
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import {
createAnalysisExecution,
@@ -37,7 +37,6 @@ import { err, ok } from "../domain/result.js";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
import { MANAGED_INSPECTION_DEFAULTS } from "./ManagedInspectionDefaults.js";
const IDENTITY: ProviderIdentity = Object.freeze(MANAGED_STATIC_PROVIDER);
@@ -108,29 +107,11 @@ class ManagedStaticClient implements AnalysisClient {
try {
if (options?.signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
const maxFileBytes = MANAGED_INSPECTION_DEFAULTS.maxFileBytes;
const metadata = await stat(this.target.path);
if (metadata.size > maxFileBytes)
return err(
new AnalysisCapabilityUnavailableError(
IDENTITY.id,
operation,
`Artifact size ${String(metadata.size)} exceeds max_file_bytes ${String(maxFileBytes)}.`,
),
);
const snapshot = this.#snapshotBytes;
const observed =
snapshot === undefined
? await readManagedSnapshot(this.target.path, options?.signal)
: await hashManagedSource(this.target.path, options?.signal);
if (observed.byteLength > maxFileBytes)
return err(
new AnalysisCapabilityUnavailableError(
IDENTITY.id,
operation,
`Artifact grew beyond max_file_bytes ${String(maxFileBytes)} while it was read.`,
),
);
if (observed.sha256 !== this.target.sha256)
return err(
new EvidenceIntegrityError(
@@ -202,6 +183,8 @@ const hashManagedSource = async (
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk))
throw new TypeError("Managed source stream returned non-buffer data");
if (chunk.length > Number.MAX_SAFE_INTEGER - byteLength)
throw new RangeError("Managed source byte length overflowed");
byteLength += chunk.length;
digest.update(chunk);
}
@@ -217,7 +200,7 @@ const limitationsFor = (operation: ManagedToolName): readonly string[] =>
Object.freeze(
operation === "inspect_managed_artifact"
? [
"This capability inventories PE/CLI identity only; inspect_managed_members admits bounded metadata members, signatures, and CIL anchors.",
"This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: operation === "inspect_managed_native_boundaries"
@@ -226,7 +209,7 @@ const limitationsFor = (operation: ManagedToolName): readonly string[] =>
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: [
"This capability decodes bounded PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
"This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
],
);
@@ -242,28 +225,14 @@ const inspectManagedOperation = (
| ManagedNativeBoundaryInspection => {
if (operation === "inspect_managed_artifact") {
managedArtifactInputSchema.parse(parameters);
return inspectManagedArtifactBytes(bytes, target, {
maxMetadataBytes: MANAGED_INSPECTION_DEFAULTS.maxMetadataBytes,
maxTableRows: MANAGED_INSPECTION_DEFAULTS.maxTableRows,
maxHeapItemBytes: MANAGED_INSPECTION_DEFAULTS.maxHeapItemBytes,
});
return inspectManagedArtifactBytes(bytes, target);
}
if (operation === "inspect_managed_native_boundaries") {
managedNativeBoundaryInputSchema.parse(parameters);
return inspectManagedNativeBoundariesBytes(bytes, target, {
maxMetadataBytes: MANAGED_INSPECTION_DEFAULTS.maxMetadataBytes,
maxTableRows: MANAGED_INSPECTION_DEFAULTS.maxTableRows,
maxHeapItemBytes: MANAGED_INSPECTION_DEFAULTS.maxHeapItemBytes,
});
return inspectManagedNativeBoundariesBytes(bytes, target);
}
managedMemberInputSchema.parse(parameters);
return inspectManagedMembersBytes(bytes, target, {
maxMetadataBytes: MANAGED_INSPECTION_DEFAULTS.maxMetadataBytes,
maxTableRows: MANAGED_INSPECTION_DEFAULTS.maxTableRows,
maxHeapItemBytes: MANAGED_INSPECTION_DEFAULTS.maxHeapItemBytes,
maxMethodBodyBytes: MANAGED_INSPECTION_DEFAULTS.maxMethodBodyBytes,
maxMethodInstructions: MANAGED_INSPECTION_DEFAULTS.maxMethodInstructions,
});
return inspectManagedMembersBytes(bytes, target);
};
const managedLocations = (
@@ -62,7 +62,7 @@ export const registerCompareManagedMembers = (
{
operation: "inspect_managed_members",
rationale:
"Repeat static member inspection with complete pages and method bodies.",
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved members must remain unknown.",
},
],
relationships: [],
@@ -79,7 +79,7 @@ export const registerVerifyManagedNativeBoundaries = (
{
operation: "inspect_managed_native_boundaries",
rationale:
"Repeat managed boundary inspection with complete ModuleRef, ImplMap, and native implementation pages.",
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved declarations must remain unknown.",
},
{
operation: "analyze_function",
@@ -22,7 +22,6 @@ import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberIns
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_MEMBER_FIXTURE_LIMITS,
} from "../../../src/dotnet/ManagedPe.fixture.js";
describe("managed runtime correlation planning", () => {
@@ -152,35 +151,6 @@ describe("managed runtime correlation planning", () => {
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisInputError");
});
it("rejects a runtime lock for instruction-limited CIL", async () => {
const directory = await createTestTempDirectory("rea-managed-runtime-");
const artifactPath = join(directory, "fixture.dll");
const executablePath = join(directory, "dotnet");
const bytes = buildManagedPeFixture();
await writeFile(artifactPath, bytes);
await writeFile(executablePath, "#!/bin/sh\n");
const fixture = inspect(bytes, artifactPath, {
...MANAGED_MEMBER_FIXTURE_LIMITS,
maxMethodInstructions: 1,
});
const authority = await authorityFor(directory, executablePath, true);
const result = await planManagedRuntimeCorrelationEvidence(
{
policy: () => enabledPolicy(directory, executablePath),
authority,
},
inputFor(fixture.evidence, fixture.method),
);
expect(fixture.method.body).toMatchObject({
status: "partial",
normalized_il_sha256: null,
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisInputError");
});
});
const enabledPolicy = (
@@ -248,13 +218,9 @@ const inputFor = (
},
});
const inspect = (
bytes: Buffer,
path: string,
limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS,
) => {
const inspect = (bytes: Buffer, path: string) => {
const target = managedPeFixtureTarget(bytes, path);
const result = inspectManagedMembersBytes(bytes, target, limits);
const result = inspectManagedMembersBytes(bytes, target);
const method = result.methods[0];
if (method === undefined) throw new Error("fixture has no method");
return {
@@ -14,9 +14,6 @@ import { inspectManagedNativeBoundariesBytes } from "../../../src/dotnet/Managed
import {
buildManagedPeFixture,
managedPeFixtureTarget,
MANAGED_ARTIFACT_FIXTURE_LIMITS,
MANAGED_MEMBER_FIXTURE_LIMITS,
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
} from "../../../src/dotnet/ManagedPe.fixture.js";
describe("managed application graph projection", () => {
@@ -29,21 +26,9 @@ describe("managed application graph projection", () => {
},
});
const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");
const managedArtifact = inspectManagedArtifactBytes(
bytes,
binary,
MANAGED_ARTIFACT_FIXTURE_LIMITS,
);
const members = inspectManagedMembersBytes(
bytes,
binary,
MANAGED_MEMBER_FIXTURE_LIMITS,
);
const boundaries = inspectManagedNativeBoundariesBytes(
bytes,
binary,
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
);
const managedArtifact = inspectManagedArtifactBytes(bytes, binary);
const members = inspectManagedMembersBytes(bytes, binary);
const boundaries = inspectManagedNativeBoundariesBytes(bytes, binary);
const artifactEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_artifact",
parameters: {},
@@ -185,21 +170,17 @@ describe("managed application graph coverage", () => {
it("preserves partial parser coverage in graph and per-fact coverage", () => {
const bytes = buildManagedPeFixture();
const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");
const members = inspectManagedMembersBytes(
bytes,
binary,
MANAGED_MEMBER_FIXTURE_LIMITS,
);
const members = inspectManagedMembersBytes(bytes, binary);
const parserPartialMembers = {
...members,
coverage: {
state: "partial" as const,
issues: [
{
code: "limit-exceeded" as const,
scope: "method.0x06000001.body.instructions",
code: "invalid-blob" as const,
scope: "metadata.#Blob",
offset: 0x0a00,
detail: "instruction limit reached",
detail: "Blob content leaves #Blob",
},
],
},