refactor(dos): centralize MZ admission and load-image evidence

This commit is contained in:
morluto committed 2026-10-26 23:02:53 +08:00
1 parent aa6c1d49fd
commit a30ffd27a6
10 files changed
+491 -96

No files matched your search

+43 -39
View File
@@ -57,9 +57,10 @@ import { admitGamecubeDol } from "../domain/gamecubeDolAdmission.js";
import { DOL_HEADER_SIZE } from "../domain/gamecubeDol.js";
import {
mzWindowsHeaderLayoutLimitation,
observeMzWindowsHeader,
parseMzHeaderEvidence,
withMzWindowsHeaderSignature,
type DosMzHeader,
type MzHeaderEvidence,
type MzWindowsHeaderObservation,
} from "../domain/dosMz.js";
import {
@@ -80,6 +81,7 @@ import {
parseCodeViewData,
claimsExecutableHeader,
parseExecutableHeader,
parseMzExecutableMetadata,
parsePeRecordFacts,
peDebugDirectoryEntry,
peCodeViewDataRanges,
@@ -877,7 +879,12 @@ const readExecutableMetadata = async (
offset,
bytes: signature.subarray(0, observedSignature.bytesRead),
};
evidence = parseMzHeaderEvidence(bytes, fileSize, signatureEvidence);
evidence = withMzWindowsHeaderSignature(
bytes,
fileSize,
evidence,
signatureEvidence,
);
}
const { windowsHeader } = evidence;
if (windowsHeader.state === "declared") {
@@ -890,31 +897,28 @@ const readExecutableMetadata = async (
mz_windows_header: { ...windowsHeader },
},
});
return readPeMetadata(
handle,
windowsHeader.offset,
bytes,
return readPeMetadata(handle, windowsHeader.offset, {
mzPrefix: bytes,
fileSize,
mzEvidence: evidence,
signal,
);
});
}
// MZ relocation records may extend past the initial probe; their format
// bounds are at most 65535 records inside a 65535-paragraph header.
const tableEnd =
bytes.length >= 28
? bytes.readUInt16LE(24) + bytes.readUInt16LE(6) * 4
: 0;
const headerBytes = bytes.length >= 28 ? bytes.readUInt16LE(8) * 16 : 0;
if (
tableEnd > bytes.length &&
tableEnd <= headerBytes &&
tableEnd <= fileSize
) {
const table = Buffer.alloc(tableEnd);
const observed = await handle.read(table, 0, table.length, 0);
const { relocationTableRead } = evidence;
if (relocationTableRead !== undefined) {
const table = Buffer.alloc(relocationTableRead.byteLength);
const observed = await handle.read(
table,
0,
table.length,
relocationTableRead.offset,
);
throwIfTargetResolutionCancelled(signal);
const observedBytes = table.subarray(0, observed.bytesRead);
evidence = parseMzHeaderEvidence(
table.subarray(0, observed.bytesRead),
observedBytes,
fileSize,
signatureEvidence,
);
@@ -922,21 +926,11 @@ const readExecutableMetadata = async (
return err(
unresolvedMzHeaderFailure(evidence.windowsHeader, evidence.dos),
);
return parseExecutableHeader(
table.subarray(0, observed.bytesRead),
hostArchitecture,
fileSize,
signatureEvidence,
);
return parseMzExecutableMetadata(observedBytes, evidence);
}
if (windowsHeader.state === "candidate")
return err(unresolvedMzHeaderFailure(windowsHeader, evidence.dos));
return parseExecutableHeader(
bytes,
hostArchitecture,
fileSize,
signatureEvidence,
);
return parseMzExecutableMetadata(bytes, evidence);
}
if (bytes.length >= 4 && bytes.toString("hex", 0, 4) === "7f454c46")
return readElfMetadata(handle, bytes, {
@@ -1264,9 +1258,17 @@ const readFatSliceHeaderEvidence = async (
const readPeMetadata = async (
handle: FileHandle,
offset: number,
mzPrefix: Buffer,
fileSize: number,
signal?: AbortSignal,
{
mzPrefix,
fileSize,
mzEvidence,
signal,
}: {
readonly mzPrefix: Buffer;
readonly fileSize: number;
readonly mzEvidence: MzHeaderEvidence;
readonly signal: AbortSignal | undefined;
},
): Promise<Result<ExecutableMetadata, string>> => {
throwIfTargetResolutionCancelled(signal);
const fileHeader = Buffer.alloc(24);
@@ -1279,10 +1281,12 @@ const readPeMetadata = async (
throwIfTargetResolutionCancelled(signal);
if (fileHeaderRead.bytesRead !== fileHeader.length)
return err("invalid or truncated PE header");
const windowsHeader = observeMzWindowsHeader(mzPrefix, fileSize, {
offset,
bytes: fileHeader.subarray(0, 4),
});
const { windowsHeader } = withMzWindowsHeaderSignature(
mzPrefix,
fileSize,
mzEvidence,
{ offset, bytes: fileHeader.subarray(0, 4) },
);
if (windowsHeader.state !== "declared")
return err("invalid Windows new-header signature in MZ image");
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
+8 -5
View File
@@ -20,6 +20,7 @@ import { readElfFixedHeader } from "./elfFixedHeader.js";
import {
mzWindowsHeaderLayoutLimitation,
parseMzHeaderEvidence,
type MzHeaderEvidence,
type MzWindowsHeaderSignatureEvidence,
} from "./dosMz.js";
import { decodePeMachine, readPeOptionalHeader } from "./peMachine.js";
@@ -126,7 +127,10 @@ export const parseExecutableHeader = (
)
return parseElf(bytes, fileSize);
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a)
return parseMz(bytes, fileSize, mzWindowsHeaderSignature);
return parseMzExecutableMetadata(
bytes,
parseMzHeaderEvidence(bytes, fileSize, mzWindowsHeaderSignature),
);
if (bytes.length < 8) return err("truncated or unsupported binary header");
const magic = bytes.readUInt32BE(0);
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
@@ -434,12 +438,11 @@ const parseElf = (
});
};
const parseMz = (
/** Select executable metadata from the reader's completed MZ observations. */
export const parseMzExecutableMetadata = (
bytes: Buffer,
fileSize: number,
signatureAtOffset?: MzWindowsHeaderSignatureEvidence,
evidence: MzHeaderEvidence,
): Result<ExecutableMetadata, string> => {
const evidence = parseMzHeaderEvidence(bytes, fileSize, signatureAtOffset);
const { windowsHeader } = evidence;
if (
windowsHeader.state === "header-needed" ||
+167
View File
@@ -4,6 +4,10 @@ import { dosMz, pe } from "./binaryTarget.fixture.js";
import { parseExecutableHeader } from "./binaryTarget.js";
import {
observeMzWindowsHeader,
readDosMzDeclarations,
dosMzDeclaredImageByteLength,
parseDeclaredMzHeaderEvidence,
withMzWindowsHeaderSignature,
parseDosMzHeader,
parseMzHeaderEvidence,
} from "./dosMz.js";
@@ -419,3 +423,166 @@ it("retains ordered DOS relocation aliases and their source/module target coordi
error: "DOS MZ relocation 1 points outside its load module",
});
});
const extendedRelocationImage = (): Buffer => {
const bytes = dosMz(8192);
bytes.writeUInt16LE(257, 8); // 4112-byte header.
bytes.writeUInt16LE(2, 6);
bytes.writeUInt16LE(4096, 24);
return bytes;
};
it("retains fixed declarations while requesting only a source-bounded relocation prefix", () => {
const bytes = extendedRelocationImage();
const prefix = bytes.subarray(0, 64);
const evidence = parseMzHeaderEvidence(prefix, bytes.length);
expect(evidence.declarations).toEqual(readDosMzDeclarations(prefix));
expect(evidence.declarations).toMatchObject({
ok: true,
value: {
headerBytes: 4112,
relocationCount: 2,
relocationTableOffset: 4096,
relocationTableEnd: 4104,
entryOffset: 0,
entrySegment: 0,
},
});
expect(evidence.relocationTableRead).toEqual({ offset: 0, byteLength: 4104 });
expect(evidence.dos).toEqual({
ok: false,
error: "truncated DOS MZ relocation table",
});
const observed = parseMzHeaderEvidence(bytes.subarray(0, 4104), bytes.length);
expect(observed.relocationTableRead).toBeUndefined();
expect(observed.dos).toMatchObject({
ok: true,
value: { relocationCount: 2, headerBytes: 4112 },
});
});
it("does not request absent or out-of-bounds relocation records", () => {
const zero = extendedRelocationImage();
zero.writeUInt16LE(512, 8);
zero.writeUInt16LE(0, 6);
zero.writeUInt16LE(6000, 24);
expect(
parseMzHeaderEvidence(zero.subarray(0, 64), zero.length)
.relocationTableRead,
).toBeUndefined();
expect(parseDosMzHeader(zero).ok).toBe(true);
const outsideHeader = extendedRelocationImage();
outsideHeader.writeUInt16LE(5000, 24);
const headerEvidence = parseMzHeaderEvidence(
outsideHeader.subarray(0, 64),
outsideHeader.length,
);
expect(headerEvidence.relocationTableRead).toBeUndefined();
expect(headerEvidence.dos).toEqual({
ok: false,
error: "DOS MZ relocation table lies outside its header",
});
const outsideFile = extendedRelocationImage();
outsideFile.writeUInt16LE(1024, 8);
outsideFile.writeUInt16LE(9000, 24);
expect(
parseMzHeaderEvidence(outsideFile.subarray(0, 64), outsideFile.length)
.relocationTableRead,
).toBeUndefined();
});
it("keeps independent malformed declarations and the established DOS error precedence", () => {
const bytes = extendedRelocationImage();
bytes.writeUInt16LE(0, 4);
bytes.writeUInt16LE(65535, 20);
const evidence = parseMzHeaderEvidence(bytes.subarray(0, 64), bytes.length);
expect(evidence.declarations).toMatchObject({
ok: true,
value: { pages: 0, entryOffset: 65535 },
});
expect(evidence.relocationTableRead?.byteLength).toBe(4104);
expect(evidence.dos).toEqual({
ok: false,
error: "invalid DOS MZ page count or final-page size",
});
expect(parseDosMzHeader(bytes, bytes.length - 1)).toEqual({
ok: false,
error: "invalid DOS MZ file length",
});
bytes.writeUInt16LE(17, 4);
expect(parseDosMzHeader(bytes.subarray(0, 64), bytes.length)).toEqual({
ok: false,
error: "truncated DOS MZ relocation table",
});
expect(parseDosMzHeader(bytes)).toEqual({
ok: false,
error: "DOS MZ entry point lies outside its initialized load module",
});
});
it("keeps declared Windows signatures independent of incomplete DOS relocations", () => {
const bytes = extendedRelocationImage();
bytes.writeUInt32LE(128, 60);
const evidence = parseMzHeaderEvidence(bytes.subarray(0, 64), bytes.length, {
offset: 128,
bytes: Buffer.from("PE\0\0"),
});
expect(evidence.windowsHeader).toEqual({
state: "declared",
signature: "PE",
offset: 128,
});
expect(evidence.dos).toEqual({
ok: false,
error: "truncated DOS MZ relocation table",
});
expect(evidence.relocationTableRead?.byteLength).toBe(4104);
});
it("completes captured evidence from the original admitted fixed declarations", () => {
const bytes = extendedRelocationImage();
const declarations = readDosMzDeclarations(bytes.subarray(0, 64));
if (!declarations.ok) throw new Error(declarations.error);
expect(dosMzDeclaredImageByteLength(declarations.value)).toEqual({
ok: true,
value: bytes.length,
});
const evidence = parseDeclaredMzHeaderEvidence(
bytes.subarray(0, 4104),
bytes.length,
declarations,
);
expect(evidence.declarations).toBe(declarations);
expect(evidence).toEqual(
parseMzHeaderEvidence(bytes.subarray(0, 4104), bytes.length),
);
expect(evidence.dos.ok).toBe(true);
});
it("adds distant Windows signatures while retaining prior DOS observations", () => {
const bytes = extendedRelocationImage();
bytes.writeUInt32LE(6000, 60);
const prefix = bytes.subarray(0, 4104);
const evidence = parseMzHeaderEvidence(prefix, bytes.length);
expect(evidence.windowsHeader).toEqual({
state: "signature-needed",
offset: 6000,
});
const signature = { offset: 6000, bytes: Buffer.from("PE\0\0") };
const completed = withMzWindowsHeaderSignature(
prefix,
bytes.length,
evidence,
signature,
);
expect(completed.dos).toBe(evidence.dos);
expect(completed.declarations).toBe(evidence.declarations);
expect(completed.windowsHeader).toEqual({
state: "declared",
signature: "PE",
offset: 6000,
});
expect(completed).toEqual(
parseMzHeaderEvidence(prefix, bytes.length, signature),
);
});
+134 -21
View File
@@ -1,11 +1,45 @@
import { err, ok, type Result } from "./result.js";
/** The unsigned 16-bit page count bounds the initialized image, not overlays. */
export const dosMzImageByteLength = (bytes: Buffer): Result<number, string> => {
if (bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ")
return err("invalid or truncated DOS MZ header");
const lastPageBytes = bytes.readUInt16LE(2);
const pages = bytes.readUInt16LE(4);
/** Source declarations in the fixed DOS header, independent of load-module validity. */
export interface DosMzDeclarations {
readonly lastPageBytes: number;
readonly pages: number;
readonly headerBytes: number;
readonly relocationCount: number;
readonly relocationTableOffset: number;
readonly relocationTableEnd: number;
readonly entrySegment: number;
readonly entryOffset: number;
}
const fixedDosMzDeclarations = (bytes: Buffer): DosMzDeclarations => {
const relocationCount = bytes.readUInt16LE(6);
const relocationTableOffset = bytes.readUInt16LE(24);
return {
lastPageBytes: bytes.readUInt16LE(2),
pages: bytes.readUInt16LE(4),
headerBytes: bytes.readUInt16LE(8) * 16,
relocationCount,
relocationTableOffset,
relocationTableEnd: relocationTableOffset + relocationCount * 4,
entrySegment: bytes.readUInt16LE(22),
entryOffset: bytes.readUInt16LE(20),
};
};
/** Admit only the fixed source header; malformed page and load declarations remain observable. */
export const readDosMzDeclarations = (
bytes: Buffer,
): Result<DosMzDeclarations, string> =>
bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ"
? err("invalid or truncated DOS MZ header")
: ok(fixedDosMzDeclarations(bytes));
/** Derive the initialized source extent from already admitted fixed declarations. */
export const dosMzDeclaredImageByteLength = ({
pages,
lastPageBytes,
}: DosMzDeclarations): Result<number, string> => {
if (pages === 0 || lastPageBytes > 511)
return err("invalid DOS MZ page count or final-page size");
return ok((pages - 1) * 512 + (lastPageBytes === 0 ? 512 : lastPageBytes));
@@ -75,13 +109,23 @@ export const observeMzWindowsHeader = (
bytes: Buffer,
fileSize = bytes.length,
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
): MzWindowsHeaderObservation =>
observeWindowsHeader(bytes, fileSize, signatureEvidence);
const observeWindowsHeader = (
bytes: Buffer,
fileSize: number,
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
declarations?: DosMzDeclarations,
): MzWindowsHeaderObservation => {
if (bytes.length < 64)
return fileSize >= 64 ? { state: "header-needed" } : { state: "absent" };
const headerBytes = bytes.readUInt16LE(8) * 16;
const relocationCount = bytes.readUInt16LE(6);
const tableStart = bytes.readUInt16LE(24);
const tableEnd = tableStart + relocationCount * 4;
const {
headerBytes,
relocationCount,
relocationTableOffset: tableStart,
relocationTableEnd: tableEnd,
} = declarations ?? fixedDosMzDeclarations(bytes);
if (
headerBytes !== 0 &&
relocationCount > 0 &&
@@ -159,22 +203,32 @@ const windowsHeaderSignature = (
export const parseDosMzHeader = (
bytes: Buffer,
fileSize = bytes.length,
): Result<DosMzHeader, string> =>
parseDeclaredDosMzHeader(bytes, fileSize, readDosMzDeclarations(bytes));
const parseDeclaredDosMzHeader = (
bytes: Buffer,
fileSize: number,
declarations: Result<DosMzDeclarations, string>,
): Result<DosMzHeader, string> => {
if (bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ")
return err("invalid or truncated DOS MZ header");
if (!declarations.ok) return declarations;
if (!Number.isSafeInteger(fileSize) || fileSize < bytes.length)
return err("invalid DOS MZ file length");
// DOS structure is parsed independently from the meaning of bytes at 0x3c.
const extent = dosMzImageByteLength(bytes);
const extent = dosMzDeclaredImageByteLength(declarations.value);
if (!extent.ok) return extent;
const imageBytes = extent.value;
const headerBytes = bytes.readUInt16LE(8) * 16;
const {
headerBytes,
relocationCount,
relocationTableOffset,
relocationTableEnd: tableEnd,
entryOffset,
entrySegment,
} = declarations.value;
if (headerBytes < 28 || headerBytes >= imageBytes)
return err("invalid DOS MZ header size or empty load module");
if (imageBytes > fileSize) return err("truncated DOS MZ load module");
const relocationCount = bytes.readUInt16LE(6);
const relocationTableOffset = bytes.readUInt16LE(24);
const tableEnd = relocationTableOffset + relocationCount * 4;
if (
relocationCount > 0 &&
(relocationTableOffset < 28 || tableEnd > headerBytes)
@@ -191,8 +245,6 @@ export const parseDosMzHeader = (
moduleBytes,
);
if (!relocations.ok) return relocations;
const entryOffset = bytes.readUInt16LE(20);
const entrySegment = bytes.readUInt16LE(22);
if (entrySegment * 16 + entryOffset >= moduleBytes)
return err("DOS MZ entry point lies outside its initialized load module");
return ok({
@@ -210,6 +262,12 @@ export const parseDosMzHeader = (
/** Preserve DOS structural facts and Windows-pointer evidence independently. */
export interface MzHeaderEvidence {
readonly declarations: Result<DosMzDeclarations, string>;
/** A source-bounded prefix read, independent of later DOS module validity. */
readonly relocationTableRead?: {
readonly offset: 0;
readonly byteLength: number;
};
readonly dos: Result<DosMzHeader, string>;
readonly windowsHeader: MzWindowsHeaderObservation;
}
@@ -218,9 +276,64 @@ export const parseMzHeaderEvidence = (
bytes: Buffer,
fileSize = bytes.length,
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
): MzHeaderEvidence =>
parseDeclaredMzHeaderEvidence(
bytes,
fileSize,
readDosMzDeclarations(bytes),
signatureEvidence,
);
/** Complete evidence using fixed declarations from the same captured source. */
export const parseDeclaredMzHeaderEvidence = (
bytes: Buffer,
fileSize: number,
declarations: Result<DosMzDeclarations, string>,
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
): MzHeaderEvidence => {
const fields = declarations.ok ? declarations.value : undefined;
const needed =
fields !== undefined &&
Number.isSafeInteger(fileSize) &&
fileSize >= bytes.length &&
fields.relocationCount > 0 &&
fields.relocationTableEnd > bytes.length &&
fields.relocationTableEnd <= fields.headerBytes &&
fields.relocationTableEnd <= fileSize;
return {
declarations,
...(needed
? {
relocationTableRead: {
offset: 0 as const,
byteLength: fields.relocationTableEnd,
},
}
: {}),
dos: parseDeclaredDosMzHeader(bytes, fileSize, declarations),
windowsHeader: observeWindowsHeader(
bytes,
fileSize,
signatureEvidence,
fields,
),
};
};
/** Add a signature observation without reinterpreting the captured DOS structure. */
export const withMzWindowsHeaderSignature = (
bytes: Buffer,
fileSize: number,
evidence: MzHeaderEvidence,
signature: MzWindowsHeaderSignatureEvidence,
): MzHeaderEvidence => ({
dos: parseDosMzHeader(bytes, fileSize),
windowsHeader: observeMzWindowsHeader(bytes, fileSize, signatureEvidence),
...evidence,
windowsHeader: observeWindowsHeader(
bytes,
fileSize,
signature,
evidence.declarations.ok ? evidence.declarations.value : undefined,
),
});
const readRelocations = (
+30
View File
@@ -189,3 +189,33 @@ it("preserves exact cancellation after an overlay descriptor read", async () =>
await handle.close();
}
});
it.each([
[0, "invalid DOS MZ page count or final-page size"],
[4, "truncated DOS MZ load module"],
] as const)(
"preserves early extent failure before Windows inspection (pages=%s)",
async (pages, error) => {
const fixture = fixtureDosLoadImage(Buffer.alloc(1024));
fixture.bytes.writeUInt16LE(pages, 4);
fixture.bytes.writeUInt32LE(512, 60);
fixture.bytes.write("NE", 512, "ascii");
const directory = await createTestTempDirectory("rea-mz-extent-");
const path = join(directory, "inert.exe");
await writeFile(path, fixture.bytes);
const handle = await open(path, "r");
try {
await expect(
attestGhidraDosSnapshot(
handle,
fixture.bytes.length,
path,
fixture.sha256,
fixture.observation,
),
).resolves.toEqual({ ok: false, error });
} finally {
await handle.close();
}
},
);
+17 -5
View File
@@ -3,8 +3,10 @@ import type { FileHandle } from "node:fs/promises";
import { readArtifactFileChunks } from "../artifacts/readArtifactFileChunks.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
dosMzImageByteLength,
observeMzWindowsHeader,
dosMzDeclaredImageByteLength,
readDosMzDeclarations,
parseDeclaredMzHeaderEvidence,
withMzWindowsHeaderSignature,
} from "../domain/dosMz.js";
import { err } from "../domain/result.js";
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
@@ -37,7 +39,9 @@ export const attestGhidraDosSnapshot = async (
});
if (header === undefined || header.length !== headerBytes)
throw new RegularFileChangedError(path);
const extent = dosMzImageByteLength(header);
const declarations = readDosMzDeclarations(header);
if (!declarations.ok) return declarations;
const extent = dosMzDeclaredImageByteLength(declarations.value);
if (!extent.ok) return extent;
if (extent.value > fileBytes) return err("truncated DOS MZ load module");
// Retain the complete initialized image and the Windows-pointer header only.
@@ -57,7 +61,8 @@ export const attestGhidraDosSnapshot = async (
}
}
if (captured !== captureBytes) throw new RegularFileChangedError(path);
const windowsHeader = observeMzWindowsHeader(prefix, fileBytes);
let evidence = parseDeclaredMzHeaderEvidence(prefix, fileBytes, declarations);
const { windowsHeader } = evidence;
let signature:
| { readonly offset: number; readonly bytes: Buffer }
| undefined;
@@ -81,7 +86,14 @@ export const attestGhidraDosSnapshot = async (
bytes: signatureBytes,
};
}
const prepared = prepareGhidraDosLoadImage(prefix, fileBytes, signature);
if (signature !== undefined)
evidence = withMzWindowsHeaderSignature(
prefix,
fileBytes,
evidence,
signature,
);
const prepared = prepareGhidraDosLoadImage(prefix, evidence);
if (!prepared.ok) return prepared;
const originalHash = createHash("sha256").update(prefix);
const modifiedHash = createHash("sha256")
+2 -1
View File
@@ -1,3 +1,4 @@
import { parseMzHeaderEvidence } from "../domain/dosMz.js";
import { createHash } from "node:crypto";
import { dosMz } from "../domain/binaryTarget.fixture.js";
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
@@ -21,7 +22,7 @@ export const compareDosFixture = (
return err(
"Ghidra load-image snapshot digest does not match the admitted target",
);
const image = prepareGhidraDosLoadImage(bytes, bytes.length);
const image = prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes));
if (!image.ok) return image;
return attestGhidraDosLoadImage(
{
+27 -16
View File
@@ -1,3 +1,4 @@
import { parseMzHeaderEvidence } from "../domain/dosMz.js";
import { describe, expect, it } from "vitest";
import { createHash } from "node:crypto";
import {
@@ -22,7 +23,9 @@ it("accepts DOS header padding after a complete relocation table", () => {
bytes.writeUInt16LE(4, 8);
bytes.writeUInt16LE(28, 24);
bytes.write("ABCD", 60, "ascii");
expect(prepareGhidraDosLoadImage(bytes, bytes.length)).toMatchObject({
expect(
prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes)),
).toMatchObject({
ok: true,
value: {
header: {
@@ -38,20 +41,25 @@ it("accepts DOS header padding after a complete relocation table", () => {
});
});
it("does not interpret an unresolved Windows-header candidate as a DOS image", () => {
const bytes = Buffer.alloc(128);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt16LE(128, 2);
bytes.writeUInt16LE(1, 4);
bytes.writeUInt16LE(0, 6);
bytes.writeUInt16LE(4, 8);
bytes.writeUInt16LE(28, 24);
bytes.write("ABCD", 60, "ascii");
expect(prepareGhidraDosLoadImage(bytes, bytes.length)).toEqual({
ok: false,
error: "MZ Windows new-header candidate is unresolved",
});
});
it.each([0, 1])(
"prioritizes an unresolved Windows-header candidate over DOS page validity (pages=%s)",
(pages) => {
const bytes = Buffer.alloc(128);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt16LE(128, 2);
bytes.writeUInt16LE(pages, 4);
bytes.writeUInt16LE(0, 6);
bytes.writeUInt16LE(4, 8);
bytes.writeUInt16LE(28, 24);
bytes.write("ABCD", 60, "ascii");
expect(
prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes)),
).toEqual({
ok: false,
error: "MZ Windows new-header candidate is unresolved",
});
},
);
const damage: ReadonlyArray<
readonly [string, (observation: NativeLoadImageObservation) => void]
@@ -505,7 +513,10 @@ it("applies duplicate aliases and overlapping DOS fixups in declared order with
bytes.writeUInt16LE(0, 42);
bytes.set([1, 0, 2], 80);
const before = Buffer.from(bytes);
const prepared = prepareGhidraDosLoadImage(bytes, bytes.length);
const prepared = prepareGhidraDosLoadImage(
bytes,
parseMzHeaderEvidence(bytes),
);
if (!prepared.ok) throw new Error(prepared.error);
expect(bytes.equals(before)).toBe(true);
expect(
+2 -8
View File
@@ -5,11 +5,7 @@ import {
type NativeLoadImageCheck,
type NativeLoadImageObservation,
} from "../domain/native/nativeLoadImage.js";
import {
parseMzHeaderEvidence,
type DosMzHeader,
type MzWindowsHeaderSignatureEvidence,
} from "../domain/dosMz.js";
import type { DosMzHeader, MzHeaderEvidence } from "../domain/dosMz.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { validateDosComLength } from "../domain/dosCom.js";
@@ -58,10 +54,8 @@ export type GhidraDosImage = {
/** Prepare initialized bytes and relocation facts without copying overlays. */
export const prepareGhidraDosLoadImage = (
bytes: Buffer,
fileBytes: number,
signatureAtOffset?: MzWindowsHeaderSignatureEvidence,
evidence: MzHeaderEvidence,
): Result<GhidraDosImage, string> => {
const evidence = parseMzHeaderEvidence(bytes, fileBytes, signatureAtOffset);
if (evidence.windowsHeader.state === "candidate")
return err("MZ Windows new-header candidate is unresolved");
if (evidence.windowsHeader.state === "header-needed")
+61 -1
View File
@@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import {
open,
type FileHandle,
chmod,
mkdir,
readFile,
@@ -12,7 +13,7 @@ import { join } from "node:path";
import { promisify } from "node:util";
import { buildBinary } from "plist";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import {
@@ -1418,3 +1419,62 @@ describe("binary target I/O: PE CodeView header ranges", () => {
},
);
});
it("does not read a relocation table offset when DOS declares zero records", async () => {
const directory = await createTestTempDirectory("rea-empty-dos-relocations-");
const path = join(directory, "legacy.exe");
const bytes = dosMz(8192);
bytes.writeUInt16LE(257, 8);
bytes.writeUInt16LE(4104, 24);
bytes.writeUInt32LE(0, 60);
await writeFile(path, bytes);
const probe = await open(path, "r");
const prototype = Object.getPrototypeOf(probe) as FileHandle;
await probe.close();
const reads = vi.spyOn(prototype, "read");
try {
expect(await parseBinaryTarget(path)).toMatchObject({
ok: true,
value: { format: "dos-mz", architecture: "x86" },
});
const lengths = reads.mock.calls.map(
(args) => (args as readonly unknown[])[2],
);
expect(lengths).toContain(4096);
expect(lengths).not.toContain(4104);
} finally {
reads.mockRestore();
}
});
it("recognizes Windows metadata independently of invalid DOS load declarations", async () => {
const directory = await createTestTempDirectory("rea-invalid-dos-valid-pe-");
const path = join(directory, "windows.exe");
const bytes = pe(0x8664, 8192);
bytes.writeUInt16LE(0, 4);
bytes.writeUInt16LE(1010, 6);
bytes.writeUInt16LE(257, 8);
bytes.writeUInt16LE(64, 24);
await writeFile(path, bytes);
expect(await parseBinaryTarget(path)).toMatchObject({
ok: true,
value: { format: "pe", architecture: "x86_64" },
});
});
it("refuses a declared DOS relocation header that is not present in the file", async () => {
const directory = await createTestTempDirectory(
"rea-truncated-dos-relocations-",
);
const path = join(directory, "legacy.exe");
const bytes = dosMz(8192);
bytes.writeUInt16LE(1010, 6);
bytes.writeUInt16LE(257, 8);
bytes.writeUInt16LE(64, 24);
bytes.writeUInt32LE(0, 60);
await writeFile(path, bytes.subarray(0, 4096));
expect(await parseBinaryTarget(path)).toMatchObject({
ok: false,
error: { reason: "truncated DOS MZ load module" },
});
});