From 48b92e1be1110fd200cc6d2fd0cdf7c6e2f9e21a Mon Sep 17 00:00:00 2001 From: Joshwong1908 <136307740+Joshwong1908@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:25:32 +0800 Subject: [PATCH] fix(go): classify JVM class files instead of universal Mach-O for CAFEBABE bytes (#2458) * test(go): pin JVM class files as unsupported Go images The regression fails on main: readGoBinaryImage(javaClassFile()) reports "Universal/fat Mach-O containers require selecting a thin slice" for a JVM class file whose bytes 4..7 are minor/major version, not nfat_arch. * fix(go): classify JVM class files instead of universal Mach-O Route the four FAT magics through the shared readFatContainerHeader and classifyFatContainerIdentity facts added for the same disambiguation in the artifact and target lanes (921bb9575). Keep the thin-slice failure only when the container identity is confirmed Mach-O; a JVM class file now falls through to the generic only-ELF/PE/thin-Mach-O failure. * test(go): pin universal Mach-O thin-slice failure with FAT fixtures Cover the kept behavior with the repository's own fatImage/machoImage builders (narrow and FAT64 tables) so the regression also proves real universal containers keep the slice-selection failure. * test(go): cover little-endian FAT magics in the universal container control * fix(go): require positive universal Mach-O identity Require positive Mach-O identity before reporting a universal thin-slice remedy; malformed FAT-shaped bytes are not proof. Add the JVM constant-pool CPU lookalike to the existing fixture control. Source inspection and git diff --check; tests/builds/typechecks/CI reruns skipped at explicit user request. * fix(go): retain malformed FAT header diagnostics Preserve the owning FAT header parser reason for malformed headers while requiring positive Mach-O proof before the universal-slice remedy. Source inspection and git diff --check only. Tests, builds and typechecks skipped by explicit user request. GitHub review writes paused globally. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/go/GoBinaryImage.ts | 30 +++++++++++++++++---- tests/boundary/go/goBinaryImage.test.ts | 35 +++++++++++++++++++++++++ tests/fixtures/javaClassFile.ts | 3 ++- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/go/GoBinaryImage.ts b/src/go/GoBinaryImage.ts index 03d0866ab..4a845c197 100644 --- a/src/go/GoBinaryImage.ts +++ b/src/go/GoBinaryImage.ts @@ -8,6 +8,10 @@ import { type GoBinaryContainer, type GoFileMapping, } from "./GoBinaryContainer.js"; +import { + classifyFatContainerIdentity, + readFatContainerHeader, +} from "../domain/apple/machoContainer.js"; import { readGoElfImage } from "./GoElfImage.js"; import { readGoMachoImage } from "./GoMachoImage.js"; import { readGoPeImage } from "./GoPeImage.js"; @@ -187,11 +191,27 @@ const readContainer = (bytes: Buffer): GoBinaryContainer => { return readGoPeImage(bytes); if (bytes.length >= 4) { const magic = bytes.readUInt32BE(); - if ([0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca].includes(magic)) - throw new GoBinaryFormatFailure( - "unsupported", - "Universal/fat Mach-O containers require selecting a thin slice", - ); + if ([0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca].includes(magic)) { + const fatHeader = readFatContainerHeader(bytes, bytes.length); + if (fatHeader.state === "malformed") + throw new GoBinaryFormatFailure("malformed", fatHeader.reason); + const identity = + fatHeader.state === "parsed" + ? classifyFatContainerIdentity(fatHeader, (offset) => { + if (offset >= bytes.length) return undefined; + if (offset + 4 > bytes.length) return null; + return bytes.readUInt32BE(offset); + }) + : undefined; + // An invalid FAT-shaped table is not proof of a Mach-O container: JVM + // constant-pool bytes can share both the magic and a known CPU value. + const confirmedFatMachO = identity?.state === "mach-o"; + if (confirmedFatMachO) + throw new GoBinaryFormatFailure( + "unsupported", + "Universal/fat Mach-O containers require selecting a thin slice", + ); + } if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic)) return readGoMachoImage(bytes); } diff --git a/tests/boundary/go/goBinaryImage.test.ts b/tests/boundary/go/goBinaryImage.test.ts index 24321ecc8..29f0e952c 100644 --- a/tests/boundary/go/goBinaryImage.test.ts +++ b/tests/boundary/go/goBinaryImage.test.ts @@ -1,4 +1,10 @@ import { expect, it } from "vitest"; +import { javaClassFile } from "../../fixtures/javaClassFile.js"; +import { + CPU, + fatImage, + machoImage, +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js"; import { createGoBinaryFixture, @@ -221,6 +227,35 @@ it("distinguishes unsupported containers from malformed recognized images", () = expect(() => readGoBinaryImage(pe.bytes)).toThrowError(/range|truncated/i); }); +it("reports JVM class files as unsupported images instead of fat Mach-O containers", () => { + expect(() => readGoBinaryImage(javaClassFile())).toThrowError( + /Only ELF, PE, and thin Mach-O native images are supported/, + ); + expect(() => readGoBinaryImage(javaClassFile(4000))).toThrowError( + /Only ELF, PE, and thin Mach-O native images are supported/, + ); + expect(() => readGoBinaryImage(javaClassFile(4000, true))).toThrowError( + /Only ELF, PE, and thin Mach-O native images are supported/, + ); +}); + +it("keeps the thin-slice failure for confirmed universal Mach-O containers", () => { + const slices = [ + { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) }, + { cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) }, + ]; + for (const universal of [ + fatImage(slices), + fatImage(slices, true), + fatImage(slices, false, true), + fatImage(slices, true, true), + ]) { + expect(() => readGoBinaryImage(Buffer.from(universal))).toThrowError( + /fat Mach-O containers require selecting a thin slice/, + ); + } +}); + it("rejects ELF header tables overlapping the native header", () => { const fixture = createGoBinaryFixture({ sectionless: true }); fixture.bytes.writeBigUInt64LE(0n, 32); diff --git a/tests/fixtures/javaClassFile.ts b/tests/fixtures/javaClassFile.ts index 4f7727193..30f5ffc37 100644 --- a/tests/fixtures/javaClassFile.ts +++ b/tests/fixtures/javaClassFile.ts @@ -1,5 +1,5 @@ /** Build a valid minimal JVM class file, optionally with an unused UTF-8 constant. */ -export const javaClassFile = (extraUtf8Bytes = 0): Buffer => { +export const javaClassFile = (extraUtf8Bytes = 0, fatCpuLookalike = false): Buffer => { const utf8 = (value: string): Buffer => { const bytes = Buffer.from(value, "utf8"); const entry = Buffer.alloc(3 + bytes.length); @@ -13,6 +13,7 @@ export const javaClassFile = (extraUtf8Bytes = 0): Buffer => { utf8("Hello"), Buffer.from([7, 0, 4]), utf8("java/lang/Object"), + ...(fatCpuLookalike ? [utf8("x"), Buffer.from([3, 0, 0, 0, 7])] : []), ...(extraUtf8Bytes === 0 ? [] : [utf8("x".repeat(extraUtf8Bytes))]), ]; const headerAndPool = Buffer.alloc(10);