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:
1 parent
1b527f4898
commit
975dff2e25
4 files changed
+139
-13
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
Reference in new issue
Block a user