From c2cf7dcb86e11350214f82e5c51de7ca6ee3d27a Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:09:50 -0700 Subject: [PATCH] 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 --- src/dotnet/ManagedArtifactInspector.test.ts | 29 ++++++ src/dotnet/ManagedMetadataInventoryRows.ts | 100 +++++++++++++++----- 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index 638dfd353..5de925254 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -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", () => { diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 3bcaada9d..ebe034f7d 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -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; @@ -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 = (