From d9d77726fffd92c33c52410516f1a9a9b9608f8a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:10:38 +0800 Subject: [PATCH] fix(pe): distinguish unsupported overlapping header layouts --- docs/installation.md | 7 +++++++ src/application/BinaryTargetResolver.ts | 12 ++++++++++-- src/domain/binaryTarget.ts | 5 ++++- src/domain/dosMz.test.ts | 14 ++++++++++++++ src/domain/dosMz.ts | 8 ++++++++ src/native/metadata/PeImage.test.ts | 13 +++++++++++++ src/native/metadata/PeImage.ts | 13 +++++++------ tests/boundary/filesystem/binaryTarget.test.ts | 17 +++++++++++++++++ 8 files changed, 80 insertions(+), 9 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 3d6a1bbd63..17985a2203 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -968,6 +968,13 @@ dossiers, instructions, recovered data types, measured load mappings, loaded memory bytes, and observed file offsets. Independent load-image attestation supports DOS MZ, explicitly selected COM, and explicit raw-binary imports; PE returns measurements with an unsupported attestation status. +Target admission preserves recognized PE/NE/LE/LX signatures separately from +reader layout support. A Windows header overlapping the first 64 DOS-header +bytes remains an unsupported layout in this release; it is not reported as an +invalid offset. Tiny PE files with short optional headers have additional +unverified loader requirements. Unconfirmed nonzero DOS padding remains an +unresolved Windows-header candidate, with independently validated DOS facts in +the target error diagnostics; those facts do not prove a Windows header absent. On Linux and macOS, `annotate_native_function` also edits a function name and/or entry comments atomically and returns refreshed analysis, and `set_address_name` and `set_addresses_names` name a function entry or any other mapped address diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index 2ed5a57966..3cd929f071 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -45,6 +45,7 @@ import { } from "../domain/rawBinary.js"; import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import { + mzWindowsHeaderLayoutLimitation, observeMzWindowsHeader, parseMzHeaderEvidence, type DosMzHeader, @@ -786,8 +787,15 @@ const readExecutableMetadata = async ( } const { windowsHeader } = evidence; if (windowsHeader.state === "declared") { - if (windowsHeader.offset < 64) - return err("invalid Windows new-header offset in MZ image"); + const limitation = mzWindowsHeaderLayoutLimitation(windowsHeader); + if (limitation !== undefined) + return err({ + reason: limitation, + diagnostics: { + unsupported_layout: true, + mz_windows_header: { ...windowsHeader }, + }, + }); return readPeMetadata( handle, windowsHeader.offset, diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index 4d34fd9ab0..d9fba4436f 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -11,6 +11,7 @@ import type { import { err, ok, type Result } from "./result.js"; import { readElfFixedHeader } from "./elfFixedHeader.js"; import { + mzWindowsHeaderLayoutLimitation, parseMzHeaderEvidence, type MzWindowsHeaderSignatureEvidence, } from "./dosMz.js"; @@ -453,7 +454,9 @@ const parseMz = ( return err("MZ Windows new-header candidate is unresolved"); if (windowsHeader.state === "declared") { const { offset } = windowsHeader; - if (offset < 64 || offset > bytes.length - 24) + const limitation = mzWindowsHeaderLayoutLimitation(windowsHeader); + if (limitation !== undefined) return err(limitation); + if (offset > bytes.length - 24) return err("invalid or truncated Windows executable header in MZ image"); if (["NE", "LE", "LX"].includes(windowsHeader.signature)) return err( diff --git a/src/domain/dosMz.test.ts b/src/domain/dosMz.test.ts index 571643add5..3e7b874926 100644 --- a/src/domain/dosMz.test.ts +++ b/src/domain/dosMz.test.ts @@ -167,6 +167,20 @@ describe("DOS MZ load-module validation", () => { }); describe("MZ Windows header observations", () => { + it("keeps a confirmed overlapping PE distinct from malformed header offsets", () => { + const bytes = pe(0x014c, 4); + expect(observeMzWindowsHeader(bytes)).toEqual({ + state: "declared", + offset: 4, + signature: "PE", + }); + expect(parseExecutableHeader(bytes, "x64")).toEqual({ + ok: false, + error: + "Unsupported overlapping PE header at file offset 4; this reader requires the Windows header outside the 64-byte DOS header", + }); + }); + it.each(["NE", "LE", "LX"])( "distinguishes unsupported %s from DOS", (signature) => { diff --git a/src/domain/dosMz.ts b/src/domain/dosMz.ts index 03a5be0e3d..02ed5076f2 100644 --- a/src/domain/dosMz.ts +++ b/src/domain/dosMz.ts @@ -45,6 +45,14 @@ export interface MzWindowsHeaderSignatureEvidence { readonly bytes: Buffer; } +/** A reader's layout limitation does not invalidate a recognized Windows signature. */ +export const mzWindowsHeaderLayoutLimitation = ( + header: Extract, +): string | undefined => + header.offset < 64 + ? `Unsupported overlapping ${header.signature} header at file offset ${String(header.offset)}; this reader requires the Windows header outside the 64-byte DOS header` + : undefined; + /** * Observe the Windows new-header pointer without mistaking short DOS module * bytes for e_lfanew. A nonzero pointer in a complete DOS header is only a diff --git a/src/native/metadata/PeImage.test.ts b/src/native/metadata/PeImage.test.ts index 2d27014993..50545bc8e7 100644 --- a/src/native/metadata/PeImage.test.ts +++ b/src/native/metadata/PeImage.test.ts @@ -1,11 +1,24 @@ import { expect, it } from "vitest"; import { readGoBinaryImage } from "../../go/GoBinaryImage.js"; import { readPeImage } from "./PeImage.js"; +import { pe } from "../../domain/binaryTarget.fixture.js"; +import { BinaryFormatFailure } from "./BinaryContainer.js"; import { createGoBinaryFixture, setGoPeVirtualSize, } from "../../../tests/fixtures/go/image.js"; +it("classifies a confirmed overlapping PE as an unsupported layout", () => { + try { + readPeImage(pe(0x014c, 4)); + throw new Error("Expected an unsupported PE layout"); + } catch (cause) { + expect(cause).toBeInstanceOf(BinaryFormatFailure); + expect(cause).toMatchObject({ kind: "unsupported" }); + expect((cause as Error).message).toContain("overlapping PE header"); + } +}); + it("keeps a DOS-valid but unsigned Windows-header candidate unresolved", () => { const bytes = Buffer.alloc(128); bytes.write("MZ", 0, "ascii"); diff --git a/src/native/metadata/PeImage.ts b/src/native/metadata/PeImage.ts index 3d03b2e1c9..5c89dc9a20 100644 --- a/src/native/metadata/PeImage.ts +++ b/src/native/metadata/PeImage.ts @@ -1,5 +1,8 @@ import { readPeSectionHeader } from "./NativeSectionDirectory.js"; -import { parseMzHeaderEvidence } from "../../domain/dosMz.js"; +import { + mzWindowsHeaderLayoutLimitation, + parseMzHeaderEvidence, +} from "../../domain/dosMz.js"; import { declaredFileRange, BinaryFormatFailure, @@ -27,11 +30,9 @@ const peHeaderOffset = (reader: BinaryReader): number => { ); } const { offset: pe } = windowsHeader; - if (pe < 64) - throw new BinaryFormatFailure( - "malformed", - "PE signature overlaps the DOS header", - ); + const limitation = mzWindowsHeaderLayoutLimitation(windowsHeader); + if (limitation !== undefined) + throw new BinaryFormatFailure("unsupported", limitation); reader.range(pe, 24, "PE signature and COFF header"); if (["NE", "LE", "LX"].includes(windowsHeader.signature)) throw new BinaryFormatFailure( diff --git a/tests/boundary/filesystem/binaryTarget.test.ts b/tests/boundary/filesystem/binaryTarget.test.ts index 3b735d136a..37dbf0f78b 100644 --- a/tests/boundary/filesystem/binaryTarget.test.ts +++ b/tests/boundary/filesystem/binaryTarget.test.ts @@ -1139,6 +1139,23 @@ describe("binary Info.plist app bundle targets", () => { }); describe("DOS binary target I/O", () => { + it("retains the declared PE signature when its overlapping layout is unsupported", async () => { + const directory = await createTestTempDirectory("rea-overlapping-pe-"); + const path = join(directory, "overlap.exe"); + await writeFile(path, pe(0x014c, 4)); + const result = await parseBinaryTarget(path); + expect(result).toMatchObject({ + ok: false, + error: { + reason: expect.stringContaining("Unsupported overlapping PE header"), + diagnostics: { + unsupported_layout: true, + mz_windows_header: { state: "declared", offset: 4, signature: "PE" }, + }, + }, + }); + }); + 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");