From d4415444c43896f256b834340137a5413a6393fb Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:00:14 +0800 Subject: [PATCH] fix(native): retain processor, hidden-slice and property type facts Separate AMDGPU processors from feature-qualified note identities. Read Apple hidden ARM64 table rows once and preserve visibility through artifact, dylib and fuse consumers. Keep Objective-C++ template commas within type attributes and incomplete metadata unknown. Validated provider/parser and production property regressions, Electron CLI/MCP consumers, and native Apple lipo -hideARM64 output through the production slice reader and public artifact inspection. LLVM execution remains unverified. Fixes #6806 Fixes #6799 Fixes #6801 --- docs/gpu-code-objects.md | 7 +- docs/native-investigation.md | 9 +- .../MachOSliceArtifactReader.test.ts | 93 +++++++++++++++++++ src/artifacts/MachOSliceArtifactReader.ts | 73 ++++++++++++--- src/artifacts/apple/DylibResolutionReader.ts | 5 +- .../apple/MachoLoadCommandReader.test.ts | 17 ++++ src/artifacts/apple/MachoLoadCommandReader.ts | 53 +++++++++++ src/domain/apple/dylibResolution.test.ts | 31 +++++++ src/domain/apple/dylibResolution.ts | 32 +++++-- src/domain/electron/electronFuses.ts | 1 + src/domain/native/nativeInspection.ts | 1 + src/electron/ElectronFuseProvider.test.ts | 33 +++++++ src/electron/ElectronFuseReader.ts | 8 +- src/gpu/AmdGpuTarget.ts | 89 +++++++++++++++++- src/gpu/GpuElfKernels.ts | 33 ++++++- src/gpu/GpuInventory.ts | 6 ++ src/native/AppleDispatchMetadata.test.ts | 27 ++++++ src/native/AppleMachoFixups.test.ts | 27 ++++++ src/native/AppleObjcProperties.ts | 26 ++++-- src/native/parsers/lipo.test.ts | 29 ++++++ src/native/parsers/lipo.ts | 25 ++++- tests/boundary/gpu/codeObjects.test.ts | 78 +++++++++++++++- tests/fixtures/gpu/codeObjects.ts | 4 +- tests/fixtures/objcPropertyMacho.ts | 21 +++-- 24 files changed, 670 insertions(+), 58 deletions(-) diff --git a/docs/gpu-code-objects.md b/docs/gpu-code-objects.md index 2ce43b0372..e3ca4aaa97 100644 --- a/docs/gpu-code-objects.md +++ b/docs/gpu-code-objects.md @@ -116,7 +116,12 @@ Kernel representations have different meanings: including V2 kernel-parameter sizes, and native section declarations. Global register declarations also cover compiler versions that leave the older text-section register field empty. -- AMDGPU reads v3+ MessagePack notes. Argument counts include hidden ABI arguments +- AMDGPU reads v3+ MessagePack notes. V3 processor identity comes from the + `EF_AMDGPU_MACH` field in ELF `e_flags`; known processor values are selectable + even when the note omits `amdhsa.target`. This inventory reports the processor + only; it does not interpret version-specific feature flags. Unknown processor values remain unknown. A known + processor conflict between ELF flags and note metadata leaves the processor + unresolved. Argument counts include hidden ABI arguments when present; SGPR/VGPR counts and fixed group/private segment sizes retain their declared meanings. Legacy YAML metadata remains unsupported. diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 6a84e3f3b9..96f41b1093 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -352,6 +352,12 @@ Available CPU type, subtype/capability bits, byte range, and alignment remain checked against the source bytes. Inventory records source ranges and hashes, and extraction copies those ranges without running the target. This does not expand any analysis provider's supported architectures. +Apple lipo's hidden ARM64 compatibility row is retained separately from the +raw `nfat_arch` count and admitted only from its extra FAT table slot when its +in-range Mach-O header matches the row. `list_architectures` preserves the +per-row `hidden` marker from lipo output. Mach-O image and Electron fuse +inspection include the row in architecture selection while retaining that +marker in their reported slices. `inspect_plist.format` describes the selected source, not plutil's conversion format. Sources explicitly identified as XML, binary or JSON retain those @@ -564,7 +570,8 @@ complete observation. So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for example `NSObject`). The `pointer_fixups` coverage facet names the mechanism. Class properties preserve each encoded attribute as a raw `{name, value}` - pair, for example `{ name: "N", value: "" }`, and report atomicity on the + pair, including commas inside C++ template arguments in the `T` type, for + example `{ name: "N", value: "" }`, and report atomicity on the property: `N` means `nonatomic`; a complete typed attribute string without `N` means the Objective-C default `atomic`; missing or incomplete attribute metadata leaves `atomicity` and any diff --git a/src/artifacts/MachOSliceArtifactReader.test.ts b/src/artifacts/MachOSliceArtifactReader.test.ts index 6193040109..7985e7c823 100644 --- a/src/artifacts/MachOSliceArtifactReader.test.ts +++ b/src/artifacts/MachOSliceArtifactReader.test.ts @@ -154,6 +154,99 @@ describe("Mach-O command cleanup lifecycle", () => { }); }); +describe("MachOSliceArtifactReader hidden architectures", () => { + it("retains Apple's hidden ARM64 row separately from the declared FAT count", async () => { + const root = await createTestTempDirectory("rea-hidden-arm64-slices-"); + const binary = join(root, "fat"); + const armv7 = { type: 12, subtype: 9 }; + const armv7Image = machoImage({ cpu: armv7, wide: false }); + const arm64Image = machoImage({ cpu: CPU.arm64 }); + const bytes = Buffer.from( + fatImage([ + { cpu: armv7, bytes: armv7Image }, + { cpu: CPU.arm64, bytes: arm64Image }, + ]), + ); + bytes.writeUInt32BE(1, 4); + await writeFile(binary, bytes); + const stdout = `fat_magic 0xcafebabe\nnfat_arch 1 (+1 hidden)\narchitecture armv7\n cputype 12\n cpusubtype 9\n offset 4096\n size ${String(armv7Image.length)}\n align 2^12 (4096)\narchitecture arm64 (hidden)\n cputype 16777228\n cpusubtype 0\n offset 8192\n size ${String(arm64Image.length)}\n align 2^12 (4096)\n`; + const reader = new MachOSliceArtifactReader( + binary, + {}, + { + run: async () => + ok({ + tool: "lipo", + executable: "/usr/bin/lipo", + executableSha256: "1".repeat(64), + toolVersion: null, + versionReason: "fixture", + arguments: [], + stdout, + stderr: "", + stdoutBytes: Buffer.byteLength(stdout), + stderrBytes: 0, + exitCode: 0, + signal: null, + }), + }, + ); + onTestFinished(() => reader.close()); + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + expect(entries.map(({ path }) => path)).toEqual([ + "slices/armv7", + "slices/arm64", + ]); + }); + + it("rejects a lipo hidden ARM64 claim when its physical header disagrees", async () => { + const root = await createTestTempDirectory("rea-hidden-arm64-mismatch-"); + const binary = join(root, "fat"); + const armv7 = { type: 12, subtype: 9 }; + const armv7Image = machoImage({ cpu: armv7, wide: false }); + const arm64Image = machoImage({ cpu: CPU.arm64 }); + const bytes = Buffer.from( + fatImage([ + { cpu: armv7, bytes: armv7Image }, + { cpu: CPU.arm64, bytes: arm64Image }, + ]), + ); + bytes.writeUInt32BE(1, 4); + bytes.writeUInt32LE(7, 8196); // Hidden FAT row claims ARM64; thin header says x86. + await writeFile(binary, bytes); + const stdout = `fat_magic 0xcafebabe\nnfat_arch 1 (+1 hidden)\narchitecture armv7\n cputype 12\n cpusubtype 9\n offset 4096\n size ${String(armv7Image.length)}\n align 2^12 (4096)\narchitecture arm64 (hidden)\n cputype 16777228\n cpusubtype 0\n offset 8192\n size ${String(arm64Image.length)}\n align 2^12 (4096)\n`; + const reader = new MachOSliceArtifactReader( + binary, + {}, + { + run: async () => + ok({ + tool: "lipo", + executable: "/usr/bin/lipo", + executableSha256: "1".repeat(64), + toolVersion: null, + versionReason: "fixture", + arguments: [], + stdout, + stderr: "", + stdoutBytes: Buffer.byteLength(stdout), + stderrBytes: 0, + exitCode: 0, + signal: null, + }), + }, + ); + onTestFinished(() => reader.close()); + const enumerate = async (): Promise => { + for await (const _entry of reader.entries()) { + /* enumerate */ + } + }; + await expect(enumerate()).rejects.toMatchObject({ reason: "integrity" }); + }); +}); + describe("MachOSliceArtifactReader", () => { it("keeps a borrowed source usable after idle and active cancelled slice reads", async () => { const root = await createTestTempDirectory("rea-slices-cancelled-"); diff --git a/src/artifacts/MachOSliceArtifactReader.ts b/src/artifacts/MachOSliceArtifactReader.ts index 355fe04b69..cf4b77522a 100644 --- a/src/artifacts/MachOSliceArtifactReader.ts +++ b/src/artifacts/MachOSliceArtifactReader.ts @@ -115,7 +115,8 @@ export class MachOSliceArtifactReader implements ArtifactReader { }; await this.#verifySource(source); const fileSize = source.initial.size; - const { architectures } = parseLipoOutput(captured.value.stdout, this.path); + const lipo = parseLipoOutput(captured.value.stdout, this.path); + const { architectures } = lipo; const structural = await readStructuralSlices(source.handle, fileSize); await this.#verifySource(source); if (structural.status === "malformed") @@ -135,26 +136,70 @@ export class MachOSliceArtifactReader implements ArtifactReader { "unavailable", `Mach-O structural slice index is unsupported: ${structural.reason}`, ); + const hiddenArchitectures = architectures.filter(({ hidden }) => hidden); + const visibleArchitectures = architectures.filter(({ hidden }) => !hidden); + if ( + lipo.hiddenArchitectureCount !== null && + lipo.hiddenArchitectureCount !== hiddenArchitectures.length + ) + throw new ArtifactReaderFailure( + "integrity", + "lipo hidden architecture count disagrees with its architecture rows", + ); + if ( + lipo.declaredArchitectureCount !== null && + lipo.declaredArchitectureCount !== visibleArchitectures.length + ) + throw new ArtifactReaderFailure( + "integrity", + "lipo declared architecture count disagrees with its visible rows", + ); + if ( + (hiddenArchitectures.length > 0 || + (lipo.hiddenArchitectureCount ?? 0) > 0) && + structural.status !== "parsed" + ) + throw new ArtifactReaderFailure( + "unavailable", + "Apple hidden ARM64 slice could not be verified against the FAT table", + ); const slices = structural.status === "parsed" ? structural.slices : null; + const hiddenSlices = + structural.status === "parsed" ? (structural.hiddenSlices ?? []) : []; const matchingSlices = new Map(); if (slices !== null) { - if (slices.length !== architectures.length) + if ( + slices.length !== visibleArchitectures.length || + hiddenSlices.length !== hiddenArchitectures.length || + (hiddenArchitectures.length > 0 && + (lipo.declaredArchitectureCount === null || + lipo.hiddenArchitectureCount === null)) || + (lipo.declaredArchitectureCount !== null && + lipo.declaredArchitectureCount !== slices.length) || + (lipo.hiddenArchitectureCount !== null && + lipo.hiddenArchitectureCount !== hiddenSlices.length) + ) throw new ArtifactReaderFailure( "integrity", - "lipo architecture count disagrees with the Mach-O FAT table", + "lipo declared and hidden architecture counts disagree with the Mach-O FAT table", ); - const unmatched = [...slices]; - for (const reported of architectures) { - const match = unmatched.findIndex((observed) => - lipoMatchesSlice(reported, observed, slices.length === 1), - ); - if (match < 0) - throw new ArtifactReaderFailure( - "integrity", - `lipo slice metadata disagrees with the Mach-O FAT table: ${reported.name} (cputype=${reported.cpu_type ?? "unknown"}, cpusubtype=${reported.cpu_subtype ?? "unknown"}, offset=${reported.file_offset ?? "unknown"}, size=${reported.size ?? "unknown"}, alignment=${reported.alignment ?? "unknown"})`, + for (const [reportedRows, observedRows] of [ + [visibleArchitectures, slices], + [hiddenArchitectures, hiddenSlices], + ] as const) { + const unmatched = [...observedRows]; + for (const reported of reportedRows) { + const match = unmatched.findIndex((observed) => + lipoMatchesSlice(reported, observed, observedRows.length === 1), ); - const [observed] = unmatched.splice(match, 1); - if (observed !== undefined) matchingSlices.set(reported, observed); + if (match < 0) + throw new ArtifactReaderFailure( + "integrity", + `lipo slice metadata disagrees with the Mach-O FAT table: ${reported.name} (cputype=${reported.cpu_type ?? "unknown"}, cpusubtype=${reported.cpu_subtype ?? "unknown"}, offset=${reported.file_offset ?? "unknown"}, size=${reported.size ?? "unknown"}, alignment=${reported.alignment ?? "unknown"})`, + ); + const [observed] = unmatched.splice(match, 1); + if (observed !== undefined) matchingSlices.set(reported, observed); + } } } for (const architecture of architectures) { diff --git a/src/artifacts/apple/DylibResolutionReader.ts b/src/artifacts/apple/DylibResolutionReader.ts index dc73bbbd43..c9d74c930a 100644 --- a/src/artifacts/apple/DylibResolutionReader.ts +++ b/src/artifacts/apple/DylibResolutionReader.ts @@ -7,6 +7,7 @@ import { resolveTreePath } from "../../domain/apple/dyldPaths.js"; import { dylibResolutionInputSchema, dylibResolutionResultSchema, + machoImageSlices, traceDylibLoading, type DylibResolutionResult, type DylibTreeEntry, @@ -212,7 +213,9 @@ const executableRoots = async ( unclassified.push(entry.path); else if ( facts.status === "parsed" && - facts.slices.some(({ file_type: type }) => type === "execute") + machoImageSlices(facts).some( + ({ file_type: type }) => type === "execute", + ) ) roots.push(entry.path); }, diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts index 870e8aa082..2d8fe1272f 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.test.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts @@ -676,6 +676,23 @@ it("selects matching arm64e subtype slices when tracing a mixed FAT image", asyn ]); }); +it("keeps Apple's hidden ARM64 FAT row outside the raw declared slice list", async () => { + const armv7 = { type: 12, subtype: 9 }; + const bytes = Buffer.from( + fatImage([ + { cpu: armv7, bytes: machoImage({ cpu: armv7 }) }, + { cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) }, + ]), + ); + bytes.writeUInt32BE(1, 4); + const facts = await readMachoImage(readerOf(bytes), bytes.length); + expect(facts).toMatchObject({ + status: "parsed", + slices: [{ architecture: "arm" }], + hiddenSlices: [{ architecture: "arm64", cpu_type: CPU.arm64.type }], + }); +}); + describe("Mach-O load command reader: decodes thin-image load commands", () => { it("preserves leading U+FEFF in load-command strings", async () => { const image = machoImage({ diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts index 0e22cd4d28..f08b1bd302 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -8,7 +8,9 @@ import { machoLoadCommandAlignment, classifyFatContainerIdentity, readFatContainerHeader, + readFatSliceDeclarations, readMachoHeaderPrefixFacts, + validateFatSlice, validateFatSliceIdentity, type FatSliceDeclaration, } from "../../domain/apple/machoContainer.js"; @@ -68,6 +70,7 @@ const PLATFORM_WATCHOSSIMULATOR = 9; const CPU_TYPE_I386 = 7; const CPU_TYPE_X86_64 = 0x01000007; +const CPU_TYPE_ARM64 = 0x0100000c; /** * Legacy `LC_VERSION_MIN_*` commands predate explicit simulator platform @@ -214,9 +217,11 @@ export const readMachoImage = async ( } if (identity.state === "needs-slice-header") return { status: "not-mach-o" }; + const hiddenSlice = await readAppleHiddenArm64Slice(readAt, size, header); return { status: "parsed", slices: await readFatSlices(readAt, header.slices), + ...(hiddenSlice === null ? {} : { hiddenSlices: [hiddenSlice] }), }; } if (magic === MH_CIGAM || magic === MH_CIGAM_64) @@ -234,6 +239,54 @@ export const readMachoImage = async ( } }; +/** + * Apple lipo stores one hidden ARM64 row after the declared 32-bit FAT table. + * It remains separate from the raw nfat_arch declarations and is only admitted + * from the single table-to-first-slice slot cctools recognizes. + */ +const readAppleHiddenArm64Slice = async ( + readAt: ReadAt, + fileSize: number, + header: Extract< + ReturnType, + { state: "parsed" } + >, +): Promise => { + if (header.wide) return null; + const firstOffset = Math.min(...header.slices.map(({ offset }) => offset)); + const end = header.tableEnd + 20; + if (end > fileSize || end > firstOffset) return null; + const bytes = await readAt(header.tableEnd, 20); + if (bytes.byteLength < 20) + throw new MachoFormatIssue( + "malformed", + "Hidden ARM64 FAT row is truncated", + ); + const row = readFatSliceDeclarations(bytes, { + count: 1, + wide: false, + littleEndian: header.littleEndian, + }); + if (row.status !== "parsed") + throw new MachoFormatIssue("malformed", row.reason); + const declaration = { ...row.slices[0]!, index: header.count }; + if (declaration.cpuType !== CPU_TYPE_ARM64) return null; + const rangeIssue = validateFatSlice(declaration, fileSize, end); + if (rangeIssue !== null) throw new MachoFormatIssue("malformed", rangeIssue); + const slice = await readSlice(readAt, declaration.offset, declaration.size, { + fatCpuType: declaration.cpuType, + fatCpuSubtype: declaration.cpuSubtype, + alignmentExponent: declaration.alignmentExponent, + }); + const identityIssue = validateFatSliceIdentity(declaration, { + cpuType: slice.cpu_type, + cpuSubtype: slice.cpu_subtype, + }); + if (identityIssue !== null) + throw new MachoFormatIssue("malformed", identityIssue); + return { ...slice, hidden: true }; +}; + const readFatSlices = async ( readAt: ReadAt, declarations: readonly FatSliceDeclaration[], diff --git a/src/domain/apple/dylibResolution.test.ts b/src/domain/apple/dylibResolution.test.ts index 385144990a..8916dc7db4 100644 --- a/src/domain/apple/dylibResolution.test.ts +++ b/src/domain/apple/dylibResolution.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + machoImageSlices, traceDylibLoading, type DylibTreeEntry, type DylibTreeView, @@ -60,6 +61,36 @@ const parsed = (...slices: MachoSlice[]): MachoImageFacts => ({ const executable = (overrides: Partial = {}): MachoImageFacts => parsed(slice({ file_type: "execute", ...overrides })); +it("uses hidden ARM64 slices as selectable images while preserving their marker", async () => { + const declared = slice({ + architecture: "arm", + cpu_type: 12, + cpu_subtype: 9, + file_type: "execute", + }); + const hidden = slice({ + architecture: "arm64", + hidden: true, + file_type: "execute", + slice_offset: 8192, + }); + const facts: MachoImageFacts = { + status: "parsed", + slices: [declared], + hiddenSlices: [hidden], + }; + const trace = await traceDylibLoading(memoryView({ [MAIN]: facts }), { + roots: [MAIN], + architecture: "arm64", + }); + expect(machoImageSlices(facts)).toEqual([declared, hidden]); + expect(trace.roots).toEqual([{ image: MAIN, architecture: "arm64" }]); + expect(trace.coverage.roots_without_architecture).toEqual([]); + expect(trace.images[0]?.slices).toContainEqual( + expect.objectContaining({ architecture: "arm64", hidden: true }), + ); +}); + /** In-memory tree: files are images (or "data"), symlinks map to targets. */ const memoryView = ( files: Readonly>, diff --git a/src/domain/apple/dylibResolution.ts b/src/domain/apple/dylibResolution.ts index e9a5155965..25963be9ae 100644 --- a/src/domain/apple/dylibResolution.ts +++ b/src/domain/apple/dylibResolution.ts @@ -81,6 +81,7 @@ const dependencySchema = z.strictObject({ /** Observed dylib-loading facts of one Mach-O slice. */ export const machoSliceSchema = z.strictObject({ architecture: z.string().min(1), + hidden: z.boolean().exactOptional(), /** Physical identity observed in the thin header and, for FAT, its table. */ slice_offset: z.int().nonnegative(), slice_size: z.int().positive(), @@ -107,13 +108,26 @@ export type MachoSlice = z.infer; /** Header parse of one file; malformed and unsupported images keep their reason. */ export type MachoImageFacts = - | { readonly status: "parsed"; readonly slices: readonly MachoSlice[] } + | { + readonly status: "parsed"; + readonly slices: readonly MachoSlice[]; + /** Apple lipo's ARM64 compatibility row, stored beyond raw nfat_arch. */ + readonly hiddenSlices?: readonly MachoSlice[]; + } | { readonly status: "malformed" | "unsupported"; readonly reason: string; } | { readonly status: "not-mach-o" }; +/** Return every slice that can participate in architecture selection. */ +export const machoImageSlices = ( + facts: Extract, +): readonly MachoSlice[] => + facts.hiddenSlices === undefined + ? facts.slices + : [...facts.slices, ...facts.hiddenSlices]; + /** One observed path below the analyzed root. Symlink targets are not followed by the view. */ export type DylibTreeEntry = | { readonly kind: "file" | "directory" } @@ -350,7 +364,10 @@ const evaluateCandidate = async ( let outcome: Candidate["outcome"]; if (facts.status !== "parsed") outcome = facts.status; else { - const selection = compatibleSlice(facts.slices, context.processIdentity); + const selection = compatibleSlice( + machoImageSlices(facts), + context.processIdentity, + ); if (selection.kind === "unknown") outcome = "undetermined"; else if (selection.kind === "missing") outcome = "architecture-missing"; // Dependency commands require a dylib; generic dyld image loadability @@ -548,7 +565,7 @@ const resolveDependency = async ( resolved.image === null ? undefined : context.images.get(resolved.image); const selection = image?.status === "parsed" - ? compatibleSlice(image.slices, context.processIdentity) + ? compatibleSlice(machoImageSlices(image), context.processIdentity) : undefined; const slice = selection?.kind === "selected" ? selection.slice : undefined; return { @@ -607,7 +624,7 @@ const traceProcess = async ( const facts = context.images.get(image); const selection = facts?.status === "parsed" - ? compatibleSlice(facts.slices, context.processIdentity) + ? compatibleSlice(machoImageSlices(facts), context.processIdentity) : undefined; if (selection?.kind !== "selected") continue; const { slice } = selection; @@ -658,7 +675,9 @@ const projectTraceResult = (input: { ? null : facts.reason, slices: - facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [], + facts.status === "parsed" + ? machoImageSlices(facts).map(withoutDependencies) + : [], })), edges: [...input.edges], findings: deriveFindings(input.edges, input.rootSlices), @@ -719,7 +738,7 @@ export const traceDylibLoading = async ( const facts = await view.image(root); images.set(root, facts); if (facts.status !== "parsed") continue; - const slices = facts.slices.filter( + const slices = machoImageSlices(facts).filter( ({ architecture }) => request.architecture === undefined || architecture === request.architecture, @@ -784,6 +803,7 @@ const withoutDependencies = ( slice: MachoSlice, ): Omit => ({ architecture: slice.architecture, + ...(slice.hidden === undefined ? {} : { hidden: slice.hidden }), slice_offset: slice.slice_offset, slice_size: slice.slice_size, cpu_type: slice.cpu_type, diff --git a/src/domain/electron/electronFuses.ts b/src/domain/electron/electronFuses.ts index 5051d0fada..77cebd24b3 100644 --- a/src/domain/electron/electronFuses.ts +++ b/src/domain/electron/electronFuses.ts @@ -88,6 +88,7 @@ const electronFuseSliceSchema = z.strictObject({ file_offset: z.int().nonnegative(), size: z.int().nonnegative(), architecture: z.string().min(1).nullable(), + hidden: z.boolean().exactOptional(), sentinel_count: z.int().nonnegative(), sentinels: z.array(electronFuseSentinelSchema), }); diff --git a/src/domain/native/nativeInspection.ts b/src/domain/native/nativeInspection.ts index ca3637d0fc..8ee28abd16 100644 --- a/src/domain/native/nativeInspection.ts +++ b/src/domain/native/nativeInspection.ts @@ -27,6 +27,7 @@ const nativeCommandInvocationSchema = z.object({ const architectureSchema = z.object({ name: z.string(), + hidden: z.boolean().exactOptional(), cpu_type: z.string().nullable(), cpu_subtype: z.string().nullable(), file_offset: z diff --git a/src/electron/ElectronFuseProvider.test.ts b/src/electron/ElectronFuseProvider.test.ts index 77cb6f3c2c..aad51e7427 100644 --- a/src/electron/ElectronFuseProvider.test.ts +++ b/src/electron/ElectronFuseProvider.test.ts @@ -34,6 +34,36 @@ const inspect = async (path: string) => { return result.value; }; +it("preserves the hidden ARM64 marker through fuse scanning and reporting", async () => { + const root = await createTestTempDirectory( + "rea-electron-fuses-hidden-arm64-", + ); + const universal = universalFuseImage(); + // Model Apple's ARM32/hidden-ARM64 layout: keep both physical FAT rows, + // but declare only the visible ARM32 row in nfat_arch. + universal.bytes.writeUInt32BE(1, 4); + universal.bytes.writeUInt32BE(12, 8); + universal.bytes.writeUInt32BE(9, 12); + universal.bytes.writeUInt32LE(0xfeedface, universal.left.fileOffset); + universal.bytes.writeUInt32LE(12, universal.left.fileOffset + 4); + universal.bytes.writeUInt32LE(9, universal.left.fileOffset + 8); + const path = join(root, "hidden-arm64"); + await writeFile(path, universal.bytes); + + const report = await inspect(path); + expect(report.slices).toHaveLength(2); + expect(report.slices[0]).not.toHaveProperty("hidden"); + expect(report.slices[1]).toMatchObject({ + architecture: "arm64", + hidden: true, + file_offset: universal.right.fileOffset, + sentinel_count: 1, + sentinels: [ + { sentinel_offset: universal.right.sentinelOffset, complete: true }, + ], + }); +}); + describe("Electron fuse wire reading", () => { it("reads thin Mach-O, universal Mach-O, PE, and ELF wires", async () => { const root = await createTestTempDirectory("rea-electron-fuses-"); @@ -51,6 +81,7 @@ describe("Electron fuse wire reading", () => { await writeFile(join(root, "app.elf"), elf.bytes); const thinReport = await inspect(join(root, "thin")); + expect(thinReport.slices[0]).not.toHaveProperty("hidden"); expect(thinReport.artifact).toMatchObject({ format: "mach-o", bytes: thin.bytes.length, @@ -83,6 +114,8 @@ describe("Electron fuse wire reading", () => { ]); const universalReport = await inspect(join(root, "universal")); + for (const slice of universalReport.slices) + expect(slice).not.toHaveProperty("hidden"); expect(universalReport.artifact.format).toBe("mach-o-universal"); expect(universalReport.slices).toMatchObject([ { diff --git a/src/electron/ElectronFuseReader.ts b/src/electron/ElectronFuseReader.ts index ff87881ac6..603152e26a 100644 --- a/src/electron/ElectronFuseReader.ts +++ b/src/electron/ElectronFuseReader.ts @@ -16,6 +16,7 @@ import { type ElectronFuseSentinel, type ElectronFuseSlice, } from "../domain/electron/electronFuses.js"; +import { machoImageSlices } from "../domain/apple/dylibResolution.js"; import { RegularFileChangedError, sameRegularFileState, @@ -52,6 +53,7 @@ interface SliceRange { readonly fileOffset: number; readonly size: number; readonly architecture: string | null; + readonly hidden?: boolean; } /** The selected container is malformed or outside the supported formats. */ @@ -259,11 +261,12 @@ const identifyMacho = async ( ); } const format = isFat(head) ? "mach-o-universal" : "mach-o"; - const slices = facts.slices.map((slice, index) => ({ + const slices = machoImageSlices(facts).map((slice, index) => ({ index, fileOffset: slice.slice_offset, size: slice.slice_size, architecture: slice.architecture, + ...(slice.hidden === undefined ? {} : { hidden: slice.hidden }), })); assertNonOverlappingSlices(slices); return { @@ -340,6 +343,9 @@ const scanSlices = async ( file_offset: state.slice.fileOffset, size: state.slice.size, architecture: state.slice.architecture, + ...(state.slice.hidden === undefined + ? {} + : { hidden: state.slice.hidden }), sentinel_count: sentinels.length, sentinels, }); diff --git a/src/gpu/AmdGpuTarget.ts b/src/gpu/AmdGpuTarget.ts index 1f372dd9ca..f29c5e9689 100644 --- a/src/gpu/AmdGpuTarget.ts +++ b/src/gpu/AmdGpuTarget.ts @@ -1,14 +1,83 @@ const processor = String.raw`gfx[0-9a-z]+(?:-[0-9a-z]+)*`; -const targetIdSource = `${processor}(?:(?::[a-z0-9_-]+[+-])|(?:\\+[a-z0-9_-]+))*`; +const targetFeaturesSource = String.raw`(?:(?::[a-z0-9_-]+[+-])|(?:\+[a-z0-9_-]+))*`; +const targetIdSource = `${processor}${targetFeaturesSource}`; const targetId = new RegExp(`^${targetIdSource}$`, "iu"); const bundleTriple = /^(?:hip|hipv4)-(?:amdgcn|amdgpu(?:[0-9]+(?:\.[0-9a-f]+)?)?)-amd-amdhsa-/iu; const bundleTarget = new RegExp(`(?:--|-)(${targetIdSource})$`, "iu"); const noteTarget = new RegExp( - `^amdgcn-amd-amdhsa(?:--|-unknown-)(${targetIdSource})$`, + `^amdgcn-amd-amdhsa(?:--|-unknown-)((${processor})${targetFeaturesSource})$`, "iu", ); +// LLVM's EF_AMDGPU_MACH values occupy the low byte of ELF e_flags. Keep this +// table limited to processor names explicitly assigned by LLVM; feature bits +// outside the processor mask have separate, code-object-version semantics. +const ELF_PROCESSORS: Readonly> = { + 0x20: "gfx600", + 0x21: "gfx601", + 0x22: "gfx700", + 0x23: "gfx701", + 0x24: "gfx702", + 0x25: "gfx703", + 0x26: "gfx704", + 0x28: "gfx801", + 0x29: "gfx802", + 0x2a: "gfx803", + 0x2b: "gfx810", + 0x2c: "gfx900", + 0x2d: "gfx902", + 0x2e: "gfx904", + 0x2f: "gfx906", + 0x30: "gfx908", + 0x31: "gfx909", + 0x32: "gfx90c", + 0x33: "gfx1010", + 0x34: "gfx1011", + 0x35: "gfx1012", + 0x36: "gfx1030", + 0x37: "gfx1031", + 0x38: "gfx1032", + 0x39: "gfx1033", + 0x3a: "gfx602", + 0x3b: "gfx705", + 0x3c: "gfx805", + 0x3d: "gfx1035", + 0x3e: "gfx1034", + 0x3f: "gfx90a", + 0x41: "gfx1100", + 0x42: "gfx1013", + 0x43: "gfx1150", + 0x44: "gfx1103", + 0x45: "gfx1036", + 0x46: "gfx1101", + 0x47: "gfx1102", + 0x48: "gfx1200", + 0x49: "gfx1250", + 0x4a: "gfx1151", + 0x4c: "gfx942", + 0x4e: "gfx1201", + 0x4f: "gfx950", + 0x50: "gfx1310", + 0x51: "gfx9-generic", + 0x52: "gfx10-1-generic", + 0x53: "gfx10-3-generic", + 0x54: "gfx11-generic", + 0x55: "gfx1152", + 0x57: "gfx1154", + 0x58: "gfx1153", + 0x59: "gfx12-generic", + 0x5a: "gfx1251", + 0x5b: "gfx12-5-generic", + 0x5c: "gfx1172", + 0x5d: "gfx1170", + 0x5e: "gfx1171", + 0x5f: "gfx9-4-generic", + 0x62: "gfx11-7-generic", + 0x63: "gfx13-generic", + 0xeb: "gfx1250-strict", +}; + /** Target IDs retain the processor spelling and any declared feature modifiers. */ export const parseAmdGpuTargetId = (value: string): string | null => targetId.test(value) ? value : null; @@ -23,6 +92,16 @@ export const readAmdGpuBundleTarget = (name: string): string | null => { return target === undefined ? null : parseAmdGpuTargetId(target); }; -/** Extract the processor identity from the two LLVM AMDGPU note spellings. */ -export const readAmdGpuNoteTargetKey = (target: string): string | null => - noteTarget.exec(target)?.[1] ?? null; +/** Preserve the full target ID separately from its processor identity. */ +export const readAmdGpuNoteTarget = ( + target: string, +): { readonly key: string; readonly processor: string } | null => { + const match = noteTarget.exec(target); + return match?.[1] === undefined || match[2] === undefined + ? null + : { key: match[1], processor: match[2] }; +}; + +/** Decode the processor field from AMDGPU ELF e_flags (EF_AMDGPU_MACH). */ +export const readAmdGpuElfProcessor = (flags: number): string | null => + ELF_PROCESSORS[flags & 0xff] ?? null; diff --git a/src/gpu/GpuElfKernels.ts b/src/gpu/GpuElfKernels.ts index 8aa8f69883..968f4e5b46 100644 --- a/src/gpu/GpuElfKernels.ts +++ b/src/gpu/GpuElfKernels.ts @@ -1,5 +1,8 @@ import { readCudaTarget } from "./CudaTarget.js"; -import { readAmdGpuNoteTargetKey } from "./AmdGpuTarget.js"; +import { + readAmdGpuElfProcessor, + readAmdGpuNoteTarget, +} from "./AmdGpuTarget.js"; import type { GpuPayloadTarget } from "./GpuObject.js"; import { err, ok, type Result } from "../domain/result.js"; import { @@ -375,16 +378,36 @@ export const readAmdElfMetadata = async ( const targets = new Set(), limitations: string[] = []; const notes: AmdElfMetadata["notes"] = []; + let processor: string | null = null; let retainedBytes = 0; const observed = (): AmdElfMetadata => { - const target = targets.size === 1 ? [...targets][0]! : null; + const noteTarget = targets.size === 1 ? [...targets][0]! : null; + const noteIdentity = + noteTarget === null ? null : readAmdGpuNoteTarget(noteTarget); + const noteConflict = targets.size > 1; + const processorConflict = + processor !== null && + noteIdentity !== null && + processor !== noteIdentity.processor.toLowerCase(); + const conflict = noteConflict || processorConflict; + const limitation = noteConflict + ? "AMDGPU notes declare conflicting processor targets; processor identity is unknown" + : processorConflict + ? "AMDGPU ELF processor flags conflict with target note metadata; processor identity is unknown" + : null; + if (limitation !== null && !limitations.includes(limitation)) + limitations.push(limitation); + const target = conflict ? null : (noteTarget ?? processor); // LLVM notes use both the empty and explicit `unknown` environment forms. - const target_key = target === null ? null : readAmdGpuNoteTargetKey(target); + const target_key = conflict + ? null + : (noteIdentity?.key ?? (noteTarget === null ? processor : null)); return { target, target_key, limitations, notes }; }; try { if (directory.machine !== 224) rejectGpu("AMDGPU metadata requires an AMDGPU ELF machine"); + processor = readAmdGpuElfProcessor(directory.flags); for (const section of directory.sections) { if (section.type !== 7) continue; const sectionSize = gpuCount(section.size); @@ -446,7 +469,7 @@ const amdKernels = ( prepared: AmdElfMetadata, onKernel: (kernel: GpuKernel) => void, onTarget?: (target: GpuPayloadTarget) => void, -): { limitations: string[]; coverage: "complete" } => { +): { limitations: string[]; coverage: "complete" | "partial" } => { onTarget?.({ value: prepared.target, key: prepared.target_key, @@ -509,6 +532,6 @@ const amdKernels = ( } return { limitations: prepared.limitations, - coverage: "complete", + coverage: prepared.limitations.length === 0 ? "complete" : "partial", }; }; diff --git a/src/gpu/GpuInventory.ts b/src/gpu/GpuInventory.ts index 9ac8a60c8e..8926f0e66e 100644 --- a/src/gpu/GpuInventory.ts +++ b/src/gpu/GpuInventory.ts @@ -472,6 +472,7 @@ export class GpuInventory { )); const inspected = await readAmdElfMetadata(child, native); const prepared = inspected.ok ? inspected.value : inspected.error.partial; + if (prepared.limitations.length > 0) this.discovery = "partial"; this.#target(row, { value: prepared.target, key: prepared.target_key, @@ -481,10 +482,15 @@ export class GpuInventory { this.discovery = "partial"; if (this.input.analysis === "kernels" && this.#selected(row)) await this.#elfFacts(child, row, "amdgpu", prepared); + else this.#objectLimitations(row, prepared.limitations); this.#retainFailure(row, inspected.error.cause); return; } + const payloadSelected = + this.input.analysis === "kernels" && this.#selected(row); await this.#payload(source, row, prepared); + if (!payloadSelected && prepared.limitations.length > 0) + this.#objectLimitations(row, prepared.limitations); } catch (cause) { this.discovery = "partial"; this.#retainFailure(row, cause); diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index a8dcde33a9..da6a488fa2 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -120,6 +120,33 @@ const fixture = ( }; const provenance = { path: "/fixture/app", sha256: "a".repeat(64) }; +it("retains Objective-C++ template commas in decoded property facts", () => { + const attributes = "T{vector=},N,V_position"; + const result = decodeAppleDispatchMetadata( + machoWithPropertyAttributes(attributes), + 100, + provenance, + ); + expect(result.objc_classes[0]?.properties).toEqual( + ["first", "second"].map((name) => ({ + name, + type_encoding: "{vector=}", + atomicity: "nonatomic", + attributes: [ + { name: "T", value: "{vector=}" }, + { name: "N", value: "" }, + { name: "V", value: "_position" }, + ], + is_readonly: false, + getter: null, + setter: null, + })), + ); + expect( + result.coverage.find(({ facet }) => facet === "objc_properties_categories"), + ).toMatchObject({ status: "complete" }); +}); + it("retains duplicate encoded method candidates without choosing one class-method address", () => { const bytes = objcRecordBudgetMacho("class", [0x700, 0x720]); const result = decodeAppleDispatchMetadata(bytes, 100, provenance); diff --git a/src/native/AppleMachoFixups.test.ts b/src/native/AppleMachoFixups.test.ts index 41f9c8b4ed..6e0ca4ac91 100644 --- a/src/native/AppleMachoFixups.test.ts +++ b/src/native/AppleMachoFixups.test.ts @@ -1543,6 +1543,23 @@ describe("Objective-C property attributes", () => { expect(parsePropertyAttributes("count", "R").atomicity).toBeNull(); }); + it("keeps template argument commas inside an Objective-C++ type", () => { + expect( + parsePropertyAttributes( + "position", + "T{vector=},N,V_position", + ), + ).toMatchObject({ + type_encoding: "{vector=}", + atomicity: "nonatomic", + attributes: [ + { name: "T", value: "{vector=}" }, + { name: "N", value: "" }, + { name: "V", value: "_position" }, + ], + }); + }); + it("establishes no defaults from an unfinished quoted class name", () => { // `,N,R` here lie inside the unterminated quote, so they are not flags. expect(parsePropertyAttributes("title", 'T@"NSString,N,R')).toMatchObject({ @@ -1555,6 +1572,16 @@ describe("Objective-C property attributes", () => { is_readonly: true, }); }); + + it("keeps defaults unknown when a template argument is unfinished", () => { + expect( + parsePropertyAttributes("position", "T{vector { diff --git a/src/native/AppleObjcProperties.ts b/src/native/AppleObjcProperties.ts index 4c820c72f0..9c95f6a83c 100644 --- a/src/native/AppleObjcProperties.ts +++ b/src/native/AppleObjcProperties.ts @@ -22,9 +22,9 @@ export const parsePropertyAttributes = ( ): ObjcProperty => decodePropertyAttributes(name, attributes).property; /** - * Decode one attribute string. A quoted class name that never closes leaves - * the string incomplete: later attributes may be inside the unfinished type, - * so a missing `N` or `R` does not establish the Objective-C defaults. + * Decode one attribute string. An unfinished quoted or templated type leaves + * later attributes ambiguous, so a missing `N` or `R` does not establish the + * Objective-C defaults. */ const decodePropertyAttributes = ( name: string, @@ -68,8 +68,8 @@ const decodePropertyAttributes = ( }; /** - * Split on commas that are outside a quoted class name in the type encoding, - * and report whether every quote closed. + * Split on separators outside quoted names and C++ template arguments in the + * type attribute, and report whether those constructs are complete. */ const splitAttributes = ( attributes: string, @@ -77,9 +77,19 @@ const splitAttributes = ( const tokens: string[] = []; let current = ""; let quoted = false; + let templateDepth = 0; for (const character of attributes) { if (character === '"') quoted = !quoted; - if (character === "," && !quoted) { + const inTypeAttribute = current.startsWith("T"); + if (!quoted && inTypeAttribute && character === "<") templateDepth += 1; + else if ( + !quoted && + inTypeAttribute && + character === ">" && + templateDepth > 0 + ) + templateDepth -= 1; + if (character === "," && !quoted && templateDepth === 0) { tokens.push(current); current = ""; } else current += character; @@ -87,7 +97,7 @@ const splitAttributes = ( tokens.push(current); return { tokens: tokens.filter((token) => token.length > 0), - complete: !quoted, + complete: !quoted && templateDepth === 0, }; }; @@ -139,7 +149,7 @@ export const readObjcPropertyList = ( if (!decoded.complete) issues.push({ code: "property_attributes_incomplete", - message: `property ${name} has an unterminated quoted class name in its type attribute, so atomicity and unestablished read-only state are unknown`, + message: `property ${name} has an incomplete quoted name or template argument in its type attribute, so atomicity and unestablished read-only state are unknown`, location, }); } catch (cause: unknown) { diff --git a/src/native/parsers/lipo.test.ts b/src/native/parsers/lipo.test.ts index b5c97a415f..7b3aa19a57 100644 --- a/src/native/parsers/lipo.test.ts +++ b/src/native/parsers/lipo.test.ts @@ -21,6 +21,32 @@ architecture x86_64 `; describe("lipo architecture facts", () => { + it("preserves Apple's declared and hidden ARM64 architecture scopes", () => { + const output = parseLipoOutput(`fat_magic 0xcafebabe +nfat_arch 1 (+1 hidden) +architecture armv7 + cputype CPU_TYPE_ARM + cpusubtype CPU_SUBTYPE_ARM_V7 + offset 4096 + size 28 + align 2^12 (4096) +architecture arm64 (hidden) + cputype CPU_TYPE_ARM64 + cpusubtype CPU_SUBTYPE_ARM64_ALL + offset 8192 + size 32 + align 2^12 (4096) +`); + expect(output).toMatchObject({ + declaredArchitectureCount: 1, + hiddenArchitectureCount: 1, + architectures: [ + { name: "armv7", hidden: false }, + { name: "arm64", hidden: true, file_offset: 8192, size: 32 }, + ], + }); + }); + it("normalizes symbolic CPU fields and arm64e capability bits", () => { const { format, architectures } = parseLipoOutput(`Fat header in: /bin/ls fat_magic 0xcafebabe @@ -189,9 +215,12 @@ describe("lipo FAT operand framing", () => { expect(parseLipoOutput(output, path)).toEqual({ format: "thin", + declaredArchitectureCount: null, + hiddenArchitectureCount: null, architectures: [ { name: "arm64", + hidden: false, cpu_type: null, cpu_subtype: null, cpu_type_code: null, diff --git a/src/native/parsers/lipo.ts b/src/native/parsers/lipo.ts index 5c60315aa5..1cc0cb2bd1 100644 --- a/src/native/parsers/lipo.ts +++ b/src/native/parsers/lipo.ts @@ -3,6 +3,7 @@ import { machoCpuSubtypeBase } from "../../domain/apple/machoCpuIdentity.js"; const architectureSchema = z.object({ name: z.string().min(1), + hidden: z.boolean(), cpu_type: z.string().nullable(), cpu_subtype: z.string().nullable(), cpu_type_code: z.int().nonnegative().nullable(), @@ -18,6 +19,9 @@ export type LipoArchitecture = z.infer; interface LipoOutput { readonly format: "thin" | "fat"; + /** `nfat_arch` counts visible rows; Apple may append one hidden ARM64 row. */ + readonly declaredArchitectureCount: number | null; + readonly hiddenArchitectureCount: number | null; readonly architectures: LipoArchitecture[]; } @@ -45,9 +49,12 @@ export const parseLipoOutput = ( if (thinArchitecture !== undefined) return { format: "thin", + declaredArchitectureCount: null, + hiddenArchitectureCount: null, architectures: [ architectureSchema.parse({ name: thinArchitecture, + hidden: false, cpu_type: null, cpu_subtype: null, cpu_type_code: null, @@ -61,6 +68,8 @@ export const parseLipoOutput = ( ], }; const architectures: LipoArchitecture[] = []; + let declaredArchitectureCount: number | null = null; + let hiddenArchitectureCount: number | null = null; let current: Record | undefined; const flush = (): void => { if (current === undefined) return; @@ -70,7 +79,8 @@ export const parseLipoOutput = ( architectureSchema.parse({ name: name.includes(" is architecture: ") ? (name.split(" is architecture: ").at(-1) ?? name) - : name, + : name.replace(/ \(hidden\)$/u, ""), + hidden: name.endsWith(" (hidden)"), cpu_type: current.cputype ?? null, cpu_subtype: current.cpusubtype ?? null, cpu_type_code: cpuTypeCode(current.cputype), @@ -89,6 +99,12 @@ export const parseLipoOutput = ( }; for (const rawLine of output.split(/\r?\n/u)) { const line = rawLine.trim(); + const count = /^nfat_arch\s+(\d+)(?:\s+\(\+(\d+) hidden\))?$/u.exec(line); + if (count?.[1] !== undefined) { + declaredArchitectureCount = integer(count[1]); + hiddenArchitectureCount = integer(count[2] ?? "0"); + continue; + } if (line.startsWith("Non-fat file:")) { flush(); current = { "Non-fat file": line }; @@ -111,7 +127,12 @@ export const parseLipoOutput = ( flush(); if (architectures.length === 0) throw new TypeError("lipo output contained no architectures"); - return { format: "fat", architectures }; + return { + format: "fat", + declaredArchitectureCount, + hiddenArchitectureCount, + architectures, + }; }; const integer = (value: string | undefined): number | null => { diff --git a/tests/boundary/gpu/codeObjects.test.ts b/tests/boundary/gpu/codeObjects.test.ts index 84c5009780..c7360c89f4 100644 --- a/tests/boundary/gpu/codeObjects.test.ts +++ b/tests/boundary/gpu/codeObjects.test.ts @@ -251,6 +251,75 @@ it("discovers standalone AMD targets before exact selection", async () => { expect(skipped.value.kernels).toEqual([]); expect(skipped.value.coverage.payloads).toBe("partial"); }); +it("derives V3 AMDGPU processor identity from ELF flags when the note omits it", async () => { + const bytes = amdFixture(false, undefined, null); + // EF_AMDGPU_MACH=0x2c identifies gfx900; high bits include feature flags. + bytes.writeUInt32LE(0x12c, 48); + const inventory = await inspect(bytes, { analysis: "inventory" }); + if (!inventory.ok) throw inventory.error; + expect(inventory.value.objects[0]).toMatchObject({ + target: "gfx900", + target_consistency: "single-source", + inspection: "inventory", + }); + const selected = await inspect(bytes, { targets: ["gfx900"] }); + if (!selected.ok) throw selected.error; + expect(selected.value.kernels).toHaveLength(1); + expect(selected.value.kernels[0]).toMatchObject({ parameter_bytes: 12 }); +}); +it("keeps AMDGPU processor identity unknown when ELF flags are unassigned", async () => { + const bytes = amdFixture(false, undefined, null); + bytes.writeUInt32LE(0x17f, 48); + const result = await inspect(bytes, { analysis: "inventory" }); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: null, + target_consistency: "unknown", + inspection: "inventory", + }); +}); +it.each(["gfx900:xnack+", "gfx900+xnack", "gfx900:sramecc-:xnack+"])( + "preserves matching AMDGPU note features with ELF processor flags (%s)", + async (target) => { + const noteTarget = `amdgcn-amd-amdhsa--${target}`; + const payload = amdFixture(false, undefined, noteTarget); + payload.writeUInt32LE(0x12c, 48); + const result = await inspect( + hipFixture(payload, `hipv4-amdgcn-amd-amdhsa--${target}`), + { targets: [target] }, + ); + if (!result.ok) throw result.error; + expect( + result.value.objects.find(({ kind }) => kind === "amdgpu"), + ).toMatchObject({ + target, + payload_target: noteTarget, + target_consistency: "consistent", + inspection: "complete", + }); + expect(result.value.kernels).toHaveLength(1); + expect(result.value.coverage.payloads).toBe("complete"); + }, +); +it("does not choose between conflicting AMDGPU note and ELF processor identities", async () => { + const bytes = amdFixture( + false, + undefined, + "amdgcn-amd-amdhsa--gfx900:xnack+", + ); + bytes.writeUInt32LE(0x141, 48); // EF_AMDGPU_MACH gfx1100 + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: null, + target_consistency: "unknown", + inspection: "partial", + }); + expect(result.value.objects[0]!.limitations.join(" ")).toContain( + "processor flags conflict with target note metadata", + ); + expect(result.value.kernels).toHaveLength(1); +}); it.each(["match", "conflict", "feature-unknown"] as const)( "compares AMDGPU targets with an explicit unknown environment (%s)", async (comparison) => { @@ -320,10 +389,9 @@ it.each(["gfx11-generic", "gfx9-4-generic"] as const)( "preserves and exactly selects the generic AMDGPU processor %s", async (processor) => { const noteTarget = `amdgcn-amd-amdhsa--${processor}`; - const bytes = hipFixture( - amdFixture(false, undefined, noteTarget), - `hipv4-amdgcn-amd-amdhsa--${processor}`, - ); + const payload = amdFixture(false, undefined, noteTarget); + payload.writeUInt32LE(processor === "gfx11-generic" ? 0x54 : 0x5f, 48); + const bytes = hipFixture(payload, `hipv4-amdgcn-amd-amdhsa--${processor}`); const result = await inspect(bytes, { targets: [processor] }); if (!result.ok) throw result.error; expect( @@ -363,7 +431,7 @@ it("bounds standalone AMD note discovery before retaining metadata", async () => ); if (!result.ok) throw result.error; expect(result.value.objects[0]).toMatchObject({ - target: null, + target: "gfx900", inspection: "capacity", }); expect(result.value.objects[0]!.limitations).toContain( diff --git a/tests/fixtures/gpu/codeObjects.ts b/tests/fixtures/gpu/codeObjects.ts index 151f945f9c..7a39a4a562 100644 --- a/tests/fixtures/gpu/codeObjects.ts +++ b/tests/fixtures/gpu/codeObjects.ts @@ -213,7 +213,7 @@ export const cubinFixture = ( export const amdFixture = ( hidden = false, trailingNote?: Buffer, - target = "amdgcn-amd-amdhsa--gfx900", + target: string | null = "amdgcn-amd-amdhsa--gfx900", ): Buffer => { const args = [ { ".offset": 0, ".size": 8, ".value_kind": "global_buffer" }, @@ -225,7 +225,7 @@ export const amdFixture = ( ]; const metadata = Buffer.from( encode({ - "amdhsa.target": target, + ...(target === null ? {} : { "amdhsa.target": target }), "amdhsa.version": [1, 1], "amdhsa.kernels": [ { diff --git a/tests/fixtures/objcPropertyMacho.ts b/tests/fixtures/objcPropertyMacho.ts index 49bfe66743..b957dad56e 100644 --- a/tests/fixtures/objcPropertyMacho.ts +++ b/tests/fixtures/objcPropertyMacho.ts @@ -5,10 +5,17 @@ */ export const machoWithPropertyAttributes = ( attributes: string, - { firstName = "first", secondNameOffset = 0x640 } = {}, + { + firstName = "first", + secondNameOffset, + }: { firstName?: string; secondNameOffset?: number } = {}, ): Buffer => { const bytes = Buffer.alloc(4096); const base = 0x100000000n; + const firstNameData = Buffer.from(`${firstName}\0`); + const attributeData = Buffer.from(`${attributes}\0`); + const attributeOffset = 0x600 + firstNameData.length; + const secondNameDataOffset = attributeOffset + attributeData.length; const u32 = (at: number, value: number) => bytes.writeUInt32LE(value, at); const pointer = (at: number, offset: number) => bytes.writeBigUInt64LE(base + BigInt(offset), at); @@ -41,11 +48,11 @@ export const machoWithPropertyAttributes = ( u32(0x500, 16); u32(0x504, 2); pointer(0x508, 0x600); - pointer(0x510, 0x620); - pointer(0x518, secondNameOffset); - pointer(0x520, 0x620); - bytes.write(`${firstName}\0`, 0x600); - bytes.write(`${attributes}\0`, 0x620); - bytes.write("second\0", 0x640); + pointer(0x510, attributeOffset); + pointer(0x518, secondNameOffset ?? secondNameDataOffset); + pointer(0x520, attributeOffset); + firstNameData.copy(bytes, 0x600); + attributeData.copy(bytes, attributeOffset); + bytes.write("second\0", secondNameDataOffset); return bytes; };