fix(test-fixtures): keep managed metadata and method bodies disjoint

Lay out resources, ReadyToRun data and IL bodies from their actual extents. Preserve RVA zero for bodyless methods and verify adjacent bodies through the production inspector.
This commit is contained in:
morluto 2026-10-14 08:58:34 +08:00
parent a769915f6e
commit 537467ab9e
3 changed files with 155 additions and 13 deletions

View File

@ -3,6 +3,8 @@ export interface ManagedPeFixtureMethod {
readonly flags?: number;
readonly implFlags?: number;
readonly name: string;
/** An explicit metadata RVA; zero represents a method without an IL body. */
readonly rva?: number;
}
export interface ManagedPeFixtureOptions {
@ -19,6 +21,7 @@ export interface ManagedPeFixtureOptions {
readonly pinvoke?: {
readonly importName?: string;
readonly mappingFlags?: number;
readonly memberRow?: number;
readonly moduleName?: string;
};
readonly readyToRun?: boolean;

View File

@ -280,7 +280,7 @@ const buildManagedMetadata = (options, methods) => {
methodDefRow({
name: stringsForMethods[index],
signature: methodSignature,
rva: method.rva ?? 0x2800 + index * 0x20,
rva: method.rva,
flags: method.flags ?? 0x0016,
implFlags: method.implFlags ?? 0,
}),
@ -325,13 +325,64 @@ const buildManagedMetadata = (options, methods) => {
return metadata;
};
const align = (value, boundary) => Math.ceil(value / boundary) * boundary;
const managedFixtureLayout = (options, methods, metadataLength) => {
const resourceLength =
4 + (options.resourceData ?? Buffer.from("resource-data")).length;
const metadataOffset = 0x0300;
const resourceOffset = Math.max(
0x0800,
align(metadataOffset + metadataLength, 0x200),
);
const readyToRunOffset = Math.max(
0x0900,
align(resourceOffset + resourceLength, 0x200),
);
const methodOffsets = [];
let nextMethodOffset = Math.max(
0x0a00,
align(readyToRunOffset + (options.readyToRun === true ? 4 : 0), 0x200),
);
for (const method of methods) {
// RVA zero denotes a method without a mapped IL body, such as P/Invoke.
if (method.rva === 0) {
methodOffsets.push(null);
continue;
}
const offset =
method.rva === undefined
? nextMethodOffset
: 0x0200 + (method.rva - 0x2000);
methodOffsets.push(offset);
nextMethodOffset =
Math.max(offset, nextMethodOffset) +
align(Math.max(0x20, (method.body ?? retOnlyIlBody).length), 0x20);
}
return { metadataOffset, resourceOffset, readyToRunOffset, methodOffsets };
};
const buildManagedImage = (options, methods, metadata) => {
const resourceData = options.resourceData ?? Buffer.from("resource-data");
const resourceDirectory = Buffer.concat([
u32(resourceData.length),
resourceData,
]);
const image = Buffer.alloc(0x1000);
const { metadataOffset, resourceOffset, readyToRunOffset, methodOffsets } =
managedFixtureLayout(options, methods, metadata.length);
const contentEnd = Math.max(
metadataOffset + metadata.length,
resourceOffset + resourceDirectory.length,
readyToRunOffset + (options.readyToRun === true ? 4 : 0),
...methodOffsets.flatMap((offset, index) =>
offset === null
? []
: [offset + (methods[index].body ?? retOnlyIlBody).length],
),
0x1000,
);
const imageSize = align(contentEnd, 0x200);
const image = Buffer.alloc(imageSize);
image.write("MZ", 0, "ascii");
image.writeUInt32LE(0x80, 0x3c);
image.writeUInt32LE(0x0000_4550, 0x80);
@ -351,16 +402,16 @@ const buildManagedImage = (options, methods, metadata) => {
else image.writeUInt32LE(0x0040_0000, optional + 28);
image.writeUInt32LE(0x1000, optional + 32);
image.writeUInt32LE(0x200, optional + 36);
image.writeUInt32LE(0x3000, optional + 56);
image.writeUInt32LE(align(0x2000 + imageSize - 0x200, 0x1000), optional + 56);
image.writeUInt32LE(0x200, optional + 60);
image.writeUInt32LE(16, optional + directoryOffset - 4);
image.writeUInt32LE(0x2000, optional + directoryOffset + 14 * 8);
image.writeUInt32LE(72, optional + directoryOffset + 14 * 8 + 4);
const section = optional + optionalSize;
image.write(".text\0\0\0", section, "ascii");
image.writeUInt32LE(0x1000, section + 8);
image.writeUInt32LE(imageSize - 0x200, section + 8);
image.writeUInt32LE(0x2000, section + 12);
image.writeUInt32LE(0x0e00, section + 16);
image.writeUInt32LE(imageSize - 0x200, section + 16);
image.writeUInt32LE(0x0200, section + 20);
image.writeUInt32LE(0x6000_0020, section + 36);
const cli = 0x0200;
@ -371,27 +422,41 @@ const buildManagedImage = (options, methods, metadata) => {
image.writeUInt32LE(metadata.length, cli + 12);
image.writeUInt32LE(options.cliFlags ?? 1, cli + 16);
image.writeUInt32LE(0x0600_0001, cli + 20);
image.writeUInt32LE(0x2600, cli + 24);
image.writeUInt32LE(0x2000 + (resourceOffset - 0x0200), cli + 24);
image.writeUInt32LE(resourceDirectory.length, cli + 28);
if (options.readyToRun === true) {
image.writeUInt32LE(0x2700, cli + 64);
image.writeUInt32LE(0x2000 + (readyToRunOffset - 0x0200), cli + 64);
image.writeUInt32LE(4, cli + 68);
image.write("RTR\0", 0x0900, "ascii");
image.write("RTR\0", readyToRunOffset, "ascii");
}
methods.forEach((method, index) => {
const offset = 0x0a00 + index * 0x20;
const offset = methodOffsets[index];
if (offset === null) return;
(method.body ?? retOnlyIlBody).copy(image, offset);
});
metadata.copy(image, 0x0300);
resourceDirectory.copy(image, 0x0800);
metadata.copy(image, metadataOffset);
resourceDirectory.copy(image, resourceOffset);
return image;
};
/** Build a source-owned PE/CLI fixture without committing compiled binaries. */
export const buildManagedPeFixture = (options = {}) => {
const methods = managedFixtureMethods(options);
const metadata = buildManagedMetadata(options, methods);
return buildManagedImage(options, methods, metadata);
const provisionalMetadata = buildManagedMetadata(
options,
methods.map((method) => ({ ...method, rva: method.rva ?? 0x2800 })),
);
const layout = managedFixtureLayout(
options,
methods,
provisionalMetadata.length,
);
const methodsWithRvas = methods.map((method, index) => ({
...method,
rva: method.rva ?? 0x2000 + (layout.methodOffsets[index] - 0x0200),
}));
const metadata = buildManagedMetadata(options, methodsWithRvas);
return buildManagedImage(options, methodsWithRvas, metadata);
};
export const buildNativePeFixture = () => {
const image = buildManagedPeFixture();

View File

@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { buildManagedPeFixture } from "../../scripts/fixtures/managed/pe.mjs";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { managedPeFixtureTarget } from "./ManagedPe.fixture.js";
const rawOffset = (rva: number) => 0x200 + (rva - 0x2000);
describe("managed PE fixture layout", () => {
it("preserves bodyless native methods without allocating an IL extent", () => {
const image = buildManagedPeFixture({
methods: [
{ name: "NativeMethod", flags: 0x2016, rva: 0 },
{ name: "ManagedMethod", body: Buffer.from([0x06, 0x2a]) },
],
});
const inspected = inspectManagedMembersBytes(
image,
managedPeFixtureTarget(image),
);
expect(inspected.methods[0]?.rva).toBe(0);
expect(inspected.methods[0]?.body.status).toBe("absent");
expect(inspected.methods[0]?.body.opcode_counts).toEqual({});
expect(inspected.methods[1]?.body.opcode_counts).toEqual({ ret: 1 });
});
it("preserves complete adjacent method bodies", () => {
const firstBody = Buffer.concat([
Buffer.from([0x9e]),
Buffer.alloc(38),
Buffer.from([0x2a]),
]);
const secondBody = Buffer.from([0x06, 0x2a]);
const image = buildManagedPeFixture({
methods: [
{ name: "LongBody", body: firstBody },
{ name: "NextBody", body: secondBody },
],
});
expect(
image.subarray(rawOffset(0x2800), rawOffset(0x2800) + firstBody.length),
).toEqual(firstBody);
expect(
image.subarray(rawOffset(0x2840), rawOffset(0x2840) + secondBody.length),
).toEqual(secondBody);
const inspected = inspectManagedMembersBytes(
image,
managedPeFixtureTarget(image),
);
expect(inspected.methods.map(({ body }) => body.opcode_counts)).toEqual([
{ nop: 38, ret: 1 },
{ ret: 1 },
]);
});
it("keeps the ReadyToRun signature outside expanded metadata", () => {
const image = buildManagedPeFixture({
readyToRun: true,
methods: Array.from({ length: 47 }, (_, index) => ({
name: `Method${index}`,
})),
});
const nativeHeaderRva = image.readUInt32LE(0x240);
const nativeHeaderSize = image.readUInt32LE(0x244);
expect(nativeHeaderSize).toBe(4);
expect(
image
.subarray(rawOffset(nativeHeaderRva), rawOffset(nativeHeaderRva) + 4)
.toString("latin1"),
).toBe("RTR\0");
});
});