fix(targets): recognize Windows headers after short DOS stubs
Observe MZ header declarations and signature uncertainty at the domain boundary, then reuse those facts in bounded target resolution, inventory, and Go PE parsing. Preserve DOS relocation ownership and fail closed when the relevant bytes remain unexamined. Validation: 59 domain tests, 130 target/Go boundary tests, and public CLI inspection of a clang/lld-link PE DLL with a short DOS header. Fixes #2172.
This commit is contained in:
1 parent
f611d8ce72
commit
c9fed50691
7 files changed
+314
-57
No files matched your search
@@ -31,7 +31,7 @@ import {
|
||||
rawBinarySelectionError,
|
||||
} from "../domain/rawBinary.js";
|
||||
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
import { mzWindowsHeaderOffset } from "../domain/dosMz.js";
|
||||
import { observeMzWindowsHeader } from "../domain/dosMz.js";
|
||||
import {
|
||||
hasZipSignature,
|
||||
isZipPackageFormat,
|
||||
@@ -412,11 +412,31 @@ const readExecutableMetadata = async (
|
||||
const fileSize = (await handle.stat()).size;
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
|
||||
const offset = mzWindowsHeaderOffset(bytes);
|
||||
if (offset !== null) {
|
||||
if (offset < 64)
|
||||
let windowsHeader = observeMzWindowsHeader(bytes, fileSize);
|
||||
let signatureBytes: Buffer | undefined;
|
||||
if (windowsHeader.state === "signature-needed") {
|
||||
const { offset } = windowsHeader;
|
||||
const signature = Buffer.alloc(Math.min(4, fileSize - offset));
|
||||
const observedSignature = await handle.read(
|
||||
signature,
|
||||
0,
|
||||
signature.length,
|
||||
offset,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
signatureBytes = signature.subarray(0, observedSignature.bytesRead);
|
||||
windowsHeader = observeMzWindowsHeader(bytes, fileSize, signatureBytes);
|
||||
}
|
||||
if (windowsHeader.state === "declared") {
|
||||
if (windowsHeader.offset < 64)
|
||||
return err("invalid Windows new-header offset in MZ image");
|
||||
return readPeMetadata(handle, offset, signal);
|
||||
return readPeMetadata(
|
||||
handle,
|
||||
windowsHeader.offset,
|
||||
bytes,
|
||||
fileSize,
|
||||
signal,
|
||||
);
|
||||
}
|
||||
// MZ relocation records may extend past the initial probe; their format
|
||||
// bounds are at most 65535 records inside a 65535-paragraph header.
|
||||
@@ -439,7 +459,12 @@ const readExecutableMetadata = async (
|
||||
fileSize,
|
||||
);
|
||||
}
|
||||
return parseExecutableHeader(bytes, hostArchitecture, fileSize);
|
||||
return parseExecutableHeader(
|
||||
bytes,
|
||||
hostArchitecture,
|
||||
fileSize,
|
||||
signatureBytes,
|
||||
);
|
||||
}
|
||||
if (bytes.length >= 8) {
|
||||
const magic = bytes.readUInt32BE(0);
|
||||
@@ -490,6 +515,8 @@ const readExecutableMetadata = async (
|
||||
const readPeMetadata = async (
|
||||
handle: FileHandle,
|
||||
offset: number,
|
||||
mzPrefix: Buffer,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<ExecutableMetadata, string>> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
@@ -503,9 +530,15 @@ const readPeMetadata = async (
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (fileHeaderRead.bytesRead !== fileHeader.length)
|
||||
return err("invalid or truncated PE header");
|
||||
const signature = fileHeader.toString("ascii", 0, 2);
|
||||
if (["NE", "LE", "LX"].includes(signature))
|
||||
return err(`unsupported ${signature} executable in MZ image`);
|
||||
const windowsHeader = observeMzWindowsHeader(
|
||||
mzPrefix,
|
||||
fileSize,
|
||||
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))
|
||||
return err(`unsupported ${windowsHeader.signature} executable in MZ image`);
|
||||
const optionalHeaderSize = fileHeader.readUInt16LE(20);
|
||||
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
|
||||
const record = Buffer.alloc(24 + optionalHeaderSize);
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
zipPackageFormatForPath,
|
||||
type ZipPackageFormat,
|
||||
} from "../../domain/zipPackageFormat.js";
|
||||
import { mzWindowsHeaderOffset, parseDosMzHeader } from "../../domain/dosMz.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
parseDosMzHeader,
|
||||
} from "../../domain/dosMz.js";
|
||||
|
||||
/** Mutable internal occurrence used until root-bound IDs are known. */
|
||||
export interface MutableOccurrence {
|
||||
@@ -450,17 +453,19 @@ export const classifyArtifactBytes = (
|
||||
prefix.length < Math.min(fileSize, 64)
|
||||
)
|
||||
return "unknown";
|
||||
const windowsOffset = mzWindowsHeaderOffset(prefix);
|
||||
if (windowsOffset !== null) {
|
||||
const windowsHeader = observeMzWindowsHeader(prefix, fileSize);
|
||||
if (windowsHeader.state === "signature-needed") return "unknown";
|
||||
if (windowsHeader.state === "declared") {
|
||||
const windowsOffset = windowsHeader.offset;
|
||||
if (
|
||||
windowsHeader.signature === "PE" &&
|
||||
windowsOffset >= 64 &&
|
||||
windowsOffset <= prefix.length - 4 &&
|
||||
prefix
|
||||
.subarray(windowsOffset, windowsOffset + 4)
|
||||
.equals(Buffer.from([0x50, 0x45, 0, 0]))
|
||||
windowsOffset <= prefix.length - 4
|
||||
)
|
||||
return "pe";
|
||||
} else if (parseDosMzHeader(prefix, fileSize).ok) return "dos-mz";
|
||||
return "unknown";
|
||||
}
|
||||
if (parseDosMzHeader(prefix, fileSize).ok) return "dos-mz";
|
||||
return "unknown";
|
||||
}
|
||||
return "file";
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
UniversalMachOSlice,
|
||||
} from "./binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { mzWindowsHeaderOffset, parseDosMzHeader } from "./dosMz.js";
|
||||
import { observeMzWindowsHeader, parseDosMzHeader } from "./dosMz.js";
|
||||
import { decodePeMachine } from "./peMachine.js";
|
||||
|
||||
/** Format and architecture facts recovered from an executable header. */
|
||||
@@ -55,6 +55,7 @@ export const parseExecutableHeader = (
|
||||
bytes: Buffer,
|
||||
hostArchitecture: NodeJS.Architecture,
|
||||
fileSize = bytes.length,
|
||||
mzWindowsHeaderSignature?: Buffer,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
if (
|
||||
bytes.length >= 4 &&
|
||||
@@ -62,7 +63,7 @@ export const parseExecutableHeader = (
|
||||
)
|
||||
return parseElf(bytes, fileSize);
|
||||
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a)
|
||||
return parseMz(bytes, fileSize);
|
||||
return parseMz(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))
|
||||
@@ -226,17 +227,33 @@ const parseElf = (
|
||||
const parseMz = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
signatureAtOffset?: Buffer,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
const offset = mzWindowsHeaderOffset(bytes);
|
||||
if (offset !== null) {
|
||||
const windowsHeader = observeMzWindowsHeader(
|
||||
bytes,
|
||||
fileSize,
|
||||
signatureAtOffset,
|
||||
);
|
||||
if (
|
||||
windowsHeader.state === "header-needed" ||
|
||||
windowsHeader.state === "signature-needed"
|
||||
)
|
||||
return err(
|
||||
"Windows new-header pointer or signature is outside the bounded prefix",
|
||||
);
|
||||
if (windowsHeader.state === "declared") {
|
||||
const { offset } = windowsHeader;
|
||||
if (offset < 64 || offset > bytes.length - 24)
|
||||
return err("invalid or truncated Windows executable header in MZ image");
|
||||
const signature = bytes.toString("ascii", offset, offset + 2);
|
||||
if (["NE", "LE", "LX"].includes(signature))
|
||||
return err(`unsupported ${signature} executable in MZ image`);
|
||||
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
|
||||
return err(
|
||||
`unsupported ${windowsHeader.signature} executable in MZ image`,
|
||||
);
|
||||
if (windowsHeader.signature !== "PE")
|
||||
return err("invalid or truncated Windows executable header in MZ image");
|
||||
return parsePeRecord(bytes.subarray(offset));
|
||||
}
|
||||
const parsed = parseDosMzHeader(bytes, fileSize);
|
||||
const parsed = parseDosMzHeader(bytes, fileSize, signatureAtOffset);
|
||||
return parsed.ok
|
||||
? ok({
|
||||
format: "dos-mz",
|
||||
|
||||
+130
-3
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { dosMz, pe } from "./binaryTarget.fixture.js";
|
||||
import { parseExecutableHeader } from "./binaryTarget.js";
|
||||
import { mzWindowsHeaderOffset, parseDosMzHeader } from "./dosMz.js";
|
||||
import { observeMzWindowsHeader, parseDosMzHeader } from "./dosMz.js";
|
||||
|
||||
describe("DOS MZ load-module validation", () => {
|
||||
it("separates initialized module and appended overlay bytes", () => {
|
||||
@@ -33,7 +33,7 @@ describe("DOS MZ load-module validation", () => {
|
||||
bytes.writeUInt16LE(1, 62);
|
||||
bytes.writeUInt16LE(8, 66);
|
||||
expect(bytes.readUInt32LE(60)).toBe(65536);
|
||||
expect(mzWindowsHeaderOffset(bytes)).toBeNull();
|
||||
expect(observeMzWindowsHeader(bytes)).toEqual({ state: "absent" });
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "dos-mz" },
|
||||
@@ -53,12 +53,54 @@ describe("DOS MZ load-module validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a partial DOS prefix unknown until the Windows pointer is examined", () => {
|
||||
const bytes = dosMz(64);
|
||||
|
||||
expect(observeMzWindowsHeader(bytes.subarray(0, 32), bytes.length)).toEqual(
|
||||
{
|
||||
state: "header-needed",
|
||||
},
|
||||
);
|
||||
expect(
|
||||
parseExecutableHeader(bytes.subarray(0, 32), "x64", bytes.length),
|
||||
).toMatchObject({ ok: false });
|
||||
expect(observeMzWindowsHeader(bytes.subarray(0, 32))).toEqual({
|
||||
state: "absent",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a tiny known-length DOS module without requiring an impossible pointer", () => {
|
||||
const bytes = Buffer.alloc(33);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(bytes.length, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(2, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes[32] = 0xc3;
|
||||
|
||||
expect(observeMzWindowsHeader(bytes.subarray(0, 28), bytes.length)).toEqual(
|
||||
{
|
||||
state: "absent",
|
||||
},
|
||||
);
|
||||
expect(
|
||||
parseExecutableHeader(bytes.subarray(0, 28), "x64", bytes.length),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "dos-mz", architecture: "x86" },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves PE classification when low relocation records do not overlap e_lfanew", () => {
|
||||
const bytes = pe(0x8664);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(1, 6);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
expect(mzWindowsHeaderOffset(bytes)).toBe(64);
|
||||
expect(observeMzWindowsHeader(bytes)).toEqual({
|
||||
state: "declared",
|
||||
offset: 64,
|
||||
signature: "PE",
|
||||
});
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "pe", architecture: "x86_64" },
|
||||
@@ -100,7 +142,9 @@ describe("DOS MZ load-module validation", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MZ Windows header observations", () => {
|
||||
it.each(["NE", "LE", "LX"])(
|
||||
"distinguishes unsupported %s from DOS",
|
||||
(signature) => {
|
||||
@@ -115,6 +159,89 @@ describe("DOS MZ load-module validation", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a short-header nonzero module value as DOS without a signature", () => {
|
||||
const bytes = dosMz(64);
|
||||
bytes.writeUInt32LE(bytes.length, 60);
|
||||
|
||||
expect(observeMzWindowsHeader(bytes)).toEqual({ state: "absent" });
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "dos-mz", architecture: "x86" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a complete non-Windows signature candidate as DOS", () => {
|
||||
const bytes = dosMz(64);
|
||||
const offset = 32;
|
||||
bytes.writeUInt32LE(offset, 60);
|
||||
bytes.write("PEZZ", offset, "ascii");
|
||||
|
||||
expect(observeMzWindowsHeader(bytes)).toEqual({ state: "absent" });
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "dos-mz", architecture: "x86" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a short-header candidate only when it points at a Windows signature", () => {
|
||||
const stub = dosMz(64);
|
||||
const offset = stub.length;
|
||||
stub.writeUInt32LE(offset, 60);
|
||||
const record = pe(0x8664).subarray(64);
|
||||
const bytes = Buffer.concat([stub, record]);
|
||||
|
||||
expect(observeMzWindowsHeader(bytes)).toMatchObject({
|
||||
state: "declared",
|
||||
offset,
|
||||
signature: "PE",
|
||||
});
|
||||
expect(parseExecutableHeader(bytes, "x64")).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "pe", architecture: "x86_64" },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["NE", "LE", "LX"])(
|
||||
"recognizes short-header %s signatures as Windows declarations",
|
||||
(signature) => {
|
||||
const stub = dosMz(64);
|
||||
const offset = stub.length;
|
||||
stub.writeUInt32LE(offset, 60);
|
||||
const bytes = Buffer.concat([stub, Buffer.alloc(24)]);
|
||||
bytes.write(signature, offset, "ascii");
|
||||
|
||||
expect(observeMzWindowsHeader(bytes)).toMatchObject({
|
||||
state: "declared",
|
||||
offset,
|
||||
signature,
|
||||
});
|
||||
expect(parseExecutableHeader(bytes, "x64")).toEqual({
|
||||
ok: false,
|
||||
error: `unsupported ${signature} executable in MZ image`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("requests the signature when a short-header candidate is outside its prefix", () => {
|
||||
const offset = 8192;
|
||||
const bytes = dosMz(64);
|
||||
bytes.writeUInt32LE(offset, 60);
|
||||
const prefix = Buffer.alloc(4096);
|
||||
bytes.copy(prefix);
|
||||
|
||||
expect(observeMzWindowsHeader(prefix, offset + 512)).toEqual({
|
||||
state: "signature-needed",
|
||||
offset,
|
||||
});
|
||||
expect(
|
||||
observeMzWindowsHeader(
|
||||
prefix,
|
||||
offset + 512,
|
||||
Buffer.from("PE\0\0", "binary"),
|
||||
),
|
||||
).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);
|
||||
|
||||
+66
-11
@@ -12,17 +12,31 @@ export interface DosMzHeader {
|
||||
readonly entryOffset: number;
|
||||
}
|
||||
|
||||
export type MzWindowsHeaderObservation =
|
||||
| { readonly state: "absent" }
|
||||
| { readonly state: "header-needed" }
|
||||
| { readonly state: "signature-needed"; readonly offset: number }
|
||||
| {
|
||||
readonly state: "declared";
|
||||
readonly offset: number;
|
||||
readonly signature: "PE" | "NE" | "LE" | "LX" | "other" | "unobserved";
|
||||
};
|
||||
|
||||
/**
|
||||
* Locate a Windows new-header declaration without interpreting a short DOS
|
||||
* header's load-module or relocation-table bytes at 0x3c as e_lfanew. A
|
||||
* new-header pointer must lie in a sufficiently large header and outside
|
||||
* actual relocation records. Zero-sized legacy stubs remain eligible for
|
||||
* PE signature validation.
|
||||
* 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.
|
||||
* A bounded prefix that does not reach the candidate requests one bounded read
|
||||
* from its owner instead of treating unexamined bytes as absent.
|
||||
*/
|
||||
export const mzWindowsHeaderOffset = (bytes: Buffer): number | null => {
|
||||
if (bytes.length < 64) return null;
|
||||
export const observeMzWindowsHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureAtOffset?: Buffer,
|
||||
): MzWindowsHeaderObservation => {
|
||||
if (bytes.length < 64)
|
||||
return fileSize >= 64 ? { state: "header-needed" } : { state: "absent" };
|
||||
const headerBytes = bytes.readUInt16LE(8) * 16;
|
||||
if (headerBytes !== 0 && headerBytes < 64) return null;
|
||||
const relocationCount = bytes.readUInt16LE(6);
|
||||
const tableStart = bytes.readUInt16LE(24);
|
||||
if (
|
||||
@@ -31,9 +45,40 @@ export const mzWindowsHeaderOffset = (bytes: Buffer): number | null => {
|
||||
tableStart < 64 &&
|
||||
tableStart + relocationCount * 4 > 60
|
||||
)
|
||||
return null;
|
||||
return { state: "absent" };
|
||||
const offset = bytes.readUInt32LE(60);
|
||||
return offset === 0 ? null : offset;
|
||||
if (offset === 0) return { state: "absent" };
|
||||
const inlineSignature =
|
||||
offset < bytes.length
|
||||
? bytes.subarray(offset, Math.min(offset + 4, bytes.length))
|
||||
: 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 (signature === "other") return { state: "absent" };
|
||||
const observedPePrefix =
|
||||
available !== undefined &&
|
||||
available.length >= 2 &&
|
||||
available.toString("ascii", 0, 2) === "PE";
|
||||
if (fileSize - offset < (observedPePrefix ? 4 : 2))
|
||||
return { state: "absent" };
|
||||
return { state: "signature-needed", offset };
|
||||
};
|
||||
|
||||
const windowsHeaderSignature = (
|
||||
bytes: Buffer | undefined,
|
||||
): "PE" | "NE" | "LE" | "LX" | "other" | "unobserved" => {
|
||||
if (bytes === undefined || bytes.length < 2) return "unobserved";
|
||||
const prefix = bytes.toString("ascii", 0, 2);
|
||||
if (prefix === "NE" || prefix === "LE" || prefix === "LX") return prefix;
|
||||
if (prefix !== "PE") return "other";
|
||||
if (bytes.length < 4) return "unobserved";
|
||||
return bytes.subarray(0, 4).equals(Buffer.from("PE\0\0", "binary"))
|
||||
? "PE"
|
||||
: "other";
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -44,13 +89,21 @@ export const mzWindowsHeaderOffset = (bytes: Buffer): number | null => {
|
||||
export const parseDosMzHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureAtOffset?: Buffer,
|
||||
): Result<DosMzHeader, string> => {
|
||||
if (bytes.length < 28 || bytes.toString("ascii", 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");
|
||||
if (mzWindowsHeaderOffset(bytes) !== null)
|
||||
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");
|
||||
const lastPageBytes = bytes.readUInt16LE(2);
|
||||
const pages = bytes.readUInt16LE(4);
|
||||
if (pages === 0 || lastPageBytes > 511)
|
||||
@@ -71,6 +124,8 @@ 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,
|
||||
|
||||
+12
-18
@@ -1,4 +1,4 @@
|
||||
import { mzWindowsHeaderOffset, parseDosMzHeader } from "../domain/dosMz.js";
|
||||
import { observeMzWindowsHeader, parseDosMzHeader } from "../domain/dosMz.js";
|
||||
import {
|
||||
GoBinaryFormatFailure,
|
||||
GoBinaryReader,
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from "./GoBinaryContainer.js";
|
||||
|
||||
const peHeaderOffset = (reader: GoBinaryReader): number => {
|
||||
const pe = mzWindowsHeaderOffset(reader.bytes);
|
||||
if (pe === null) {
|
||||
const windowsHeader = observeMzWindowsHeader(reader.bytes);
|
||||
if (windowsHeader.state !== "declared") {
|
||||
const dos = parseDosMzHeader(reader.bytes);
|
||||
if (!dos.ok) throw new GoBinaryFormatFailure("malformed", dos.error);
|
||||
throw new GoBinaryFormatFailure(
|
||||
@@ -17,18 +17,22 @@ const peHeaderOffset = (reader: GoBinaryReader): number => {
|
||||
"DOS MZ images are outside the supported ELF, PE and thin Mach-O formats",
|
||||
);
|
||||
}
|
||||
const { offset: pe } = windowsHeader;
|
||||
if (pe < 64)
|
||||
throw new GoBinaryFormatFailure(
|
||||
"malformed",
|
||||
"PE signature overlaps the DOS header",
|
||||
);
|
||||
const signature = reader
|
||||
.range(pe, 2, "Windows new-header signature")
|
||||
.toString("latin1");
|
||||
if (["NE", "LE", "LX"].includes(signature))
|
||||
reader.range(pe, 24, "PE signature and COFF header");
|
||||
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
|
||||
throw new GoBinaryFormatFailure(
|
||||
"unsupported",
|
||||
`${signature} images are outside the supported ELF, PE and thin Mach-O formats`,
|
||||
`${windowsHeader.signature} images are outside the supported ELF, PE and thin Mach-O formats`,
|
||||
);
|
||||
if (windowsHeader.signature !== "PE")
|
||||
throw new GoBinaryFormatFailure(
|
||||
"malformed",
|
||||
"PE signature is invalid or overlaps the DOS header",
|
||||
);
|
||||
return pe;
|
||||
};
|
||||
@@ -51,16 +55,6 @@ const optionalHeaderMagic = (
|
||||
export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
|
||||
const reader = new GoBinaryReader(bytes, true);
|
||||
const pe = peHeaderOffset(reader);
|
||||
if (
|
||||
!reader
|
||||
.range(pe, 24, "PE signature and COFF header")
|
||||
.subarray(0, 4)
|
||||
.equals(Buffer.from("PE\0\0"))
|
||||
)
|
||||
throw new GoBinaryFormatFailure(
|
||||
"malformed",
|
||||
"PE signature is invalid or overlaps the DOS header",
|
||||
);
|
||||
const optional = pe + 24;
|
||||
const optionalSize = reader.u16(pe + 20);
|
||||
const magic = optionalHeaderMagic(reader, optional, optionalSize);
|
||||
|
||||
@@ -804,6 +804,32 @@ describe("binary Info.plist app bundle targets", () => {
|
||||
});
|
||||
|
||||
describe("DOS binary target I/O", () => {
|
||||
it("resolves a short-header PE signature beyond the bounded prefix", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-short-stub-pe-target-",
|
||||
);
|
||||
const path = join(directory, "short-stub.dll");
|
||||
const offset = 8192;
|
||||
const stub = dosMz(64);
|
||||
stub.writeUInt32LE(offset, 60);
|
||||
const peRecord = pe(0x8664, offset, 0x2002).subarray(offset);
|
||||
const bytes = Buffer.concat([
|
||||
stub,
|
||||
Buffer.alloc(offset - stub.length),
|
||||
peRecord,
|
||||
]);
|
||||
await writeFile(path, bytes);
|
||||
|
||||
expect(await parseBinaryTarget(path)).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
format: "pe",
|
||||
architecture: "x86_64",
|
||||
executableRole: "shared-library",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("validates a complete DOS file beyond the initial metadata probe", async () => {
|
||||
const directory = await createTestTempDirectory("rea-dos-target-");
|
||||
const path = join(directory, "legacy.exe");
|
||||
|
||||
Reference in new issue
Block a user