From ac57c88efe64c3125850b54d3d833227e6b8ec51 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:24:29 +0800 Subject: [PATCH] fix(formats): preserve high bits in Windows signature checks Decode MZ and Windows signature bytes without ASCII high-bit masking. Move malformed signature cases into the shared Go diagnostics fixtures so provider, compiled CLI, and MCP workflows cover NE, LE, LX, and PE lookalikes; remove the superseded provider-only cases. Validation: runtime build, check:ci, and focused DOS/Go/target/CLI/MCP tests. Windows-only reference-import cases await CI. --- src/domain/dosMz.test.ts | 30 ++++++++++++++-------- src/domain/dosMz.ts | 6 ++--- tests/boundary/go/goBinaryProvider.test.ts | 14 ---------- tests/fixtures/go/image.ts | 9 +++++++ 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/domain/dosMz.test.ts b/src/domain/dosMz.test.ts index 510149142..bd98ca7e8 100644 --- a/src/domain/dosMz.test.ts +++ b/src/domain/dosMz.test.ts @@ -170,18 +170,26 @@ describe("MZ Windows header observations", () => { }); }); - 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"); + it.each(["PEZZ", "NE", "LE", "LX", "PE"])( + "keeps a non-Windows %s signature candidate as DOS", + (signature) => { + const bytes = dosMz(64); + const offset = 32; + bytes.writeUInt32LE(offset, 60); + const candidate = Buffer.from( + [...Buffer.from(signature)].map((byte) => + signature === "PEZZ" ? byte : byte | 0x80, + ), + ); + bytes.set(candidate, offset); - expect(observeMzWindowsHeader(bytes)).toEqual({ state: "absent" }); - expect(parseExecutableHeader(bytes, "x64")).toMatchObject({ - ok: true, - value: { format: "dos-mz", architecture: "x86" }, - }); - }); + 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); diff --git a/src/domain/dosMz.ts b/src/domain/dosMz.ts index 22e7e4611..235dc25f4 100644 --- a/src/domain/dosMz.ts +++ b/src/domain/dosMz.ts @@ -62,7 +62,7 @@ export const observeMzWindowsHeader = ( const observedPePrefix = available !== undefined && available.length >= 2 && - available.toString("ascii", 0, 2) === "PE"; + available.toString("latin1", 0, 2) === "PE"; if (fileSize - offset < (observedPePrefix ? 4 : 2)) return { state: "absent" }; return { state: "signature-needed", offset }; @@ -72,7 +72,7 @@ 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); + const prefix = bytes.toString("latin1", 0, 2); if (prefix === "NE" || prefix === "LE" || prefix === "LX") return prefix; if (prefix !== "PE") return "other"; if (bytes.length < 4) return "unobserved"; @@ -91,7 +91,7 @@ export const parseDosMzHeader = ( fileSize = bytes.length, signatureAtOffset?: Buffer, ): Result => { - if (bytes.length < 28 || bytes.toString("ascii", 0, 2) !== "MZ") + 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"); diff --git a/tests/boundary/go/goBinaryProvider.test.ts b/tests/boundary/go/goBinaryProvider.test.ts index 9ee3ad4b4..5a43cd8d2 100644 --- a/tests/boundary/go/goBinaryProvider.test.ts +++ b/tests/boundary/go/goBinaryProvider.test.ts @@ -49,20 +49,6 @@ it.each([32, 64] as const)( }, ); -it.each([{ signature: [0xce, 0xc5] }, { signature: [0xcc, 0xd8] }])( - "rejects high-bit lookalikes of Windows signatures %j", - async ({ signature }) => { - const root = await createTestTempDirectory("rea-go-pe-signature-"); - const fixture = createGoBinaryFixture({ format: "pe" }); - fixture.bytes.set(signature, 128); - const path = join(root, "corrupt-signature.exe"); - await writeFile(path, fixture.bytes); - const result = await new GoBinaryProvider().inspect({ path }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisInputError); - }, -); - it("preserves the actual build-info byte guard as a resource failure", async () => { const fixture = createGoBinaryFixture(); fixture.bytes.set([0x81, 0x80, 0x40], fixture.headerOffset + 32); diff --git a/tests/fixtures/go/image.ts b/tests/fixtures/go/image.ts index 009c92917..0c56050dc 100644 --- a/tests/fixtures/go/image.ts +++ b/tests/fixtures/go/image.ts @@ -203,6 +203,14 @@ export const createGoBinaryDiagnosticFixtures = () => { } const damagedPe = createGoBinaryFixture({ format: "pe" }).bytes; damagedPe.write("NO", 128, "ascii"); + const highBitSignatures = ["NE", "LE", "LX", "PE"].map((signature) => { + const bytes = createGoBinaryFixture({ format: "pe" }).bytes; + bytes.set( + [...Buffer.from(signature)].map((byte) => byte | 0x80), + 128, + ); + return { name: `high-bit-${signature.toLowerCase()}`, bytes }; + }); const absentDirectories = createGoBinaryFixture({ format: "pe" }).bytes; absentDirectories.writeUInt32LE(1, 152 + 108); const shortOptionalHeaders = [0, 1].map((size) => { @@ -220,6 +228,7 @@ export const createGoBinaryDiagnosticFixtures = () => { })), ...[ { name: "damaged-pe", bytes: damagedPe }, + ...highBitSignatures, { name: "absent-directories", bytes: absentDirectories }, ...shortOptionalHeaders, ].map((fixture) => ({