fix(managed): budget decoding before representation expansion
Validation: integrated changes passed formatting, typecheck, lint, resolved module-boundary verification and git diff --check. Tests and real-provider workflows were not run.
This commit is contained in:
1 parent
6d4cefc4f7
commit
0f0d6e4d43
18 files changed
+705
-362
No files matched your search
@@ -38,6 +38,20 @@ Inspect separately obtained components explicitly and preserve their identities.
|
||||
A valid implementation marker establishes a candidate native boundary, not
|
||||
recovered native semantics or runtime behavior.
|
||||
|
||||
### Resource admission
|
||||
|
||||
The static provider admits regular input files up to 128 MiB before retaining
|
||||
their bytes. Reads remain bounded if a file grows after admission. Metadata,
|
||||
CIL decoding, and projected inspection records have a separate conservative
|
||||
64 MiB representation budget; shared heap strings are cached, but repeated
|
||||
output occurrences still consume that budget. These are inspection resource
|
||||
policies, not PE format limits or a hard bound on total V8 heap use.
|
||||
|
||||
Input overflow returns a resource constraint. Decode or projection overflow
|
||||
reports `resource-limit` with unavailable or partial coverage; omitted facts do
|
||||
not become empty observations. Ambiguous PE section mappings are rejected
|
||||
rather than interpreted according to section order.
|
||||
|
||||
## Analysis objective
|
||||
|
||||
REA's managed-code track is intended to answer five different questions without
|
||||
|
||||
@@ -282,6 +282,7 @@ const managedFieldAccessSchema = z.object({
|
||||
|
||||
const managedParseIssueSchema = z.object({
|
||||
code: z.enum([
|
||||
"resource-limit",
|
||||
"invalid-cli-header",
|
||||
"invalid-directory",
|
||||
"invalid-metadata-root",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
admitManagedProjection,
|
||||
managedDecodeBudget,
|
||||
} from "./ManagedDecodeBudget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
managedArtifactInspectionSchema,
|
||||
@@ -107,40 +111,44 @@ const unavailableResult = (
|
||||
: [
|
||||
"The PE has no CLI data directory; managed metadata and CIL are unavailable.",
|
||||
];
|
||||
return managedArtifactInspectionSchema.parse({
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: malformed ? "malformed" : "not-managed",
|
||||
container: "pe",
|
||||
runtime_family: "unknown",
|
||||
implementation: malformed ? "unknown" : "not-managed",
|
||||
managed_architecture: "unknown",
|
||||
evidence: [
|
||||
{
|
||||
code: malformed ? "cli-directory-malformed" : "cli-directory-absent",
|
||||
detail: limitations[0],
|
||||
file_offset: layout.cliIssue?.offset ?? null,
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
status: malformed ? "malformed" : "absent",
|
||||
version: null,
|
||||
stream_names: [],
|
||||
table_row_counts: {},
|
||||
},
|
||||
module: null,
|
||||
assembly: null,
|
||||
target_frameworks: [],
|
||||
references: [],
|
||||
resources: [],
|
||||
attributes: [],
|
||||
coverage: {
|
||||
state: malformed ? "partial" : "unavailable",
|
||||
issues,
|
||||
},
|
||||
limitations,
|
||||
});
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
admitManagedProjection({
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: malformed ? "malformed" : "not-managed",
|
||||
container: "pe",
|
||||
runtime_family: "unknown",
|
||||
implementation: malformed ? "unknown" : "not-managed",
|
||||
managed_architecture: "unknown",
|
||||
evidence: [
|
||||
{
|
||||
code: malformed
|
||||
? "cli-directory-malformed"
|
||||
: "cli-directory-absent",
|
||||
detail: limitations[0],
|
||||
file_offset: layout.cliIssue?.offset ?? null,
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
status: malformed ? "malformed" : "absent",
|
||||
version: null,
|
||||
stream_names: [],
|
||||
table_row_counts: {},
|
||||
},
|
||||
module: null,
|
||||
assembly: null,
|
||||
target_frameworks: [],
|
||||
references: [],
|
||||
resources: [],
|
||||
attributes: [],
|
||||
coverage: {
|
||||
state: malformed ? "partial" : "unavailable",
|
||||
issues,
|
||||
},
|
||||
limitations,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
interface RuntimeClassificationInput {
|
||||
@@ -288,7 +296,7 @@ const partialMetadataResult = ({
|
||||
}: PartialMetadataContext): ManagedArtifactInspection => ({
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: "malformed",
|
||||
status: issue.code === "resource-limit" ? "managed" : "malformed",
|
||||
container: "pe",
|
||||
runtime_family:
|
||||
(layout.cli?.flags ?? 1) & 1 ? "unknown" : "mixed-clr-native",
|
||||
@@ -303,7 +311,7 @@ const partialMetadataResult = ({
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
status: "malformed",
|
||||
status: issue.code === "resource-limit" ? "partial" : "malformed",
|
||||
version: null,
|
||||
stream_names: [],
|
||||
table_row_counts: {},
|
||||
@@ -316,7 +324,9 @@ const partialMetadataResult = ({
|
||||
attributes: [],
|
||||
coverage: { state: "partial", issues: [issue] },
|
||||
limitations: [
|
||||
"CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.",
|
||||
issue.code === "resource-limit"
|
||||
? "CLI metadata inspection stopped at its representation budget; omitted facts are unknown."
|
||||
: "CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.",
|
||||
],
|
||||
});
|
||||
|
||||
@@ -343,12 +353,14 @@ export const inspectManagedArtifactBytes = (
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
partialMetadataResult({
|
||||
target,
|
||||
bytes,
|
||||
layout,
|
||||
issue: cause.issue,
|
||||
}),
|
||||
admitManagedProjection(
|
||||
partialMetadataResult({
|
||||
target,
|
||||
bytes,
|
||||
layout,
|
||||
issue: cause.issue,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
const resourceIssues: ManagedParseIssue[] = [];
|
||||
@@ -356,43 +368,57 @@ export const inspectManagedArtifactBytes = (
|
||||
layout,
|
||||
resourceIssues,
|
||||
);
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
metadata,
|
||||
resourceDirectory,
|
||||
);
|
||||
const issues = [...resourceIssues, ...inventory.issues];
|
||||
const limitations = [
|
||||
...(issues.length === 0
|
||||
? []
|
||||
: [
|
||||
"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.",
|
||||
];
|
||||
return managedArtifactInspectionSchema.parse({
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: "managed",
|
||||
container: "pe",
|
||||
...runtimeClassification({ layout, metadata, inventory }),
|
||||
},
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: metadata.version,
|
||||
stream_names: metadata.streamNames,
|
||||
table_row_counts: managedTableRowCounts(metadata),
|
||||
},
|
||||
module: inventory.module,
|
||||
assembly: inventory.assembly,
|
||||
target_frameworks: inventory.targetFrameworks,
|
||||
references: inventory.references,
|
||||
resources: inventory.resources,
|
||||
attributes: inventory.attributes,
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations,
|
||||
});
|
||||
try {
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
metadata,
|
||||
resourceDirectory,
|
||||
);
|
||||
const issues = [...resourceIssues, ...inventory.issues];
|
||||
const limitations = [
|
||||
...(issues.length === 0
|
||||
? []
|
||||
: [
|
||||
"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.",
|
||||
];
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
admitManagedProjection(
|
||||
{
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: "managed",
|
||||
container: "pe",
|
||||
...runtimeClassification({ layout, metadata, inventory }),
|
||||
},
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: metadata.version,
|
||||
stream_names: metadata.streamNames,
|
||||
table_row_counts: managedTableRowCounts(metadata),
|
||||
},
|
||||
module: inventory.module,
|
||||
assembly: inventory.assembly,
|
||||
target_frameworks: inventory.targetFrameworks,
|
||||
references: inventory.references,
|
||||
resources: inventory.resources,
|
||||
attributes: inventory.attributes,
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations,
|
||||
},
|
||||
managedDecodeBudget(metadata),
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
admitManagedProjection(
|
||||
partialMetadataResult({ target, bytes, layout, issue: cause.issue }),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
|
||||
/** Per-inspection capacity for eagerly retained metadata and its JSON projection. */
|
||||
const MAX_METADATA_REPRESENTATION_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
interface CachedString {
|
||||
readonly value: string;
|
||||
readonly encodedBytes: number;
|
||||
}
|
||||
|
||||
/** The layout owns decoding capacity and heap identity for all of its consumers. */
|
||||
export class ManagedDecodeBudget {
|
||||
#remaining = MAX_METADATA_REPRESENTATION_BYTES;
|
||||
readonly #strings = new Map<number, CachedString>();
|
||||
readonly #digests = new WeakMap<ArrayBufferLike, Map<string, string>>();
|
||||
|
||||
reserve(bytes: number, offset: number | null): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw managedFailure(
|
||||
"resource-limit",
|
||||
"metadata.representation",
|
||||
`Managed metadata exceeds the ${String(MAX_METADATA_REPRESENTATION_BYTES)} byte representation budget`,
|
||||
offset,
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
}
|
||||
|
||||
/** UTF-8 cannot decode to more UTF-16 units than bytes; JSON uses at most six bytes per unit. */
|
||||
reserveString(encodedBytes: number, offset: number | null): void {
|
||||
this.reserve(encodedBytes * 6 + 2, offset);
|
||||
}
|
||||
|
||||
/** Snapshot buffers are immutable within a layout; cache exact backing-buffer ranges. */
|
||||
digest(bytes: Buffer, algorithm: "sha256" | "sha1"): string {
|
||||
let ranges = this.#digests.get(bytes.buffer);
|
||||
if (ranges === undefined) {
|
||||
ranges = new Map();
|
||||
this.#digests.set(bytes.buffer, ranges);
|
||||
}
|
||||
const key = `${algorithm}:${String(bytes.byteOffset)}:${String(bytes.byteLength)}`;
|
||||
const cached = ranges.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
this.reserve(256, null);
|
||||
const digest = createHash(algorithm).update(bytes).digest("hex");
|
||||
ranges.set(key, digest);
|
||||
return digest;
|
||||
}
|
||||
|
||||
cachedString(index: number): CachedString | undefined {
|
||||
return this.#strings.get(index);
|
||||
}
|
||||
|
||||
cacheString(index: number, value: string, encodedBytes: number): void {
|
||||
this.#strings.set(index, { value, encodedBytes });
|
||||
}
|
||||
}
|
||||
|
||||
const budgets = new WeakMap<ManagedMetadataLayout, ManagedDecodeBudget>();
|
||||
|
||||
/** Transfer metadata-root admission capacity to its completed immutable layout. */
|
||||
export const attachManagedDecodeBudget = (
|
||||
layout: ManagedMetadataLayout,
|
||||
budget: ManagedDecodeBudget,
|
||||
): void => {
|
||||
budgets.set(layout, budget);
|
||||
};
|
||||
|
||||
/** Share one owner for the lifetime of the admitted layout, including all inspection facets. */
|
||||
export const managedDecodeBudget = (
|
||||
layout: ManagedMetadataLayout,
|
||||
): ManagedDecodeBudget => {
|
||||
let budget = budgets.get(layout);
|
||||
if (budget === undefined) {
|
||||
budget = new ManagedDecodeBudget();
|
||||
budgets.set(layout, budget);
|
||||
}
|
||||
return budget;
|
||||
};
|
||||
|
||||
/** Admit repeated values too: shared heap strings still expand at the JSON boundary. */
|
||||
export const admitManagedProjection = <Value>(
|
||||
value: Value,
|
||||
budget: ManagedDecodeBudget = new ManagedDecodeBudget(),
|
||||
): Value => {
|
||||
const visit = (item: unknown): void => {
|
||||
if (typeof item === "string") {
|
||||
budget.reserveString(item.length, null);
|
||||
} else if (Array.isArray(item)) {
|
||||
budget.reserve(item.length + 2, null);
|
||||
for (const child of item) visit(child);
|
||||
} else if (item !== null && typeof item === "object") {
|
||||
budget.reserve(2, null);
|
||||
for (const key of Object.keys(item)) {
|
||||
budget.reserve(key.length * 6 + 4, null);
|
||||
visit(Reflect.get(item, key));
|
||||
}
|
||||
} else {
|
||||
budget.reserve(32, null);
|
||||
}
|
||||
};
|
||||
visit(value);
|
||||
return value;
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
admitManagedProjection,
|
||||
managedDecodeBudget,
|
||||
} from "./ManagedDecodeBudget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
@@ -31,40 +35,47 @@ const unavailable = (
|
||||
bytes: Buffer,
|
||||
issue: ManagedParseIssue | null,
|
||||
): ManagedMemberInspection =>
|
||||
managedMemberInspectionSchema.parse({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: null,
|
||||
metadata: {
|
||||
status: issue === null ? "absent" : "malformed",
|
||||
version: null,
|
||||
table_row_counts: {},
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: null,
|
||||
},
|
||||
types: [],
|
||||
fields: [],
|
||||
methods: [],
|
||||
member_refs: [],
|
||||
call_edges: [],
|
||||
field_accesses: [],
|
||||
coverage: {
|
||||
state: "unavailable",
|
||||
issues: issue === null ? [] : [issue],
|
||||
},
|
||||
limitations: [
|
||||
issue === null
|
||||
? "The PE has no admitted CLI metadata; managed member inspection is unavailable."
|
||||
: "The CLI metadata could not be admitted; managed member inspection is unavailable.",
|
||||
],
|
||||
});
|
||||
managedMemberInspectionSchema.parse(
|
||||
admitManagedProjection({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: null,
|
||||
metadata: {
|
||||
status:
|
||||
issue === null
|
||||
? "absent"
|
||||
: issue.code === "resource-limit"
|
||||
? "partial"
|
||||
: "malformed",
|
||||
version: null,
|
||||
table_row_counts: {},
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: null,
|
||||
},
|
||||
types: [],
|
||||
fields: [],
|
||||
methods: [],
|
||||
member_refs: [],
|
||||
call_edges: [],
|
||||
field_accesses: [],
|
||||
coverage: {
|
||||
state: "unavailable",
|
||||
issues: issue === null ? [] : [issue],
|
||||
},
|
||||
limitations: [
|
||||
issue === null
|
||||
? "The PE has no admitted CLI metadata; managed member inspection is unavailable."
|
||||
: "The CLI metadata could not be admitted; managed member inspection is unavailable.",
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const readMemberInventory = (
|
||||
bytes: Buffer,
|
||||
@@ -132,41 +143,48 @@ export const inspectManagedMembersBytes = (
|
||||
fields.core,
|
||||
memberRefs.core,
|
||||
);
|
||||
return managedMemberInspectionSchema.parse({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
types: parsedTypes.types,
|
||||
fields: fields.fields,
|
||||
methods: methods.methods,
|
||||
member_refs: memberRefs.refs,
|
||||
call_edges: related.callEdges,
|
||||
field_accesses: related.fieldAccesses,
|
||||
coverage: {
|
||||
state:
|
||||
issues.length === 0 && !incompleteMembers ? "complete" : "partial",
|
||||
issues: coverageIssues,
|
||||
},
|
||||
limitations: [
|
||||
"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.",
|
||||
],
|
||||
});
|
||||
return managedMemberInspectionSchema.parse(
|
||||
admitManagedProjection(
|
||||
{
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
types: parsedTypes.types,
|
||||
fields: fields.fields,
|
||||
methods: methods.methods,
|
||||
member_refs: memberRefs.refs,
|
||||
call_edges: related.callEdges,
|
||||
field_accesses: related.fieldAccesses,
|
||||
coverage: {
|
||||
state:
|
||||
issues.length === 0 && !incompleteMembers
|
||||
? "complete"
|
||||
: "partial",
|
||||
issues: coverageIssues,
|
||||
},
|
||||
limitations: [
|
||||
"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.",
|
||||
],
|
||||
},
|
||||
managedDecodeBudget(layout),
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ManagedReaderFailure)
|
||||
return unavailable(target, bytes, cause.issue);
|
||||
|
||||
@@ -405,10 +405,13 @@ const callingConvention = (value: number): string => {
|
||||
return flags.length === 0 ? name : `${name} ${flags.join(" ")}`;
|
||||
};
|
||||
|
||||
export const signature = (blob: Buffer): ManagedSignature => {
|
||||
export const signature = (
|
||||
blob: Buffer,
|
||||
rawSha256?: string,
|
||||
): ManagedSignature => {
|
||||
const raw = {
|
||||
raw_length: blob.length,
|
||||
raw_sha256: sha256Bytes(blob),
|
||||
raw_sha256: rawSha256 ?? sha256Bytes(blob),
|
||||
};
|
||||
try {
|
||||
if (blob.length === 0)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import type {
|
||||
ManagedExceptionRegion,
|
||||
ManagedInstructionAnchor,
|
||||
@@ -434,6 +435,7 @@ export const parseExceptionRegions = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
methodEnd: number,
|
||||
budget?: ManagedDecodeBudget,
|
||||
): ExceptionRegionParseResult => {
|
||||
const regions: ManagedExceptionRegion[] = [];
|
||||
let sectionOffset = offset;
|
||||
@@ -487,7 +489,14 @@ export const parseExceptionRegions = (
|
||||
);
|
||||
|
||||
const count = (size - 4) / clauseSize;
|
||||
regions.push(...readExceptionClauses(bytes, sectionOffset + 4, count, fat));
|
||||
budget?.reserve(count * 256, sectionOffset);
|
||||
for (const region of readExceptionClauses(
|
||||
bytes,
|
||||
sectionOffset + 4,
|
||||
count,
|
||||
fat,
|
||||
))
|
||||
regions.push(region);
|
||||
|
||||
hasMoreSections = (kind & 0x80) !== 0;
|
||||
sectionOffset += size;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import type { ManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
@@ -64,7 +65,7 @@ export const parseFields = (
|
||||
declaring_type: declared?.fullName ?? null,
|
||||
name,
|
||||
flags,
|
||||
signature: signature(sig),
|
||||
signature: budgetedSignature(sig, layout),
|
||||
});
|
||||
core.set(token, { token, name });
|
||||
}, issues);
|
||||
@@ -129,7 +130,7 @@ export const parseMemberRefs = (
|
||||
layout.rowCounts,
|
||||
),
|
||||
name,
|
||||
signature: signature(sig),
|
||||
signature: budgetedSignature(sig, layout),
|
||||
});
|
||||
core.set(token, { token, name });
|
||||
}, issues);
|
||||
@@ -189,8 +190,14 @@ export const parseMethods = ({
|
||||
rva,
|
||||
impl_flags: implFlags,
|
||||
flags,
|
||||
signature: signature(sig),
|
||||
body: methodBody(bytes, pe, rva, { implFlags, flags }),
|
||||
signature: budgetedSignature(sig, layout),
|
||||
body: methodBody(
|
||||
bytes,
|
||||
pe,
|
||||
rva,
|
||||
{ implFlags, flags },
|
||||
managedDecodeBudget(layout),
|
||||
),
|
||||
});
|
||||
core.set(token, {
|
||||
token,
|
||||
@@ -258,3 +265,11 @@ export const edges = (
|
||||
}
|
||||
return { callEdges, fieldAccesses };
|
||||
};
|
||||
|
||||
const budgetedSignature = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
): ReturnType<typeof signature> => {
|
||||
managedDecodeBudget(layout).reserve(bytes.length * 256, layout.blob.offset);
|
||||
return signature(bytes, managedDecodeBudget(layout).digest(bytes, "sha256"));
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
|
||||
import {
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
@@ -117,6 +119,11 @@ export const readMetadataString = (
|
||||
layout.strings.offset + Math.max(index, 0),
|
||||
);
|
||||
const start = layout.strings.offset + index;
|
||||
const cached = managedDecodeBudget(layout).cachedString(index);
|
||||
if (cached !== undefined && cached.encodedBytes <= heapExtent) {
|
||||
managedDecodeBudget(layout).reserveString(cached.encodedBytes, start);
|
||||
return cached.value;
|
||||
}
|
||||
const maximum = Math.min(
|
||||
layout.strings.offset + layout.strings.size,
|
||||
start + heapExtent + 1,
|
||||
@@ -130,10 +137,14 @@ export const readMetadataString = (
|
||||
"String heap item has no terminator within #Strings",
|
||||
start,
|
||||
);
|
||||
managedDecodeBudget(layout).reserveString(end - start, start);
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes.subarray(start, end),
|
||||
);
|
||||
const value = new TextDecoder("utf-8", {
|
||||
fatal: true,
|
||||
ignoreBOM: true,
|
||||
}).decode(bytes.subarray(start, end));
|
||||
managedDecodeBudget(layout).cacheString(index, value, end - start);
|
||||
return value;
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
throw managedFailure(
|
||||
@@ -218,12 +229,6 @@ export const metadataToken = (table: number, row: number): string =>
|
||||
export const sha256Bytes = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** Derive the conventional strong-name token from a full public key. */
|
||||
export const strongNameToken = (publicKey: Buffer): string =>
|
||||
Buffer.from(createHash("sha1").update(publicKey).digest().subarray(-8))
|
||||
.reverse()
|
||||
.toString("hex");
|
||||
|
||||
/** Create a cursor bounded to one admitted metadata table row. */
|
||||
export const metadataRowCursor = (
|
||||
bytes: Buffer,
|
||||
@@ -240,6 +245,8 @@ export const metadataRowCursor = (
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
// Reserve per-row object/index bookkeeping before readers retain decoded facts.
|
||||
managedDecodeBudget(layout).reserve(256, start);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
|
||||
import type {
|
||||
ManagedArtifactInspection,
|
||||
ManagedParseIssue,
|
||||
@@ -14,8 +16,6 @@ import {
|
||||
readMetadataBlob,
|
||||
readMetadataGuid,
|
||||
readMetadataString,
|
||||
sha256Bytes,
|
||||
strongNameToken,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
|
||||
@@ -29,15 +29,22 @@ type CustomAttribute = ManagedArtifactInspection["attributes"][number];
|
||||
const publicKeyIdentity = (
|
||||
bytes: Buffer,
|
||||
kind: "public-key" | "public-key-token",
|
||||
layout: ManagedMetadataLayout,
|
||||
): AssemblyIdentity["public_key"] => ({
|
||||
kind: bytes.length === 0 ? "none" : kind,
|
||||
byte_length: bytes.length,
|
||||
sha256: bytes.length === 0 ? null : sha256Bytes(bytes),
|
||||
sha256:
|
||||
bytes.length === 0
|
||||
? null
|
||||
: managedDecodeBudget(layout).digest(bytes, "sha256"),
|
||||
token:
|
||||
bytes.length === 0
|
||||
? null
|
||||
: kind === "public-key"
|
||||
? strongNameToken(bytes)
|
||||
? Buffer.from(managedDecodeBudget(layout).digest(bytes, "sha1"), "hex")
|
||||
.subarray(-8)
|
||||
.reverse()
|
||||
.toString("hex")
|
||||
: bytes.length === 8
|
||||
? bytes.toString("hex")
|
||||
: null,
|
||||
@@ -127,6 +134,7 @@ export const readAssembly = (
|
||||
public_key: publicKeyIdentity(
|
||||
key,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
layout,
|
||||
),
|
||||
token: metadataToken(32, 1),
|
||||
row_offset: table.offset,
|
||||
@@ -179,8 +187,12 @@ export const readAssemblyReference = (
|
||||
public_key_or_token: publicKeyIdentity(
|
||||
keyOrToken,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
layout,
|
||||
),
|
||||
hash_value_sha256: hashValue.length === 0 ? null : sha256Bytes(hashValue),
|
||||
hash_value_sha256:
|
||||
hashValue.length === 0
|
||||
? null
|
||||
: managedDecodeBudget(layout).digest(hashValue, "sha256"),
|
||||
hash_value_length: hashValue.length,
|
||||
token: metadataToken(35, row),
|
||||
row_offset: metadataRowOffset(layout, 35, row),
|
||||
@@ -338,13 +350,17 @@ const decodeCompressedLength = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const decodeFixedString = (blob: Buffer): string | null => {
|
||||
const decodeFixedString = (
|
||||
blob: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
): string | null => {
|
||||
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
|
||||
return null;
|
||||
const length = decodeCompressedLength(blob, 2);
|
||||
if (length === undefined) return null;
|
||||
const start = 2 + length.prefix;
|
||||
if (start > blob.length - length.length) return null;
|
||||
managedDecodeBudget(layout).reserveString(length.length, layout.blob.offset);
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
blob.subarray(start, start + length.length),
|
||||
@@ -427,10 +443,10 @@ export const readCustomAttribute = (
|
||||
),
|
||||
type_name: typeName,
|
||||
value_length: value.length,
|
||||
value_sha256: sha256Bytes(value),
|
||||
value_sha256: managedDecodeBudget(layout).digest(value, "sha256"),
|
||||
decoded_fixed_string:
|
||||
typeName === "System.Runtime.Versioning.TargetFrameworkAttribute"
|
||||
? decodeFixedString(value)
|
||||
? decodeFixedString(value, layout)
|
||||
: null,
|
||||
token: metadataToken(12, row),
|
||||
row_offset: cursor.start,
|
||||
@@ -505,8 +521,9 @@ export const readResource = ({
|
||||
detail: "Embedded resource length leaves the CLI resources directory",
|
||||
});
|
||||
} else {
|
||||
dataSha256 = sha256Bytes(
|
||||
dataSha256 = managedDecodeBudget(layout).digest(
|
||||
bytes.subarray(start + 4, start + 4 + dataLength),
|
||||
"sha256",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
attachManagedDecodeBudget,
|
||||
ManagedDecodeBudget,
|
||||
} from "./ManagedDecodeBudget.js";
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import {
|
||||
computeRowSize,
|
||||
@@ -208,6 +212,7 @@ interface ParsedStreams {
|
||||
const readMetadataStreams = (
|
||||
bytes: Buffer,
|
||||
rootOffset: number,
|
||||
budget: ManagedDecodeBudget,
|
||||
): ParsedStreams => {
|
||||
if (bytes.readUInt32LE(0) !== 0x424a_5342)
|
||||
throw managedFailure(
|
||||
@@ -220,8 +225,10 @@ const readMetadataStreams = (
|
||||
requireRange(bytes, 16, versionLength, "metadata.version");
|
||||
const versionBytes = bytes.subarray(16, 16 + versionLength);
|
||||
const zero = versionBytes.indexOf(0);
|
||||
const decodedVersionLength = zero < 0 ? versionBytes.length : zero;
|
||||
budget.reserveString(decodedVersionLength, rootOffset + 16);
|
||||
const version = versionBytes
|
||||
.subarray(0, zero < 0 ? versionBytes.length : zero)
|
||||
.subarray(0, decodedVersionLength)
|
||||
.toString("utf8");
|
||||
let cursor = align4(16 + versionLength);
|
||||
requireRange(bytes, cursor, 4, "metadata.stream-count");
|
||||
@@ -390,7 +397,8 @@ export const readManagedMetadataLayout = (
|
||||
rootOffset,
|
||||
);
|
||||
const bytes = artifact.subarray(rootOffset, rootOffset + size);
|
||||
const { streams, version } = readMetadataStreams(bytes, rootOffset);
|
||||
const budget = new ManagedDecodeBudget();
|
||||
const { streams, version } = readMetadataStreams(bytes, rootOffset, budget);
|
||||
const { tableBytes, tablesOffset, heaps, valid } = readTablesHeader(
|
||||
artifact,
|
||||
streams,
|
||||
@@ -403,7 +411,7 @@ export const readManagedMetadataLayout = (
|
||||
valid,
|
||||
tablesOffset,
|
||||
});
|
||||
return {
|
||||
const layout: ManagedMetadataLayout = {
|
||||
rootOffset,
|
||||
size,
|
||||
version,
|
||||
@@ -420,6 +428,8 @@ export const readManagedMetadataLayout = (
|
||||
tableIndexSize: (index) => tableSize(index, rowCounts),
|
||||
codedIndexSize: (name) => codedSize(name, rowCounts),
|
||||
};
|
||||
attachManagedDecodeBudget(layout, budget);
|
||||
return layout;
|
||||
};
|
||||
|
||||
/** Resolve a one-based metadata row to its exact file offset. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import type { ManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import type { ManagedMethodBody } from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
@@ -62,19 +63,6 @@ const readMethodBodyHeader = (
|
||||
};
|
||||
};
|
||||
|
||||
const readExceptionRegions = (
|
||||
bytes: Buffer,
|
||||
sectionOffset: number,
|
||||
methodEnd: number,
|
||||
ilSize: number,
|
||||
instructionOffsets: readonly number[],
|
||||
): ReturnType<typeof parseExceptionRegions> =>
|
||||
validateExceptionRegionRanges(
|
||||
parseExceptionRegions(bytes, sectionOffset, methodEnd),
|
||||
ilSize,
|
||||
instructionOffsets,
|
||||
);
|
||||
|
||||
/** Decode admitted managed CIL, retaining unavailable implementation metadata as partial. */
|
||||
export const methodBody = (
|
||||
bytes: Buffer,
|
||||
@@ -84,6 +72,7 @@ export const methodBody = (
|
||||
implFlags: 0,
|
||||
flags: 0,
|
||||
},
|
||||
budget?: ManagedDecodeBudget,
|
||||
): ManagedMethodBody => {
|
||||
if (rva === 0) return emptyMethodBody(rva, "absent", null);
|
||||
if (
|
||||
@@ -108,6 +97,9 @@ export const methodBody = (
|
||||
if (ilOffset > bytes.length - header.ilSize)
|
||||
throw new RangeError("Method IL bytes leave artifact");
|
||||
const il = bytes.subarray(ilOffset, ilOffset + header.ilSize);
|
||||
// One byte can produce an instruction record, start index, normalized tuple,
|
||||
// and anchor. Reserve conservative bookkeeping capacity before decoding.
|
||||
budget?.reserve(il.length * 256, ilOffset);
|
||||
const decoded = decodeInstructions(il);
|
||||
const opcodeCounts: Record<string, number> = {};
|
||||
for (const instruction of decoded.parsed)
|
||||
@@ -135,10 +127,13 @@ export const methodBody = (
|
||||
const sectionOffset = (methodEnd + 3) & ~3;
|
||||
const exceptionRegions =
|
||||
header.format === "fat" && (header.flags & 8) !== 0
|
||||
? readExceptionRegions(
|
||||
bytes,
|
||||
sectionOffset,
|
||||
offset + methodExtent,
|
||||
? validateExceptionRegionRanges(
|
||||
parseExceptionRegions(
|
||||
bytes,
|
||||
sectionOffset,
|
||||
offset + methodExtent,
|
||||
budget,
|
||||
),
|
||||
header.ilSize,
|
||||
decoded.instructionStarts,
|
||||
)
|
||||
@@ -179,6 +174,11 @@ export const methodBody = (
|
||||
: null),
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
cause instanceof ManagedReaderFailure &&
|
||||
cause.issue.code === "resource-limit"
|
||||
)
|
||||
throw cause;
|
||||
if (
|
||||
!(cause instanceof ManagedReaderFailure) &&
|
||||
!(cause instanceof RangeError)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
admitManagedProjection,
|
||||
managedDecodeBudget,
|
||||
} from "./ManagedDecodeBudget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
@@ -427,40 +431,45 @@ export const buildNativeBoundaryInspection = ({
|
||||
native,
|
||||
issues,
|
||||
}: BoundaryInspectionContext): ManagedNativeBoundaryInspection =>
|
||||
managedNativeBoundaryInspectionSchema.parse({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: moduleRefs,
|
||||
pinvoke_imports: imports,
|
||||
native_implementations: implementations,
|
||||
summary: nativeBoundarySummary(native, {
|
||||
module_ref_count: moduleRefs.length,
|
||||
pinvoke_import_count: imports.length,
|
||||
native_implementation_count: implementations.length,
|
||||
}),
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations: [
|
||||
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
|
||||
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
|
||||
],
|
||||
});
|
||||
managedNativeBoundaryInspectionSchema.parse(
|
||||
admitManagedProjection(
|
||||
{
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: moduleRefs,
|
||||
pinvoke_imports: imports,
|
||||
native_implementations: implementations,
|
||||
summary: nativeBoundarySummary(native, {
|
||||
module_ref_count: moduleRefs.length,
|
||||
pinvoke_import_count: imports.length,
|
||||
native_implementation_count: implementations.length,
|
||||
}),
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations: [
|
||||
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
|
||||
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
|
||||
],
|
||||
},
|
||||
managedDecodeBudget(layout),
|
||||
),
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { admitManagedProjection } from "./ManagedDecodeBudget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
@@ -47,39 +48,45 @@ const emptyInspection = (
|
||||
readonly issues?: readonly ManagedParseIssue[];
|
||||
} = {},
|
||||
): ManagedNativeBoundaryInspection => {
|
||||
return managedNativeBoundaryInspectionSchema.parse({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: null,
|
||||
metadata: {
|
||||
status: classification === "malformed" ? "malformed" : "absent",
|
||||
version: null,
|
||||
table_row_counts: {},
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: [],
|
||||
pinvoke_imports: [],
|
||||
native_implementations: [],
|
||||
summary: nativeBoundarySummary(native, {
|
||||
module_ref_count: 0,
|
||||
pinvoke_import_count: 0,
|
||||
native_implementation_count: 0,
|
||||
return managedNativeBoundaryInspectionSchema.parse(
|
||||
admitManagedProjection({
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: null,
|
||||
metadata: {
|
||||
status: issues.some((issue) => issue.code === "resource-limit")
|
||||
? "partial"
|
||||
: classification === "malformed"
|
||||
? "malformed"
|
||||
: "absent",
|
||||
version: null,
|
||||
table_row_counts: {},
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: [],
|
||||
pinvoke_imports: [],
|
||||
native_implementations: [],
|
||||
summary: nativeBoundarySummary(native, {
|
||||
module_ref_count: 0,
|
||||
pinvoke_import_count: 0,
|
||||
native_implementation_count: 0,
|
||||
}),
|
||||
coverage: { state: "unavailable", issues },
|
||||
limitations: [
|
||||
"No CLI metadata was admitted; native boundary declarations are unavailable.",
|
||||
"Static inspection does not load or execute target code, so native export resolution is not performed.",
|
||||
],
|
||||
}),
|
||||
coverage: { state: "unavailable", issues },
|
||||
limitations: [
|
||||
"No CLI metadata was admitted; native boundary declarations are unavailable.",
|
||||
"Static inspection does not load or execute target code, so native export resolution is not performed.",
|
||||
],
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const readBoundaryInventory = (
|
||||
@@ -142,40 +149,48 @@ export const inspectManagedNativeBoundariesBytes = (
|
||||
issues: [cause.issue],
|
||||
});
|
||||
}
|
||||
const heapExtent = Math.max(layout.strings.size, layout.blob.size);
|
||||
issues.push(...inventory.issues);
|
||||
const moduleRefs = parseModuleRefs(bytes, layout, heapExtent, issues);
|
||||
const members = new Map([
|
||||
...parseFields(bytes, layout, heapExtent, issues),
|
||||
...parseMethods(bytes, layout, heapExtent, issues),
|
||||
]);
|
||||
const imports = parseImplMaps({
|
||||
bytes,
|
||||
layout,
|
||||
heapExtent,
|
||||
modules: moduleRefs,
|
||||
members,
|
||||
issues,
|
||||
});
|
||||
const pinvokeTokens = new Set(
|
||||
imports
|
||||
.map(({ member_token }) => member_token)
|
||||
.filter((token): token is string => token !== null),
|
||||
);
|
||||
const implementations = nativeImplementations(
|
||||
members.values(),
|
||||
pinvokeTokens,
|
||||
);
|
||||
return buildNativeBoundaryInspection({
|
||||
target,
|
||||
bytes,
|
||||
pe,
|
||||
layout,
|
||||
inventory,
|
||||
moduleRefs,
|
||||
imports,
|
||||
implementations,
|
||||
native: cliNative(pe),
|
||||
issues,
|
||||
});
|
||||
try {
|
||||
const heapExtent = Math.max(layout.strings.size, layout.blob.size);
|
||||
issues.push(...inventory.issues);
|
||||
const moduleRefs = parseModuleRefs(bytes, layout, heapExtent, issues);
|
||||
const members = new Map([
|
||||
...parseFields(bytes, layout, heapExtent, issues),
|
||||
...parseMethods(bytes, layout, heapExtent, issues),
|
||||
]);
|
||||
const imports = parseImplMaps({
|
||||
bytes,
|
||||
layout,
|
||||
heapExtent,
|
||||
modules: moduleRefs,
|
||||
members,
|
||||
issues,
|
||||
});
|
||||
const pinvokeTokens = new Set(
|
||||
imports
|
||||
.map(({ member_token }) => member_token)
|
||||
.filter((token): token is string => token !== null),
|
||||
);
|
||||
const implementations = nativeImplementations(
|
||||
members.values(),
|
||||
pinvokeTokens,
|
||||
);
|
||||
return buildNativeBoundaryInspection({
|
||||
target,
|
||||
bytes,
|
||||
pe,
|
||||
layout,
|
||||
inventory,
|
||||
moduleRefs,
|
||||
imports,
|
||||
implementations,
|
||||
native: cliNative(pe),
|
||||
issues,
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return emptyInspection(target, bytes, "malformed", {
|
||||
native: cliNative(pe),
|
||||
issues: [...issues, cause.issue],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -121,31 +121,87 @@ const architectureFor = (machine: number): ManagedPeLayout["architecture"] => {
|
||||
}
|
||||
};
|
||||
|
||||
const mapRva = (mapping: RvaMapping): number => {
|
||||
/** Resolve one section identity for both exact reads and available-byte queries. */
|
||||
const resolveRva = (
|
||||
mapping: RvaMapping,
|
||||
): { readonly offset: number; readonly available: number } => {
|
||||
const { bytes, sections, rva, size, scope } = mapping;
|
||||
if (rva === 0 && size === 0) return 0;
|
||||
for (const section of sections) {
|
||||
const mappedSize = Math.max(section.virtualSize, section.rawSize);
|
||||
if (rva < section.virtualAddress) continue;
|
||||
if (
|
||||
!Number.isSafeInteger(rva) ||
|
||||
!Number.isSafeInteger(size) ||
|
||||
rva < 0 ||
|
||||
size < 0 ||
|
||||
rva > 0xffff_ffff ||
|
||||
(size > 0 && size > 0x1_0000_0000 - rva)
|
||||
)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} has an invalid RVA range`,
|
||||
);
|
||||
const selected = sections.find((section) => {
|
||||
const within = rva - section.virtualAddress;
|
||||
if (within > mappedSize || size > mappedSize - within) continue;
|
||||
if (within > section.rawSize || size > section.rawSize - within)
|
||||
const mappedSize = Math.max(section.virtualSize, section.rawSize);
|
||||
return (
|
||||
mappedSize > 0 &&
|
||||
within >= 0 &&
|
||||
within <= mappedSize &&
|
||||
size <= mappedSize - within
|
||||
);
|
||||
});
|
||||
if (selected === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} RVA is not covered by one PE section`,
|
||||
);
|
||||
for (const section of sections) {
|
||||
if (
|
||||
section === selected ||
|
||||
Math.max(section.virtualSize, section.rawSize) === 0
|
||||
)
|
||||
continue;
|
||||
const end =
|
||||
section.virtualAddress + Math.max(section.virtualSize, section.rawSize);
|
||||
const intersects =
|
||||
size === 0
|
||||
? rva >= section.virtualAddress && rva < end
|
||||
: rva < end && rva + size > section.virtualAddress;
|
||||
if (intersects)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} occupies virtual bytes without file-backed data`,
|
||||
`${scope} RVA range has ambiguous overlapping PE section mappings`,
|
||||
);
|
||||
const offset = add(section.rawOffset, within, scope);
|
||||
requireRange(bytes, offset, size, scope);
|
||||
return offset;
|
||||
}
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} RVA is not covered by a PE section`,
|
||||
const within = rva - selected.virtualAddress;
|
||||
if (within > selected.rawSize || size > selected.rawSize - within)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} occupies virtual bytes without file-backed data`,
|
||||
);
|
||||
const offset = add(selected.rawOffset, within, scope);
|
||||
requireRange(bytes, offset, size, scope);
|
||||
let available = Math.min(
|
||||
selected.rawSize - within,
|
||||
bytes.length - offset,
|
||||
0x1_0000_0000 - rva,
|
||||
);
|
||||
// A future overlap ends the uniquely mapped extent even if the current byte is unique.
|
||||
for (const section of sections)
|
||||
if (
|
||||
section !== selected &&
|
||||
section.virtualAddress > rva &&
|
||||
Math.max(section.virtualSize, section.rawSize) > 0
|
||||
)
|
||||
available = Math.min(available, section.virtualAddress - rva);
|
||||
return { offset, available };
|
||||
};
|
||||
|
||||
const mapRva = (mapping: RvaMapping): number =>
|
||||
mapping.rva === 0 && mapping.size === 0 ? 0 : resolveRva(mapping).offset;
|
||||
|
||||
const readDirectory = (bytes: Buffer, offset: number): PeDataDirectory => {
|
||||
requireRange(bytes, offset, 8, "pe.data-directory");
|
||||
return {
|
||||
@@ -356,22 +412,7 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
cliIssue,
|
||||
rvaToOffset: (rva, size, scope) =>
|
||||
mapRva({ bytes, sections, rva, size, scope }),
|
||||
rvaAvailableBytes: (rva, scope) => {
|
||||
mapRva({ bytes, sections, rva, size: 1, scope });
|
||||
for (const section of sections) {
|
||||
if (rva < section.virtualAddress) continue;
|
||||
const within = rva - section.virtualAddress;
|
||||
if (within < section.rawSize)
|
||||
return Math.min(
|
||||
section.rawSize - within,
|
||||
bytes.length - section.rawOffset - within,
|
||||
);
|
||||
}
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
scope,
|
||||
`${scope} RVA is not covered by file-backed PE section data`,
|
||||
);
|
||||
},
|
||||
rvaAvailableBytes: (rva, scope) =>
|
||||
resolveRva({ bytes, sections, rva, size: 1, scope }).available,
|
||||
};
|
||||
};
|
||||
@@ -27,7 +27,11 @@ export const readManagedValue = <Value>(
|
||||
try {
|
||||
return operation();
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
if (
|
||||
!(cause instanceof ManagedReaderFailure) ||
|
||||
cause.issue.code === "resource-limit"
|
||||
)
|
||||
throw cause;
|
||||
issues.push(cause.issue);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
@@ -18,11 +16,12 @@ import {
|
||||
managedTargetInputSchema,
|
||||
type ManagedToolName,
|
||||
} from "../contracts/managed/managedToolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -38,6 +37,23 @@ import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
|
||||
|
||||
import { openRegularFile } from "../filesystem/RegularFile.js";
|
||||
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
||||
|
||||
// This provider eagerly retains the full PE snapshot. Keep its input resource
|
||||
// policy separate from decoded metadata/output capacity; it is not a PE format limit.
|
||||
const MAX_MANAGED_SNAPSHOT_BYTES = 128 * 1024 * 1024;
|
||||
|
||||
const snapshotCapacityError = (
|
||||
operation: string,
|
||||
): AnalysisResourceConstraintError =>
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"memory",
|
||||
"Managed static inspection requires an eagerly retained PE snapshot within its input byte budget.",
|
||||
{ max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES },
|
||||
);
|
||||
|
||||
/** Execution-free managed PE/CLI auxiliary provider. */
|
||||
export class ManagedStaticProvider implements AnalysisProvider {
|
||||
readonly #capabilities: readonly CapabilityDescriptor[] =
|
||||
@@ -90,8 +106,16 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
const snapshot = this.#snapshotBytes;
|
||||
const observed =
|
||||
snapshot === undefined
|
||||
? await readManagedSnapshot(this.target.path, options?.signal)
|
||||
: await hashManagedSource(this.target.path, options?.signal);
|
||||
? await readManagedSnapshot(
|
||||
this.target.path,
|
||||
operation,
|
||||
options?.signal,
|
||||
)
|
||||
: await hashManagedSource(
|
||||
this.target.path,
|
||||
operation,
|
||||
options?.signal,
|
||||
);
|
||||
if (observed.sha256 !== this.target.sha256)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
@@ -119,6 +143,7 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
} catch (cause: unknown) {
|
||||
if (options?.signal?.aborted === true)
|
||||
return err(new AnalysisCancelledError(operation));
|
||||
if (cause instanceof AnalysisError) return err(cause);
|
||||
return err(
|
||||
new ProviderAdapterError(MANAGED_STATIC_PROVIDER.id, operation, {
|
||||
cause,
|
||||
@@ -141,38 +166,59 @@ interface ManagedSourceObservation {
|
||||
|
||||
const readManagedSnapshot = async (
|
||||
path: string,
|
||||
operation: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ManagedSourceObservation> => {
|
||||
const bytes = await readFile(
|
||||
path,
|
||||
signal === undefined ? undefined : { signal },
|
||||
);
|
||||
return {
|
||||
byteLength: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes,
|
||||
};
|
||||
const handle = await openRegularFile(path, { symlinks: "follow", signal });
|
||||
try {
|
||||
const metadata = await handle.stat();
|
||||
if (metadata.size > MAX_MANAGED_SNAPSHOT_BYTES)
|
||||
throw snapshotCapacityError(operation);
|
||||
const bytes = await readBoundedFileBytes(
|
||||
handle,
|
||||
MAX_MANAGED_SNAPSHOT_BYTES,
|
||||
signal,
|
||||
);
|
||||
if (bytes === undefined) throw snapshotCapacityError(operation);
|
||||
signal?.throwIfAborted();
|
||||
return {
|
||||
byteLength: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes,
|
||||
};
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
|
||||
const hashManagedSource = async (
|
||||
path: string,
|
||||
operation: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ManagedSourceObservation> => {
|
||||
const digest = createHash("sha256");
|
||||
let byteLength = 0;
|
||||
const stream = createReadStream(
|
||||
path,
|
||||
signal === undefined ? undefined : { signal },
|
||||
);
|
||||
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);
|
||||
const handle = await openRegularFile(path, { symlinks: "follow", signal });
|
||||
try {
|
||||
if ((await handle.stat()).size > MAX_MANAGED_SNAPSHOT_BYTES)
|
||||
throw snapshotCapacityError(operation);
|
||||
const digest = createHash("sha256");
|
||||
let byteLength = 0;
|
||||
const stream = handle.createReadStream({
|
||||
autoClose: false,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
signal?.throwIfAborted();
|
||||
if (!Buffer.isBuffer(chunk))
|
||||
throw new TypeError("Managed source stream returned non-buffer data");
|
||||
if (chunk.length > MAX_MANAGED_SNAPSHOT_BYTES - byteLength)
|
||||
throw snapshotCapacityError(operation);
|
||||
byteLength += chunk.length;
|
||||
digest.update(chunk);
|
||||
}
|
||||
return { byteLength, sha256: digest.digest("hex") };
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
return { byteLength, sha256: digest.digest("hex") };
|
||||
};
|
||||
|
||||
const isManagedOperation = (
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { FileHandle } from "node:fs/promises";
|
||||
export const readBoundedFileBytes = async (
|
||||
handle: FileHandle,
|
||||
maxBytes: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Buffer | undefined> => {
|
||||
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
||||
throw new RangeError("File byte limit must be a nonnegative safe integer");
|
||||
@@ -11,6 +12,7 @@ export const readBoundedFileBytes = async (
|
||||
const buffer = Buffer.allocUnsafe(Math.min(64 * 1_024, maxBytes + 1));
|
||||
let total = 0;
|
||||
for (;;) {
|
||||
signal?.throwIfAborted();
|
||||
const { bytesRead } = await handle.read(
|
||||
buffer,
|
||||
0,
|
||||
|
||||
Reference in new issue
Block a user