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,