From 2a9252e4e6e04d8f24dda816ea61b76c75005e39 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Wed, 14 Oct 2026 17:06:06 -0700 Subject: [PATCH] fix(managed): report metadata range failures at artifact file offsets (#3508) ManagedMetadataLayout's range check reported offsets relative to the buffer it was given: the metadata root for version and stream headers, and the table stream for the tables header, row counts and table data. Other diagnostics in the same reader already use artifact file offsets, so a truncated table stream at file offset 888 was reported at offset 24, inside the DOS header. Pass each buffer's artifact file offset to the range check so every invalid-metadata-root issue carries a file offset. Fixes #3364 Co-authored-by: Claude Opus 5.5 --- src/dotnet/ManagedMetadataExtraData.test.ts | 23 ++++++++++- src/dotnet/ManagedMetadataLayout.ts | 45 ++++++++++++++++----- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/dotnet/ManagedMetadataExtraData.test.ts b/src/dotnet/ManagedMetadataExtraData.test.ts index 38434390b9..012e8d3352 100644 --- a/src/dotnet/ManagedMetadataExtraData.test.ts +++ b/src/dotnet/ManagedMetadataExtraData.test.ts @@ -153,7 +153,8 @@ describe("CLR ExtraData index widths and boundaries", () => { it("reports a truncated row count before interpreting ExtraData", () => { const bytes = buildManagedPeFixture({ metadataExtraData: 0x1234_5678 }); - const { streamHeader, extraDataOffset } = fixtureTablesStream(bytes); + const { streamHeader, tablesOffset, extraDataOffset } = + fixtureTablesStream(bytes); bytes.writeUInt32LE(extraDataOffset - 1, streamHeader + 4); const result = inspectManagedMembersBytes( bytes, @@ -166,6 +167,8 @@ describe("CLR ExtraData index widths and boundaries", () => { expect.objectContaining({ code: "invalid-metadata-root", scope: "metadata.table-row-count", + // The last row count's artifact file offset, not its stream offset. + offset: tablesOffset + extraDataOffset - 4, }), ], }); @@ -173,6 +176,24 @@ describe("CLR ExtraData index widths and boundaries", () => { expect(result.methods).toEqual([]); }); + it("reports a stream extent beyond the metadata root at its file offset", () => { + const bytes = buildManagedPeFixture(); + const { streamHeader, tablesOffset } = fixtureTablesStream(bytes); + bytes.writeUInt32LE(0xffff_ffff, streamHeader + 4); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.coverage.issues).toEqual([ + expect.objectContaining({ + code: "invalid-metadata-root", + scope: "metadata.#~", + offset: tablesOffset, + }), + ]); + }); + it.each([0, 1, 3])( "rejects an ExtraData word with only %i declared bytes", (availableBytes) => { diff --git a/src/dotnet/ManagedMetadataLayout.ts b/src/dotnet/ManagedMetadataLayout.ts index b388546453..9ad02a0aa7 100644 --- a/src/dotnet/ManagedMetadataLayout.ts +++ b/src/dotnet/ManagedMetadataLayout.ts @@ -116,8 +116,10 @@ const CODED_INDEXES = { const SUPPORTED_TABLE_MASK = (1n << BigInt(METADATA_TABLE_NAMES.length)) - 1n; +/** Check a range within `bytes`, which start at artifact file offset `base`. */ const requireRange = ( bytes: Buffer, + base: number, offset: number, length: number, scope: string, @@ -133,7 +135,7 @@ const requireRange = ( "invalid-metadata-root", scope, `${scope} leaves the metadata byte range`, - Number.isSafeInteger(offset) && offset >= 0 ? offset : null, + Number.isSafeInteger(base + offset) && offset >= 0 ? base + offset : null, ); }; @@ -224,7 +226,7 @@ const readMetadataStreams = ( rootOffset, ); const versionLength = bytes.readUInt32LE(12); - requireRange(bytes, 16, versionLength, "metadata.version"); + requireRange(bytes, rootOffset, 16, versionLength, "metadata.version"); const versionBytes = bytes.subarray(16, 16 + versionLength); const zero = versionBytes.indexOf(0); const decodedVersionLength = zero < 0 ? versionBytes.length : zero; @@ -233,7 +235,7 @@ const readMetadataStreams = ( .subarray(0, decodedVersionLength) .toString("utf8"); let cursor = align4(16 + versionLength); - requireRange(bytes, cursor, 4, "metadata.stream-count"); + requireRange(bytes, rootOffset, cursor, 4, "metadata.stream-count"); const streamCount = bytes.readUInt16LE(cursor + 2); if (streamCount === 0 || streamCount > 64) throw managedFailure( @@ -245,7 +247,7 @@ const readMetadataStreams = ( cursor += 4; const streams = new Map(); for (let index = 0; index < streamCount; index += 1) { - requireRange(bytes, cursor, 8, "metadata.stream-header"); + requireRange(bytes, rootOffset, cursor, 8, "metadata.stream-header"); const relativeOffset = bytes.readUInt32LE(cursor); const streamSize = bytes.readUInt32LE(cursor + 4); const named = readStreamName(bytes, cursor + 8); @@ -256,7 +258,13 @@ const readMetadataStreams = ( `Duplicate metadata stream ${named.name}`, rootOffset + cursor, ); - requireRange(bytes, relativeOffset, streamSize, `metadata.${named.name}`); + requireRange( + bytes, + rootOffset, + relativeOffset, + streamSize, + `metadata.${named.name}`, + ); streams.set(named.name, { name: named.name, offset: rootOffset + relativeOffset, @@ -295,7 +303,13 @@ const readTablesHeader = ( tablesStream.offset, tablesStream.offset + tablesStream.size, ); - requireRange(tableBytes, 0, 24, "metadata.tables-header"); + requireRange( + tableBytes, + tablesStream.offset, + 0, + 24, + "metadata.tables-header", + ); const heapSizes = tableBytes[6] ?? 0; const heaps = { string: (heapSizes & 0x01) === 0 ? (2 as const) : (4 as const), @@ -315,13 +329,20 @@ const readTablesHeader = ( const readRowCounts = ( tableBytes: Buffer, + tablesOffset: number, valid: bigint, ): readonly number[] => { const rowCounts = Array(METADATA_TABLE_NAMES.length).fill(0); let tableCursor = 24; for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) { if ((valid & (1n << BigInt(index))) === 0n) continue; - requireRange(tableBytes, tableCursor, 4, "metadata.table-row-count"); + requireRange( + tableBytes, + tablesOffset, + tableCursor, + 4, + "metadata.table-row-count", + ); const count = tableBytes.readUInt32LE(tableCursor); rowCounts[index] = count; tableCursor += 4; @@ -384,7 +405,13 @@ const buildMetadataTables = ({ "Metadata table byte size overflowed", tablesOffset + tableCursor, ); - requireRange(tableBytes, tableCursor, byteLength, "metadata.table-data"); + requireRange( + tableBytes, + tablesOffset, + tableCursor, + byteLength, + "metadata.table-data", + ); tables.set(index, { index, name: METADATA_TABLE_NAMES[index] ?? `Table${String(index)}`, @@ -417,7 +444,7 @@ export const readManagedMetadataLayout = ( artifact, streams, ); - const rowCounts = readRowCounts(tableBytes, valid); + const rowCounts = readRowCounts(tableBytes, tablesOffset, valid); const tables = buildMetadataTables({ tableBytes, rowCounts,