diff --git a/src/artifacts/apple/machoContainer.test.ts b/src/artifacts/apple/machoContainer.test.ts index 40403e397c..8683a3e778 100644 --- a/src/artifacts/apple/machoContainer.test.ts +++ b/src/artifacts/apple/machoContainer.test.ts @@ -12,6 +12,7 @@ import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js"; import { classifyArtifactBytes } from "../inventory/ArtifactGraphConstruction.js"; import { createArtifactClassificationProbe } from "../ArtifactClassificationProbe.js"; import { parseExecutableHeader } from "../../domain/binaryTarget.js"; +import { isJvmClassFile } from "../../domain/apple/jvmClassFile.js"; import { classifyFatContainerIdentity, classifyFatContainerEvidence, @@ -313,6 +314,39 @@ describe("FAT Mach-O identity", () => { }); describe("FAT slice header identity and bounds", () => { + const classWithInterface = (reference: number): Buffer => { + // Retain enough bytes for the coincident FAT table claim to be readable; + // otherwise rejecting JVM identity would still yield not-fat on truncation. + const source = javaClassFile(2048, true); + const interfaceCountOffset = source.length - 8; + const bytes = Buffer.concat([ + source.subarray(0, interfaceCountOffset + 2), + Buffer.alloc(2), + source.subarray(interfaceCountOffset + 2), + ]); + bytes.writeUInt16BE(1, interfaceCountOffset); + bytes.writeUInt16BE(reference, interfaceCountOffset + 2); + return bytes; + }; + + it("preserves JVM identity for class-tagged interface references", () => { + const bytes = classWithInterface(3); + expect(isJvmClassFile(bytes)).toBe(true); + expect(readFatContainerHeader(bytes, bytes.length)).toEqual({ + state: "not-fat", + }); + }); + + it.each([0, 2, 8, 65535])( + "does not suppress FAT examination for invalid JVM interface index %i", + (reference) => { + // Zero, a UTF-8 entry, and out-of-pool indices are not class references. + const bytes = classWithInterface(reference); + expect(isJvmClassFile(bytes)).toBe(false); + expect(readFatContainerHeader(bytes, bytes.length).state).toBe("parsed"); + }, + ); + it("keeps Java CAFEBABE class files outside FAT and Mach-O identity", async () => { const bytes = javaClassFile(); expect(readFatContainerHeader(bytes, bytes.byteLength)).toEqual({ diff --git a/src/domain/apple/jvmClassFile.ts b/src/domain/apple/jvmClassFile.ts index 40f52e2cd1..d7f9271491 100644 --- a/src/domain/apple/jvmClassFile.ts +++ b/src/domain/apple/jvmClassFile.ts @@ -256,7 +256,14 @@ export const isJvmClassFile = (bytes: Uint8Array): boolean => { ) return false; const interfaces = cursor.u2(); - if (interfaces === null || !cursor.skip(interfaces * 2)) return false; + if (interfaces === null) return false; + // JVMS 4.1: every interface index must name CONSTANT_Class_info, just + // like this_class and nonzero super_class. Framing alone is insufficient + // to establish JVM identity when the same magic also identifies FAT. + for (let index = 0; index < interfaces; index += 1) { + const reference = cursor.u2(); + if (reference === null || !poolHasTag(pool, reference, 7)) return false; + } if (!readMembers(cursor, pool) || !readMembers(cursor, pool)) return false; const attributes = cursor.u2(); return (