fix(managed): index attribute constructor owners once per layout (#1943)

Each CustomAttribute whose constructor is a local MethodDef scanned the
TypeDef table to find the declaring type, so inspect_managed_artifact did
O(attributes x types) row decoding. A 584 KB assembly with 16,000 types
and 16,000 such attributes blocked for about 38 s.

Build a first-match owner index once per metadata layout. Already-owned
list positions are skipped, so overlapping malformed method lists keep
their previous first-match owner in near-linear time. The index is
charged to the decode budget, and a build failure is cached like the
member pointer lookup.

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kaoru0822
2026-10-12 04:09:50 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 14935e4d47
commit c2cf7dcb86
2 changed files with 105 additions and 24 deletions
@@ -259,6 +259,35 @@ describe("managed artifact coded indexes", () => {
layout.codedIndexSize("HasCustomAttribute"),
);
});
it("resolves local attribute constructors without rescanning TypeDef rows", () => {
// The owning type follows every empty type. A per-attribute TypeDef scan
// took about 38 s here; the owner index is built once per layout.
const bytes = buildManagedPeFixture({
additionalEmptyTypes: 16_000,
customAttributeRowCount: 16_000,
customAttributeTypeRaw: (1 << 3) | 2,
typeName: "LookupAttribute",
extendsRaw: 0,
methodName: ".ctor",
methodFlags: 0x1886,
methodSignature: Buffer.from([0x20, 0x01, 0x01, 0x0e]),
});
const start = performance.now();
const artifact = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(performance.now() - start).toBeLessThan(2000);
expect(artifact.coverage).toEqual({ state: "complete", issues: [] });
expect(artifact.attributes).toHaveLength(16_000);
expect(
artifact.attributes.every(
(attribute) => attribute.type_name === "Fixture.LookupAttribute",
),
).toBe(true);
}, 30_000);
});
describe("managed artifact reference inventory", () => {
+76 -24
View File
@@ -22,7 +22,11 @@ import {
readMetadataGuid,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
import {
managedFailure,
ManagedReaderFailure,
readManagedValue,
} from "./ManagedReaderFailure.js";
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
@@ -240,6 +244,69 @@ const readTypeName = ({
return namespace.length === 0 ? name : `${namespace}.${name}`;
};
const methodOwners = new WeakMap<
ManagedMetadataLayout,
Uint32Array | ManagedReaderFailure
>();
/**
* Index the first TypeDef whose method list contains each list position, once
* per layout. Malformed lists may overlap; skipping already-owned positions
* keeps first-match ownership without rescanning TypeDef rows per attribute.
*/
const methodOwnerRows = (
bytes: Buffer,
layout: ManagedMetadataLayout,
types: number,
positions: number,
): Uint32Array => {
const cached = methodOwners.get(layout);
if (cached instanceof ManagedReaderFailure) throw cached;
if (cached !== undefined) return cached;
try {
managedDecodeBudget(layout).reserve(
(2 * positions + 3) * Uint32Array.BYTES_PER_ELEMENT + 256,
layout.table(2)?.offset ?? null,
);
const owners = new Uint32Array(positions + 1);
// next[p] leads to the first unowned position at or after p.
const next = Uint32Array.from({ length: positions + 2 }, (_, p) => p);
const unowned = (position: number): number => {
let root = position;
while (next[root] !== root) root = next[root] ?? root;
for (let p = position; p !== root;) {
const following = next[p] ?? root;
next[p] = root;
p = following;
}
return root;
};
let start = types === 0 ? 0 : methodListForType(bytes, layout, 1);
for (let row = 1; row <= types; row += 1) {
const end =
row === types
? positions + 1
: methodListForType(bytes, layout, row + 1);
const limit = Math.min(end, positions + 1);
for (
let p = unowned(Math.min(Math.max(start, 1), positions + 1));
p < limit;
p = unowned(p + 1)
) {
owners[p] = row;
next[p] = p + 1;
}
start = end;
}
methodOwners.set(layout, owners);
return owners;
} catch (cause: unknown) {
// Invalid metadata must not rebuild the index for every attribute.
if (cause instanceof ManagedReaderFailure) methodOwners.set(layout, cause);
throw cause;
}
};
const declaringTypeForMethod = (
bytes: Buffer,
layout: ManagedMetadataLayout,
@@ -252,29 +319,14 @@ const declaringTypeForMethod = (
if (methodRow < 1 || methodRow > (layout.table(6)?.rowCount ?? 0))
return null;
const position = memberListRowLookup(bytes, layout, 6)(methodRow);
for (let row = 1; row <= types.rowCount; row += 1) {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
const methodEnd =
row === types.rowCount
? methods.rowCount + 1
: methodListForType(bytes, layout, row + 1);
if (position < methodStart || position >= methodEnd) continue;
const name = readMetadataString(bytes, layout, nameIndex, heapExtent);
const namespace = readMetadataString(
bytes,
layout,
namespaceIndex,
heapExtent,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
}
return null;
const owner = methodOwnerRows(
bytes,
layout,
types.rowCount,
methods.rowCount,
)[position];
if (owner === undefined || owner === 0) return null;
return readTypeName({ bytes, layout, table: 2, row: owner, heapExtent });
};
const methodListForType = (