From 975dff2e25b14b634e14dd5e0db1537d0ca9815b Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:02:16 +0800 Subject: [PATCH] fix(dotnet): bound RVA reads by section virtual size Use the shared resolver for virtual containment, overlap checks, and file-backed extent. Raw alignment padding no longer supplies method bytes or shadows a later virtual section. Preserve malformed body coverage and suppress CIL hashes outside the admitted range. Verify zero, exact-end, truncated-body, padding, and true-overlap cases through compiled CLI and MCP consumers. Synthetic PE fixtures only; no Windows loader or real managed-provider verification. Fixes #1596. Supersedes #1595. --- src/dotnet/ManagedPe.fixture.ts | 4 +- src/dotnet/ManagedPeReader.ts | 20 ++--- tests/acceptance/managedCliOutput.test.ts | 99 +++++++++++++++++++++++ tests/boundary/mcp/managedMcp.test.ts | 29 +++++++ 4 files changed, 139 insertions(+), 13 deletions(-) diff --git a/src/dotnet/ManagedPe.fixture.ts b/src/dotnet/ManagedPe.fixture.ts index b7056fd4ef..2ce3bbae01 100644 --- a/src/dotnet/ManagedPe.fixture.ts +++ b/src/dotnet/ManagedPe.fixture.ts @@ -32,6 +32,7 @@ interface ManagedPeFixtureOptions { readonly targetFramework?: string | null; readonly typeName?: string; readonly typeNamespace?: string; + readonly virtualSectionSize?: number; } const textEncoder = new TextEncoder(); @@ -846,7 +847,8 @@ const buildManagedFixtureImage = ( readyToRunOffset === null ? 0 : readyToRunOffset - 0x0200 + 4, ) / 0x0200, ) * 0x0200; - const virtualSectionSize = Math.max(0x1000, rawSectionSize); + const virtualSectionSize = + options.virtualSectionSize ?? Math.max(0x1000, rawSectionSize); const image = Buffer.alloc(0x0200 + rawSectionSize); image.write("MZ", 0, "ascii"); image.writeUInt32LE(0x80, 0x3c); diff --git a/src/dotnet/ManagedPeReader.ts b/src/dotnet/ManagedPeReader.ts index 8100a521f3..5f345d4026 100644 --- a/src/dotnet/ManagedPeReader.ts +++ b/src/dotnet/ManagedPeReader.ts @@ -139,14 +139,14 @@ const resolveRva = ( scope, `${scope} has an invalid RVA range`, ); + // VirtualSize owns RVA containment; raw size also includes alignment padding. const selected = sections.find((section) => { const within = rva - section.virtualAddress; - const mappedSize = Math.max(section.virtualSize, section.rawSize); return ( - mappedSize > 0 && + section.virtualSize > 0 && within >= 0 && - within <= mappedSize && - size <= mappedSize - within + within < section.virtualSize && + size <= section.virtualSize - within ); }); if (selected === undefined) @@ -156,13 +156,8 @@ const resolveRva = ( `${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); + if (section === selected || section.virtualSize === 0) continue; + const end = section.virtualAddress + section.virtualSize; const intersects = size === 0 ? rva >= section.virtualAddress && rva < end @@ -184,6 +179,7 @@ const resolveRva = ( const offset = add(selected.rawOffset, within, scope); requireRange(bytes, offset, size, scope); let available = Math.min( + selected.virtualSize - within, selected.rawSize - within, bytes.length - offset, 0x1_0000_0000 - rva, @@ -193,7 +189,7 @@ const resolveRva = ( if ( section !== selected && section.virtualAddress > rva && - Math.max(section.virtualSize, section.rawSize) > 0 + section.virtualSize > 0 ) available = Math.min(available, section.virtualAddress - rva); return { offset, available }; diff --git a/tests/acceptance/managedCliOutput.test.ts b/tests/acceptance/managedCliOutput.test.ts index 84cec5222c..be7506c85c 100644 --- a/tests/acceptance/managedCliOutput.test.ts +++ b/tests/acceptance/managedCliOutput.test.ts @@ -60,3 +60,102 @@ cliTest( expect(jsonl.json).toBe("inspect_managed_members"); }, ); + +cliTest( + "managed CLI bounds CIL claims by section virtual ranges", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-virtual-size-"); + for (const virtualSectionSize of [0, 0x800, 0x801, 0x802]) { + // The two-byte tiny method starts at RVA 0x2800. Its header alone fits + // at size 0x801, and its full CIL fits exactly at size 0x802. + const bytes = buildManagedPeFixture({ + virtualSectionSize, + ilBody: Buffer.from([0x06, 0x2a]), + }); + const path = join(root, `virtual-${virtualSectionSize}.dll`); + await writeFile(path, bytes); + const result = await cli.run({ + arguments: ["inspect-managed-members", path, "--json"], + }); + expect(result.exitCode).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: + virtualSectionSize === 0 + ? { + methods: [], + metadata: { status: "malformed" }, + coverage: { state: "unavailable" }, + } + : { + methods: [ + { + rva: 0x2800, + body: + virtualSectionSize === 0x802 + ? { + status: "present", + normalized_il_sha256: expect.any(String), + } + : { + status: "malformed", + normalized_il_sha256: null, + }, + }, + ], + coverage: { + state: virtualSectionSize === 0x802 ? "complete" : "partial", + }, + }, + }); + } + }, +); + +cliTest( + "managed CLI distinguishes raw padding from overlapping virtual sections", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-section-identity-"); + for (const virtualSectionSize of [0x800, 0x900]) { + const bytes = buildManagedPeFixture({ virtualSectionSize }); + const coff = bytes.readUInt32LE(0x3c) + 4; + const firstSection = coff + 20 + bytes.readUInt16LE(coff + 16); + const secondSection = firstSection + 40; + bytes.writeUInt16LE(2, coff + 2); + bytes.write(".il", secondSection, "ascii"); + bytes.writeUInt32LE(0x100, secondSection + 8); + bytes.writeUInt32LE(0x2800, secondSection + 12); + bytes.writeUInt32LE(0x100, secondSection + 16); + bytes.writeUInt32LE(0x0a00, secondSection + 20); + const path = join(root, `sections-${virtualSectionSize}.dll`); + await writeFile(path, bytes); + const result = await cli.run({ + arguments: ["inspect-managed-members", path, "--json"], + }); + expect(result.exitCode).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + methods: [ + { + rva: 0x2800, + body: + virtualSectionSize === 0x800 + ? { + status: "present", + file_offset: 0x0a00, + normalized_il_sha256: expect.any(String), + } + : { + status: "malformed", + normalized_il_sha256: null, + issue: expect.stringContaining("ambiguous overlapping"), + }, + }, + ], + coverage: { + state: virtualSectionSize === 0x800 ? "complete" : "partial", + }, + }, + }); + } + }, +); diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index 43043152ff..dc662eafe4 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -29,8 +29,16 @@ it("runs every managed static inspection independently of an active native targe ); const path = join(directory, "fixture.dll"); const otherPath = join(directory, "other-fixture.dll"); + const partialPath = join(directory, "partial-method.dll"); await writeFile(path, buildManagedPeFixture()); await writeFile(otherPath, buildManagedPeFixture({ methodName: "Other" })); + await writeFile( + partialPath, + buildManagedPeFixture({ + virtualSectionSize: 0x801, + ilBody: Buffer.from([0x06, 0x2a]), + }), + ); const session = composeBinarySession(new AnalysisProviderRegistry([]), [ new ManagedStaticProvider(), ]); @@ -116,6 +124,27 @@ it("runs every managed static inspection independently of an active native targe provider: { id: "rea-dotnet-static" }, subject: { local_path: path, format: "pe" }, }); + const partial = structured( + await client.callTool({ + name: "inspect_managed_members", + arguments: { path: partialPath }, + }), + ); + expect(partial).toMatchObject({ + subject: { local_path: partialPath, format: "pe" }, + normalized_result: { + methods: [ + { + body: { + status: "malformed", + normalized_il_sha256: null, + issue: "Method body leaves file-backed PE section data", + }, + }, + ], + coverage: { state: "partial" }, + }, + }); } finally { await Promise.all([client.close(), server.close()]); await session.close();