mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
refactor(targets): preserve ambiguous MZ header evidence
This commit is contained in:
1 parent
06828b2e65
commit
39d1b2d395
12 files changed
+308
-59
No files matched your search
+9
-5
@@ -25,11 +25,15 @@ same admitted target and provider semantics.
|
||||
|
||||
The classifier validates MZ page counts, initialized module length, header and
|
||||
relocation-table bounds, relocation destinations, and the initialized entry
|
||||
point. It distinguishes DOS load-module and relocation bytes from the Windows
|
||||
new-header field. PE remains PE; NE, LE, and LX declarations are explicitly
|
||||
unsupported. A damaged declared Windows header does not silently become DOS.
|
||||
Inventory classification returns `unknown` when its bounded prefix cannot
|
||||
establish the format; this is distinct from the complete target admission check.
|
||||
point. DOS structural facts are parsed independently from Windows-header
|
||||
evidence. A recognized PE signature remains PE; recognized NE, LE, and LX
|
||||
headers are explicitly unsupported. A nonzero Windows-header pointer without a
|
||||
recognized signature remains an unresolved candidate, even when the DOS
|
||||
structure validates: automatic target selection fails closed and reports the
|
||||
DOS facts alongside the candidate. REA does not infer DOS intent from padding
|
||||
or infer a damaged Windows header from the pointer alone. Inventory
|
||||
classification returns `unknown` for that candidate and when its bounded prefix
|
||||
cannot establish the format; this is distinct from complete DOS validation.
|
||||
|
||||
Ghidra imports a private target snapshot with `MzLoader`, language
|
||||
`x86:LE:16:Real Mode`, and compiler specification `default`. These settings,
|
||||
|
||||
@@ -32,7 +32,12 @@ import {
|
||||
rawBinarySelectionError,
|
||||
} from "../domain/rawBinary.js";
|
||||
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
import { observeMzWindowsHeader } from "../domain/dosMz.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
parseMzHeaderEvidence,
|
||||
type DosMzHeader,
|
||||
type MzWindowsHeaderObservation,
|
||||
} from "../domain/dosMz.js";
|
||||
import {
|
||||
createXmlPlistSignatureProbe,
|
||||
hasBinaryPlistSignature,
|
||||
@@ -63,6 +68,7 @@ import {
|
||||
} from "../domain/apple/machoContainer.js";
|
||||
import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
const XML_PLIST_PROBE_CHUNK_BYTES = 64 * 1024;
|
||||
|
||||
@@ -242,9 +248,18 @@ export const parseBinaryTarget = async (
|
||||
kind: "archive",
|
||||
format: zipRootPackageFormat(path),
|
||||
});
|
||||
const reason =
|
||||
typeof detected.error === "string"
|
||||
? detected.error
|
||||
: detected.error.reason;
|
||||
const diagnostics =
|
||||
typeof detected.error === "string"
|
||||
? undefined
|
||||
: detected.error.diagnostics;
|
||||
return err(
|
||||
new BinaryTargetError(path, detected.error, {
|
||||
new BinaryTargetError(path, reason, {
|
||||
constraint: "file_format_rejected",
|
||||
...(diagnostics === undefined ? {} : { diagnostics }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -547,7 +562,7 @@ const readExecutableMetadata = async (
|
||||
handle: FileHandle,
|
||||
hostArchitecture: NodeJS.Architecture,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<ExecutableMetadata, string>> => {
|
||||
): Promise<Result<ExecutableMetadata, ExecutableMetadataFailure>> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const prefix = Buffer.alloc(4096);
|
||||
const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
|
||||
@@ -558,10 +573,10 @@ const readExecutableMetadata = async (
|
||||
const fileSize = (await handle.stat()).size;
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
|
||||
let windowsHeader = observeMzWindowsHeader(bytes, fileSize);
|
||||
let evidence = parseMzHeaderEvidence(bytes, fileSize);
|
||||
let signatureBytes: Buffer | undefined;
|
||||
if (windowsHeader.state === "signature-needed") {
|
||||
const { offset } = windowsHeader;
|
||||
if (evidence.windowsHeader.state === "signature-needed") {
|
||||
const { offset } = evidence.windowsHeader;
|
||||
const signature = Buffer.alloc(Math.min(4, fileSize - offset));
|
||||
const observedSignature = await handle.read(
|
||||
signature,
|
||||
@@ -571,8 +586,9 @@ const readExecutableMetadata = async (
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
signatureBytes = signature.subarray(0, observedSignature.bytesRead);
|
||||
windowsHeader = observeMzWindowsHeader(bytes, fileSize, signatureBytes);
|
||||
evidence = parseMzHeaderEvidence(bytes, fileSize, signatureBytes);
|
||||
}
|
||||
const { windowsHeader } = evidence;
|
||||
if (windowsHeader.state === "declared") {
|
||||
if (windowsHeader.offset < 64)
|
||||
return err("invalid Windows new-header offset in MZ image");
|
||||
@@ -599,12 +615,23 @@ const readExecutableMetadata = async (
|
||||
const table = Buffer.alloc(tableEnd);
|
||||
const observed = await handle.read(table, 0, table.length, 0);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
evidence = parseMzHeaderEvidence(
|
||||
table.subarray(0, observed.bytesRead),
|
||||
fileSize,
|
||||
signatureBytes,
|
||||
);
|
||||
if (evidence.windowsHeader.state === "candidate")
|
||||
return err(
|
||||
unresolvedMzHeaderFailure(evidence.windowsHeader, evidence.dos),
|
||||
);
|
||||
return parseExecutableHeader(
|
||||
table.subarray(0, observed.bytesRead),
|
||||
hostArchitecture,
|
||||
fileSize,
|
||||
);
|
||||
}
|
||||
if (windowsHeader.state === "candidate")
|
||||
return err(unresolvedMzHeaderFailure(windowsHeader, evidence.dos));
|
||||
return parseExecutableHeader(
|
||||
bytes,
|
||||
hostArchitecture,
|
||||
@@ -653,6 +680,42 @@ const readExecutableMetadata = async (
|
||||
});
|
||||
};
|
||||
|
||||
type ExecutableMetadataFailure =
|
||||
| string
|
||||
| {
|
||||
readonly reason: string;
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
};
|
||||
|
||||
const unresolvedMzHeaderFailure = (
|
||||
windowsHeader: Extract<MzWindowsHeaderObservation, { state: "candidate" }>,
|
||||
dos: Result<DosMzHeader, string>,
|
||||
): Exclude<ExecutableMetadataFailure, string> => {
|
||||
return {
|
||||
reason: "MZ Windows new-header candidate is unresolved",
|
||||
diagnostics: {
|
||||
mz_windows_header: {
|
||||
state: windowsHeader.state,
|
||||
offset: windowsHeader.offset,
|
||||
signature: windowsHeader.signature,
|
||||
},
|
||||
mz_dos: dos.ok
|
||||
? {
|
||||
state: "valid",
|
||||
header_bytes: dos.value.headerBytes,
|
||||
image_bytes: dos.value.imageBytes,
|
||||
module_bytes: dos.value.moduleBytes,
|
||||
overlay_bytes: dos.value.overlayBytes,
|
||||
relocation_count: dos.value.relocationCount,
|
||||
relocation_table_offset: dos.value.relocationTableOffset,
|
||||
entry_segment: dos.value.entrySegment,
|
||||
entry_offset: dos.value.entryOffset,
|
||||
}
|
||||
: { state: "invalid", reason: dos.error },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Read the validated FAT table and bounded thin-header identity prefixes. */
|
||||
const readFatSliceHeaderEvidence = async (
|
||||
handle: FileHandle,
|
||||
|
||||
@@ -19,6 +19,21 @@ import type { ArtifactEntry } from "../ArtifactReader.js";
|
||||
|
||||
import { classifyArtifactBytesAtPath } from "../../../tests/fixtures/artifactClassification.js";
|
||||
|
||||
it("keeps an ambiguous DOS MZ inventory candidate unknown", () => {
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(2, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
expect(classifyArtifactBytesAtPath("legacy.exe", bytes)).toEqual({
|
||||
kind: "unknown",
|
||||
format: "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
const entry = (path: string, kind: ArtifactEntry["kind"]): ArtifactEntry => ({
|
||||
path,
|
||||
kind,
|
||||
|
||||
@@ -27,10 +27,7 @@ import {
|
||||
zipPackageFormatForPath,
|
||||
type ZipPackageFormat,
|
||||
} from "../../domain/zipPackageFormat.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
parseDosMzHeader,
|
||||
} from "../../domain/dosMz.js";
|
||||
import { parseMzHeaderEvidence } from "../../domain/dosMz.js";
|
||||
import {
|
||||
classifyFatContainerIdentity,
|
||||
readFatContainerHeader,
|
||||
@@ -585,8 +582,10 @@ export const classifyArtifactBytes = (
|
||||
prefix.length < Math.min(fileSize, 64)
|
||||
)
|
||||
return "unknown";
|
||||
const windowsHeader = observeMzWindowsHeader(prefix, fileSize);
|
||||
const evidence = parseMzHeaderEvidence(prefix, fileSize);
|
||||
const { windowsHeader } = evidence;
|
||||
if (windowsHeader.state === "signature-needed") return "unknown";
|
||||
if (windowsHeader.state === "candidate") return "unknown";
|
||||
if (windowsHeader.state === "declared") {
|
||||
const windowsOffset = windowsHeader.offset;
|
||||
if (
|
||||
@@ -597,7 +596,7 @@ export const classifyArtifactBytes = (
|
||||
return "pe";
|
||||
return "unknown";
|
||||
}
|
||||
if (parseDosMzHeader(prefix, fileSize).ok) return "dos-mz";
|
||||
if (evidence.dos.ok) return "dos-mz";
|
||||
return "unknown";
|
||||
}
|
||||
return "file";
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
} from "./binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { readElfFixedHeader } from "./elfFixedHeader.js";
|
||||
import { observeMzWindowsHeader, parseDosMzHeader } from "./dosMz.js";
|
||||
import { parseMzHeaderEvidence } from "./dosMz.js";
|
||||
import {
|
||||
decodePeMachine,
|
||||
peOptionalHeaderMagic,
|
||||
@@ -438,11 +438,8 @@ const parseMz = (
|
||||
fileSize: number,
|
||||
signatureAtOffset?: Buffer,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
const windowsHeader = observeMzWindowsHeader(
|
||||
bytes,
|
||||
fileSize,
|
||||
signatureAtOffset,
|
||||
);
|
||||
const evidence = parseMzHeaderEvidence(bytes, fileSize, signatureAtOffset);
|
||||
const { windowsHeader } = evidence;
|
||||
if (
|
||||
windowsHeader.state === "header-needed" ||
|
||||
windowsHeader.state === "signature-needed"
|
||||
@@ -450,6 +447,8 @@ const parseMz = (
|
||||
return err(
|
||||
"Windows new-header pointer or signature is outside the bounded prefix",
|
||||
);
|
||||
if (windowsHeader.state === "candidate")
|
||||
return err("MZ Windows new-header candidate is unresolved");
|
||||
if (windowsHeader.state === "declared") {
|
||||
const { offset } = windowsHeader;
|
||||
if (offset < 64 || offset > bytes.length - 24)
|
||||
@@ -463,7 +462,7 @@ const parseMz = (
|
||||
const codeView = parsePeCodeViewFromBuffer(bytes, offset);
|
||||
return parsePeRecord(bytes.subarray(offset), codeView);
|
||||
}
|
||||
const parsed = parseDosMzHeader(bytes, fileSize, signatureAtOffset);
|
||||
const parsed = evidence.dos;
|
||||
return parsed.ok
|
||||
? ok({
|
||||
format: "dos-mz",
|
||||
|
||||
@@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { dosMz, pe } from "./binaryTarget.fixture.js";
|
||||
import { parseExecutableHeader } from "./binaryTarget.js";
|
||||
import { observeMzWindowsHeader, parseDosMzHeader } from "./dosMz.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
parseDosMzHeader,
|
||||
parseMzHeaderEvidence,
|
||||
} from "./dosMz.js";
|
||||
|
||||
describe("DOS MZ load-module validation", () => {
|
||||
it("separates initialized module and appended overlay bytes", () => {
|
||||
@@ -268,16 +272,45 @@ describe("MZ Windows header observations", () => {
|
||||
).toEqual({ state: "declared", offset, signature: "PE" });
|
||||
});
|
||||
|
||||
it("does not fall back to DOS when a Windows declaration is damaged", () => {
|
||||
const bytes = Buffer.alloc(256);
|
||||
dosMz().copy(bytes, 0, 0, 28);
|
||||
bytes.writeUInt16LE(256, 2);
|
||||
it("preserves DOS facts while leaving an unsigned Windows pointer unresolved", () => {
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(2, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(64, 24);
|
||||
bytes.writeUInt32LE(192, 60);
|
||||
expect(parseDosMzHeader(bytes).ok).toBe(false);
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({ ok: false });
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.writeUInt32LE(0, 28);
|
||||
bytes.writeUInt32LE(0, 32);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
|
||||
const evidence = parseMzHeaderEvidence(bytes);
|
||||
expect(evidence.dos).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
headerBytes: 64,
|
||||
imageBytes: 128,
|
||||
moduleBytes: 64,
|
||||
relocationCount: 2,
|
||||
relocationTableOffset: 28,
|
||||
},
|
||||
});
|
||||
expect(evidence.windowsHeader).toEqual({
|
||||
state: "candidate",
|
||||
offset: 0x44434241,
|
||||
signature: "unobserved",
|
||||
});
|
||||
expect(parseExecutableHeader(bytes, "x64")).toEqual({
|
||||
ok: false,
|
||||
error: "MZ Windows new-header candidate is unresolved",
|
||||
});
|
||||
|
||||
bytes.writeUInt32LE(0xfffffff0, 60);
|
||||
expect(parseMzHeaderEvidence(bytes).windowsHeader).toEqual({
|
||||
state: "candidate",
|
||||
offset: 0xfffffff0,
|
||||
signature: "unobserved",
|
||||
});
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({ ok: false });
|
||||
});
|
||||
});
|
||||
+41
-17
@@ -27,16 +27,24 @@ export type MzWindowsHeaderObservation =
|
||||
| { readonly state: "absent" }
|
||||
| { readonly state: "header-needed" }
|
||||
| { readonly state: "signature-needed"; readonly offset: number }
|
||||
| {
|
||||
/** A nonzero pointer exists, but no Windows signature confirms it. */
|
||||
readonly state: "candidate";
|
||||
readonly offset: number;
|
||||
readonly signature: "other" | "unobserved";
|
||||
}
|
||||
| {
|
||||
readonly state: "declared";
|
||||
readonly offset: number;
|
||||
readonly signature: "PE" | "NE" | "LE" | "LX" | "other" | "unobserved";
|
||||
readonly signature: "PE" | "NE" | "LE" | "LX";
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe the Windows new-header pointer without mistaking short DOS module
|
||||
* bytes for e_lfanew. Complete DOS headers declare their pointer; for shorter
|
||||
* headers, only a PE/NE/LE/LX signature at that pointer confirms the candidate.
|
||||
* bytes for e_lfanew. A nonzero pointer in a complete DOS header is only a
|
||||
* candidate until a PE/NE/LE/LX signature is observed. For shorter DOS headers,
|
||||
* only a recognized signature confirms a candidate because the field may be
|
||||
* module data.
|
||||
* A bounded prefix that does not reach the candidate requests one bounded read
|
||||
* from its owner instead of treating unexamined bytes as absent.
|
||||
*/
|
||||
@@ -65,10 +73,22 @@ export const observeMzWindowsHeader = (
|
||||
: undefined;
|
||||
const available = signatureAtOffset ?? inlineSignature;
|
||||
const signature = windowsHeaderSignature(available);
|
||||
if (headerBytes === 0 || headerBytes >= 64)
|
||||
return { state: "declared", offset, signature };
|
||||
if (signature !== "other" && signature !== "unobserved")
|
||||
return { state: "declared", offset, signature };
|
||||
if (headerBytes === 0 || headerBytes >= 64) {
|
||||
const remaining = fileSize - offset;
|
||||
const needsMore =
|
||||
(available === undefined && remaining >= 2) ||
|
||||
(available !== undefined && available.length < 2 && remaining >= 2) ||
|
||||
(available !== undefined &&
|
||||
available.length >= 2 &&
|
||||
available.toString("latin1", 0, 2) === "PE" &&
|
||||
available.length < 4 &&
|
||||
remaining >= 4);
|
||||
if (needsMore && signatureAtOffset === undefined)
|
||||
return { state: "signature-needed", offset };
|
||||
return { state: "candidate", offset, signature };
|
||||
}
|
||||
if (signature === "other") return { state: "absent" };
|
||||
const observedPePrefix =
|
||||
available !== undefined &&
|
||||
@@ -101,21 +121,12 @@ const windowsHeaderSignature = (
|
||||
export const parseDosMzHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureAtOffset?: Buffer,
|
||||
): Result<DosMzHeader, string> => {
|
||||
if (bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ")
|
||||
return err("invalid or truncated DOS MZ header");
|
||||
if (!Number.isSafeInteger(fileSize) || fileSize < bytes.length)
|
||||
return err("invalid DOS MZ file length");
|
||||
const windowsHeader = observeMzWindowsHeader(
|
||||
bytes,
|
||||
fileSize,
|
||||
signatureAtOffset,
|
||||
);
|
||||
if (windowsHeader.state === "declared")
|
||||
return err("MZ declares a Windows new header; it is not a DOS-only image");
|
||||
if (windowsHeader.state === "signature-needed")
|
||||
return err("MZ Windows new-header signature is outside the bounded prefix");
|
||||
// DOS structure is parsed independently from the meaning of bytes at 0x3c.
|
||||
const extent = dosMzImageByteLength(bytes);
|
||||
if (!extent.ok) return extent;
|
||||
const imageBytes = extent.value;
|
||||
@@ -133,8 +144,6 @@ export const parseDosMzHeader = (
|
||||
return err("DOS MZ relocation table lies outside its header");
|
||||
if (relocationCount > 0 && tableEnd > bytes.length)
|
||||
return err("truncated DOS MZ relocation table");
|
||||
if (windowsHeader.state === "header-needed")
|
||||
return err("MZ Windows new-header pointer is outside the bounded prefix");
|
||||
const moduleBytes = imageBytes - headerBytes;
|
||||
const relocations = validateRelocations(
|
||||
bytes,
|
||||
@@ -159,6 +168,21 @@ export const parseDosMzHeader = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Preserve DOS structural facts and Windows-pointer evidence independently. */
|
||||
export interface MzHeaderEvidence {
|
||||
readonly dos: Result<DosMzHeader, string>;
|
||||
readonly windowsHeader: MzWindowsHeaderObservation;
|
||||
}
|
||||
|
||||
export const parseMzHeaderEvidence = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureAtOffset?: Buffer,
|
||||
): MzHeaderEvidence => ({
|
||||
dos: parseDosMzHeader(bytes, fileSize),
|
||||
windowsHeader: observeMzWindowsHeader(bytes, fileSize, signatureAtOffset),
|
||||
});
|
||||
|
||||
const validateRelocations = (
|
||||
bytes: Buffer,
|
||||
relocationTableOffset: number,
|
||||
|
||||
@@ -6,6 +6,22 @@ import {
|
||||
} from "./GhidraLoadImage.fixture.js";
|
||||
import { nativeLoadImageObservationSchema } from "../domain/native/nativeLoadImage.js";
|
||||
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
|
||||
import { prepareGhidraDosLoadImage } from "./GhidraLoadImageValues.js";
|
||||
|
||||
it("does not interpret an ambiguous 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(2, 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",
|
||||
});
|
||||
});
|
||||
|
||||
const damage: ReadonlyArray<
|
||||
readonly [string, (observation: NativeLoadImageObservation) => void]
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type NativeLoadImageCheck,
|
||||
type NativeLoadImageObservation,
|
||||
} from "../domain/native/nativeLoadImage.js";
|
||||
import { parseDosMzHeader, type DosMzHeader } from "../domain/dosMz.js";
|
||||
import { parseMzHeaderEvidence, type DosMzHeader } from "../domain/dosMz.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { validateDosComLength } from "../domain/dosCom.js";
|
||||
@@ -57,7 +57,16 @@ export const prepareGhidraDosLoadImage = (
|
||||
fileBytes: number,
|
||||
signatureAtOffset?: Buffer,
|
||||
): Result<GhidraDosImage, string> => {
|
||||
const parsed = parseDosMzHeader(bytes, fileBytes, signatureAtOffset);
|
||||
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")
|
||||
return err("MZ Windows new-header pointer is outside the bounded prefix");
|
||||
if (evidence.windowsHeader.state === "signature-needed")
|
||||
return err("MZ Windows new-header signature is outside the bounded prefix");
|
||||
if (evidence.windowsHeader.state === "declared")
|
||||
return err("MZ declares a Windows new header; it is not a DOS-only image");
|
||||
const parsed = evidence.dos;
|
||||
if (!parsed.ok) return parsed;
|
||||
const header = parsed.value;
|
||||
if (header.imageBytes > bytes.length)
|
||||
|
||||
@@ -6,6 +6,20 @@ import {
|
||||
setGoPeVirtualSize,
|
||||
} from "../../../tests/fixtures/go/image.js";
|
||||
|
||||
it("keeps a DOS-valid but unsigned Windows-header candidate unresolved", () => {
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(2, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
expect(() => readPeImage(bytes)).toThrow(
|
||||
"MZ Windows new-header candidate is unresolved",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([32, 64] as const)(
|
||||
"uses raw-size fallback for PE%d Go build-info and keeps RVA mappings intact",
|
||||
(bits) => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { readPeSectionHeader } from "./NativeSectionDirectory.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
parseDosMzHeader,
|
||||
} from "../../domain/dosMz.js";
|
||||
import { parseMzHeaderEvidence } from "../../domain/dosMz.js";
|
||||
import {
|
||||
declaredFileRange,
|
||||
BinaryFormatFailure,
|
||||
@@ -14,9 +11,15 @@ import {
|
||||
} from "./BinaryContainer.js";
|
||||
|
||||
const peHeaderOffset = (reader: BinaryReader): number => {
|
||||
const windowsHeader = observeMzWindowsHeader(reader.bytes);
|
||||
const evidence = parseMzHeaderEvidence(reader.bytes);
|
||||
const { windowsHeader } = evidence;
|
||||
if (windowsHeader.state === "candidate")
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"MZ Windows new-header candidate is unresolved",
|
||||
);
|
||||
if (windowsHeader.state !== "declared") {
|
||||
const dos = parseDosMzHeader(reader.bytes);
|
||||
const dos = evidence.dos;
|
||||
if (!dos.ok) throw new BinaryFormatFailure("malformed", dos.error);
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
|
||||
@@ -1118,6 +1118,76 @@ describe("binary Info.plist app bundle targets", () => {
|
||||
});
|
||||
|
||||
describe("DOS binary target I/O", () => {
|
||||
it("retains valid DOS facts while refusing automatic selection for an unresolved pointer", async () => {
|
||||
const directory = await createTestTempDirectory("rea-ambiguous-mz-target-");
|
||||
const path = join(directory, "legacy.exe");
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(2, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
await writeFile(path, bytes);
|
||||
|
||||
const result = await parseBinaryTarget(path);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "MZ Windows new-header candidate is unresolved",
|
||||
diagnostics: {
|
||||
mz_windows_header: {
|
||||
state: "candidate",
|
||||
offset: 0x44434241,
|
||||
signature: "unobserved",
|
||||
},
|
||||
mz_dos: {
|
||||
state: "valid",
|
||||
header_bytes: 64,
|
||||
image_bytes: 128,
|
||||
module_bytes: 64,
|
||||
relocation_count: 2,
|
||||
relocation_table_offset: 28,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("validates ambiguous DOS facts after extending the bounded prefix for relocations", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-ambiguous-mz-relocations-",
|
||||
);
|
||||
const path = join(directory, "legacy.exe");
|
||||
const bytes = Buffer.alloc(8192);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(16, 4);
|
||||
bytes.writeUInt16LE(1010, 6);
|
||||
bytes.writeUInt16LE(257, 8);
|
||||
bytes.writeUInt16LE(64, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
await writeFile(path, bytes);
|
||||
|
||||
const result = await parseBinaryTarget(path);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "MZ Windows new-header candidate is unresolved",
|
||||
diagnostics: {
|
||||
mz_dos: {
|
||||
state: "valid",
|
||||
header_bytes: 4112,
|
||||
image_bytes: 8192,
|
||||
module_bytes: 4080,
|
||||
relocation_count: 1010,
|
||||
relocation_table_offset: 64,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves a short-header PE signature beyond the bounded prefix", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-short-stub-pe-target-",
|
||||
|
||||
Reference in new issue
Block a user