From 0d11f71e0dc5cf95d3bea093355f7edc09089a68 Mon Sep 17 00:00:00 2001 From: Jeremy Levartovsky <140487034+JayOfTheKeyboard@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:41:23 +1100 Subject: [PATCH] fix(managed): admit ReadyToRun images built for non-Windows hosts (#1985) ReadyToRun images compiled for Linux, macOS or the BSDs store the COFF machine XORed with IMAGE_FILE_MACHINE_NATIVE_OS_OVERRIDE (dotnet/runtime src/coreclr/inc/pedecoder.h). Every framework assembly in a Linux .NET install has machine 0xFD1D, so target admission and the managed PE reader rejected them as an unsupported architecture. Decode the PE machine once in src/domain/peMachine.ts, replacing the two duplicated switches, and accept an OS override only on images with a CLI directory. --- src/domain/binaryTarget.test.ts | 35 ++++++++++++ src/domain/binaryTarget.ts | 26 ++------- src/domain/peMachine.ts | 50 +++++++++++++++++ src/dotnet/ManagedArtifactFailures.test.ts | 8 +++ src/dotnet/ManagedPeReader.ts | 34 +++++------- .../managed/managedNativeOsMachine.test.ts | 55 +++++++++++++++++++ 6 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 src/domain/peMachine.ts create mode 100644 tests/boundary/providers/managed/managedNativeOsMachine.test.ts diff --git a/src/domain/binaryTarget.test.ts b/src/domain/binaryTarget.test.ts index 053232ac0b..ed73f582cd 100644 --- a/src/domain/binaryTarget.test.ts +++ b/src/domain/binaryTarget.test.ts @@ -50,6 +50,41 @@ describe("executable header parsing", () => { }); }); + it.each([ + ["Linux x86_64", 0x8664 ^ 0x7b79, "x86_64"], + ["Linux arm64", 0xaa64 ^ 0x7b79, "arm64"], + ["Apple x86_64", 0x8664 ^ 0x4644, "x86_64"], + ["Apple arm64", 0xaa64 ^ 0x4644, "arm64"], + ["FreeBSD x86_64", 0x8664 ^ 0xadc4, "x86_64"], + ["OpenBSD x86_64", 0x8664 ^ 0xadc5, "x86_64"], + ["NetBSD x86_64", 0x8664 ^ 0x1993, "x86_64"], + ["SunOS x86_64", 0x8664 ^ 0x1992, "x86_64"], + ] as const)( + "decodes the %s ReadyToRun OS-override machine on a managed image", + (_label, machine, architecture) => { + const result = parseExecutableHeader( + pe(machine, 64, 0x2002, true), + "x64", + ); + expect(result.ok && result.value).toMatchObject({ + format: "pe", + architecture, + managed: true, + }); + }, + ); + + it("rejects an OS-override machine on a native image", () => { + const result = parseExecutableHeader( + pe(0x8664 ^ 0x7b79, 64, 0x2002), + "x64", + ); + expect(result).toEqual({ + ok: false, + error: "unsupported PE architecture", + }); + }); + it("selects the host architecture from a FAT table", () => { const bytes = fat([0x01000007, 0x0100000c]); const arm = parseExecutableHeader(bytes, "arm64"); diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index 2d0735b467..ed4daf6cbf 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -4,6 +4,7 @@ import type { } from "./binaryTargetTypes.js"; import { err, ok, type Result } from "./result.js"; import { mzWindowsHeaderOffset, parseDosMzHeader } from "./dosMz.js"; +import { decodePeMachine } from "./peMachine.js"; /** Format and architecture facts recovered from an executable header. */ export type ExecutableMetadata = @@ -216,14 +217,17 @@ export const parsePeRecord = ( record.toString("binary", 0, 4) !== "PE\u0000\u0000" ) return err("invalid or truncated PE header"); - const architecture = peArchitecture(record.readUInt16LE(4)); - if (architecture === undefined) return err("unsupported PE architecture"); + const machine = decodePeMachine(record.readUInt16LE(4)); + if (machine === undefined) return err("unsupported PE architecture"); + const { architecture } = machine; const optionalHeaderSize = record.readUInt16LE(20); if (optionalHeaderSize > 4096 || record.length < 24 + optionalHeaderSize) return err("invalid or truncated PE optional header"); const optionalHeader = record.subarray(24, 24 + optionalHeaderSize); const managed = peManagedStatus(optionalHeader, architecture); if (!managed.ok) return managed; + if (machine.nativeOsOverride && !managed.value) + return err("unsupported PE architecture"); const characteristics = record.readUInt16LE(22); return ok({ format: "pe", @@ -299,21 +303,3 @@ const elfArchitecture = (machine: number): BinaryArchitecture | undefined => { } return undefined; }; - -const peArchitecture = ( - machine: number, -): Exclude | undefined => { - switch (machine) { - case 0x14c: - return "x86"; - case 0x8664: - return "x86_64"; - case 0x1c0: - case 0x1c2: - case 0x1c4: - return "arm"; - case 0xaa64: - return "arm64"; - } - return undefined; -}; diff --git a/src/domain/peMachine.ts b/src/domain/peMachine.ts new file mode 100644 index 0000000000..16a5ace371 --- /dev/null +++ b/src/domain/peMachine.ts @@ -0,0 +1,50 @@ +import type { BinaryArchitecture } from "./binaryTargetTypes.js"; + +export type PeArchitecture = Exclude; + +/** A decoded COFF machine and whether it carried a ReadyToRun OS override. */ +export interface PeMachine { + readonly architecture: PeArchitecture; + readonly nativeOsOverride: boolean; +} + +const architectureForMachine = ( + machine: number, +): PeArchitecture | undefined => { + switch (machine) { + case 0x014c: + return "x86"; + case 0x8664: + return "x86_64"; + case 0x01c0: + case 0x01c2: + case 0x01c4: + return "arm"; + case 0xaa64: + return "arm64"; + } + return undefined; +}; + +/** + * ReadyToRun images compiled for a non-Windows OS store the COFF machine XORed + * with IMAGE_FILE_MACHINE_NATIVE_OS_OVERRIDE (dotnet/runtime + * src/coreclr/inc/pedecoder.h): Apple, FreeBSD, OpenBSD, Linux, NetBSD, SunOS. + */ +const NATIVE_OS_OVERRIDES = [0x4644, 0xadc4, 0xadc5, 0x7b79, 0x1993, 0x1992]; + +/** + * Decode a COFF machine. Only managed images carry an OS override, so callers + * must reject `nativeOsOverride` when the image has no CLI header. + */ +export const decodePeMachine = (machine: number): PeMachine | undefined => { + const direct = architectureForMachine(machine); + if (direct !== undefined) + return { architecture: direct, nativeOsOverride: false }; + for (const override of NATIVE_OS_OVERRIDES) { + const architecture = architectureForMachine(machine ^ override); + if (architecture !== undefined) + return { architecture, nativeOsOverride: true }; + } + return undefined; +}; diff --git a/src/dotnet/ManagedArtifactFailures.test.ts b/src/dotnet/ManagedArtifactFailures.test.ts index eafc705000..2f953bee2e 100644 --- a/src/dotnet/ManagedArtifactFailures.test.ts +++ b/src/dotnet/ManagedArtifactFailures.test.ts @@ -46,6 +46,14 @@ describe("managed artifact failure classification", () => { ]); }); + it("rejects a ReadyToRun OS-override machine without a CLI directory", () => { + const bytes = buildNativePeFixture(); + bytes.writeUInt16LE(0x014c ^ 0x7b79, bytes.readUInt32LE(0x3c) + 4); + expect(() => + inspectManagedArtifactBytes(bytes, managedPeFixtureTarget(bytes)), + ).toThrow("Unsupported PE machine 0x7a35"); + }); + it("keeps malformed reference and attribute issues across inventory callers", () => { const bytes = buildManagedPeFixture({ references: [ diff --git a/src/dotnet/ManagedPeReader.ts b/src/dotnet/ManagedPeReader.ts index 5f345d4026..6f11cc4914 100644 --- a/src/dotnet/ManagedPeReader.ts +++ b/src/dotnet/ManagedPeReader.ts @@ -1,4 +1,5 @@ import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; +import { decodePeMachine } from "../domain/peMachine.js"; import { managedFailure, ManagedReaderFailure, @@ -100,25 +101,18 @@ const add = (left: number, right: number, scope: string): number => { return result; }; -const architectureFor = (machine: number): ManagedPeLayout["architecture"] => { - switch (machine) { - case 0x014c: - return "x86"; - case 0x8664: - return "x86_64"; - case 0x01c0: - case 0x01c2: - case 0x01c4: - return "arm"; - case 0xaa64: - return "arm64"; - default: - throw managedFailure( - "invalid-directory", - "pe.machine", - `Unsupported PE machine 0x${machine.toString(16).padStart(4, "0")}`, - ); - } +const architectureFor = ( + machine: number, + managed: boolean, +): ManagedPeLayout["architecture"] => { + const decoded = decodePeMachine(machine); + if (decoded === undefined || (decoded.nativeOsOverride && !managed)) + throw managedFailure( + "invalid-directory", + "pe.machine", + `Unsupported PE machine 0x${machine.toString(16).padStart(4, "0")}`, + ); + return decoded.architecture; }; /** Resolve one section identity for both exact reads and available-byte queries. */ @@ -398,7 +392,7 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => { } return { machine: coff.machine, - architecture: architectureFor(coff.machine), + architecture: architectureFor(coff.machine, cliDirectoryPresent), optionalHeader: optional.optionalHeader, sectionCount: coff.sectionCount, characteristics: coff.characteristics, diff --git a/tests/boundary/providers/managed/managedNativeOsMachine.test.ts b/tests/boundary/providers/managed/managedNativeOsMachine.test.ts new file mode 100644 index 0000000000..9a31b99d4e --- /dev/null +++ b/tests/boundary/providers/managed/managedNativeOsMachine.test.ts @@ -0,0 +1,55 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; +import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; +import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; +import { ManagedStaticProvider } from "../../../../src/dotnet/ManagedStaticProvider.js"; + +// ReadyToRun images compiled for a non-Windows OS store the machine XORed with +// IMAGE_FILE_MACHINE_NATIVE_OS_OVERRIDE (dotnet/runtime src/coreclr/inc/pedecoder.h). +it.each([ + ["Linux x86", 0x014c ^ 0x7b79, 0, "x86"], + ["Linux ARM", 0x01c4 ^ 0x7b79, 0, "arm"], + ["Apple ARM", 0x01c4 ^ 0x4644, 0, "arm"], + ["FreeBSD x86", 0x014c ^ 0xadc4, 0, "x86"], +] as const)( + "opens and inspects the %s ReadyToRun machine through the public path", + async (_label, machine, cliFlags, architecture) => { + const bytes = buildManagedPeFixture({ cliFlags, readyToRun: true }); + bytes.writeUInt16LE(machine, bytes.readUInt32LE(0x3c) + 4); + const directory = await createTestTempDirectory("rea-managed-os-machine-"); + const path = join(directory, "fixture.dll"); + await writeFile(path, bytes); + const parsed = await parseBinaryTarget(path); + if (!parsed.ok) throw parsed.error; + expect(parsed.value).toMatchObject({ + kind: "executable", + format: "pe", + architecture, + managed: true, + }); + const client = new ManagedStaticProvider().createClient(parsed.value); + try { + for (const operation of [ + "inspect_managed_artifact", + "inspect_managed_members", + "inspect_managed_native_boundaries", + ] as const) { + const observed = await client.execute(operation, {}); + if (!observed.ok) throw observed.error; + expect(observed.value.result).toMatchObject({ + module: { name: "Fixture.dll" }, + }); + } + const artifact = await client.execute("inspect_managed_artifact", {}); + if (!artifact.ok) throw artifact.error; + expect(artifact.value.result).toMatchObject({ + pe: { machine, architecture }, + classification: { managed_architecture: architecture }, + }); + } finally { + await client.close(); + } + }, +);