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.
This commit is contained in:
morluto committed 2026-10-11 07:02:16 +08:00
1 parent 1b527f4898
commit 975dff2e25
4 files changed
+139 -13

No files matched your search

+3 -1
View File
@@ -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);
+8 -12
View File
@@ -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 };
+99
View File
@@ -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",
},
},
});
}
},
);
+29
View File
@@ -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();