diff --git a/src/domain/peOptionalHeader.ts b/src/domain/peOptionalHeader.ts index b65e706ced..4500b288b5 100644 --- a/src/domain/peOptionalHeader.ts +++ b/src/domain/peOptionalHeader.ts @@ -8,11 +8,14 @@ export interface PeDataDirectory { readonly offset: number; } -/** Structural facts copied from one declared PE optional header. */ -export interface PeOptionalHeaderFixedFields { +export interface PeOptionalHeaderIdentity { readonly magic: 0x10b | 0x20b; readonly bits: 32 | 64; readonly format: "pe32" | "pe32-plus"; +} + +/** Structural facts copied from one declared PE optional header. */ +export interface PeOptionalHeaderFixedFields extends PeOptionalHeaderIdentity { readonly imageBase: bigint; readonly sectionAlignment: number; readonly fileAlignment: number; @@ -37,10 +40,10 @@ export interface PeOptionalHeaderFailure { readonly message: string; } -/** Decode mapping fields without requiring a directory count or table. */ -export const readPeOptionalHeaderFixedFields = ( +/** Decode format identity without requiring mapping fields or directories. */ +export const readPeOptionalHeaderIdentity = ( bytes: Uint8Array, -): Result => { +): Result => { if (bytes.byteLength < 2) return err({ kind: "malformed", @@ -56,6 +59,16 @@ export const readPeOptionalHeaderFixedFields = ( message: "Only PE32 and PE32+ image optional headers are supported", }); const bits = magic === 0x20b ? 64 : 32; + return ok({ magic, bits, format: bits === 64 ? "pe32-plus" : "pe32" }); +}; + +/** Decode mapping fields without requiring a directory count or table. */ +export const readPeOptionalHeaderFixedFields = ( + bytes: Uint8Array, +): Result => { + const identity = readPeOptionalHeaderIdentity(bytes); + if (!identity.ok) return identity; + const { bits } = identity.value; const directoryOffset = bits === 64 ? 112 : 96; if (bytes.byteLength < 64) return err({ @@ -63,10 +76,9 @@ export const readPeOptionalHeaderFixedFields = ( reason: "truncated-fixed-fields", message: "truncated PE optional header", }); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); return ok({ - magic, - bits, - format: bits === 64 ? "pe32-plus" : "pe32", + ...identity.value, imageBase: bits === 64 ? view.getBigUint64(24, true) diff --git a/src/domain/windowsNativeArtifact.test.ts b/src/domain/windowsNativeArtifact.test.ts index 248469d7a7..e7bc10621f 100644 --- a/src/domain/windowsNativeArtifact.test.ts +++ b/src/domain/windowsNativeArtifact.test.ts @@ -7,11 +7,12 @@ import { } from "./windowsNativeArtifact.js"; const fixture = () => { - const bytes = Buffer.alloc(128); + const bytes = Buffer.alloc(152); bytes.write("MZ"); bytes.writeUInt32LE(64, 0x3c); bytes.write("PE\0\0", 64, "ascii"); bytes.writeUInt16LE(0x8664, 68); + bytes.writeUInt16LE(64, 84); bytes.writeUInt16LE(0x20b, 88); const manifest = { packageVersion: "1.0.0", @@ -116,3 +117,26 @@ it.each(["offset", "signature", "machine", "optional magic"])( }); }, ); + +it.each([0, 1, 65, 0xffff])( + "rejects an unavailable declared optional header of %i bytes", + (length) => { + const { bytes } = fixture(); + bytes.writeUInt16LE(length, 84); + expect(createWindowsNativeManifest("1.0.0", bytes)).toMatchObject({ + ok: false, + error: { message: expect.stringContaining("PE32+") }, + }); + }, +); + +it.each([2, 32, 63, 64])( + "admits PE32+ identity in a present declared header of %i bytes", + (length) => { + const { bytes } = fixture(); + bytes.writeUInt16LE(length, 84); + expect(createWindowsNativeManifest("1.0.0", bytes)).toMatchObject({ + ok: true, + }); + }, +); diff --git a/src/domain/windowsNativeArtifact.ts b/src/domain/windowsNativeArtifact.ts index 0412340549..cdb943cbec 100644 --- a/src/domain/windowsNativeArtifact.ts +++ b/src/domain/windowsNativeArtifact.ts @@ -1,6 +1,7 @@ import { observeMzWindowsHeader } from "./dosMz.js"; import { createHash } from "node:crypto"; import { readPeCoffHeader } from "./peCoffHeader.js"; +import { readPeOptionalHeaderIdentity } from "./peOptionalHeader.js"; import { err, ok, type Result } from "./result.js"; /** Fixed compatibility contract shared by the artifact producer and readers. */ @@ -99,14 +100,21 @@ const validateWindowsX64Artifact = ( ), ); const offset = observed.offset; - if (offset > bytes.length - 26 || bytes.readUInt16LE(offset + 24) !== 0x20b) + const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24)); + if ( + !coff.ok || + coff.value.machine !== 0x8664 || + coff.value.optionalHeaderSize > bytes.length - offset - 24 + ) return err( new WindowsNativeArtifactError( "Windows native artifact is not a Windows x64 PE32+ image", ), ); - const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24)); - if (!coff.ok || coff.value.machine !== 0x8664) + const optional = readPeOptionalHeaderIdentity( + bytes.subarray(offset + 24, offset + 24 + coff.value.optionalHeaderSize), + ); + if (!optional.ok || optional.value.bits !== 64) return err( new WindowsNativeArtifactError( "Windows native artifact is not a Windows x64 PE32+ image",