diff --git a/docs/providers/ghidra-provider.md b/docs/providers/ghidra-provider.md index f7553abea8..3b6cd70886 100644 --- a/docs/providers/ghidra-provider.md +++ b/docs/providers/ghidra-provider.md @@ -33,6 +33,11 @@ undefined. Installation `application.properties` has a fixed 1 MiB read allowanc with no runtime override. The failure retains that allowance and directs recovery at the selected installation metadata. +Xtensa ELF files (`EM_XTENSA = 94`) use the header-selected Ghidra import +language and report the canonical `xtensa` architecture in CLI/MCP results. +Processor extensions and calling-convention compatibility remain specific to +the selected Ghidra language; family recognition alone does not establish them. + Retained process diagnostics can report termination by `SIGKILL`; the signal alone cannot establish an OOM kill. REA gives neutral recovery guidance and preserves the signal. Confirm resource causes from process output or host/container records; diff --git a/scripts/verify/advertised-schemas/summary.json b/scripts/verify/advertised-schemas/summary.json index d4a21d49f8..300e0530d6 100644 --- a/scripts/verify/advertised-schemas/summary.json +++ b/scripts/verify/advertised-schemas/summary.json @@ -964,7 +964,7 @@ }, "open_binary": { "input compact": "706f1985ca397688", - "output compact": "4cbb79604f9b9e14" + "output compact": "52860f8f8ed909d0" }, "open_bundle_resources": { "input compact": "18d266261424a97c", diff --git a/src/domain/binaryTarget.test.ts b/src/domain/binaryTarget.test.ts index c22a49dd45..1e26c4cae1 100644 --- a/src/domain/binaryTarget.test.ts +++ b/src/domain/binaryTarget.test.ts @@ -23,6 +23,7 @@ describe("executable header parsing", () => { [elf(1, 1, 62), "elf", "x86_64"], [elf(2, 2, 183), "elf", "arm64"], [elf(1, 1, 164), "elf", "hexagon"], + [elf(1, 1, 94), "elf", "xtensa"], [thinMach(0xfeedfacf, 0x0100000c), "mach-o", "arm64"], [thinMach(0xcefaedfe, 0x01000007), "mach-o", "x86_64"], [thinMach(0xcefaedfe, 0x0200000c), "mach-o", "arm64_32"], diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index 4e3ac41753..b15c2eaa77 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -70,6 +70,11 @@ export type ExecutableMetadata = readonly architecture: "hexagon"; readonly availableArchitectures: readonly "hexagon"[]; } + | { + readonly format: "elf"; + readonly architecture: "xtensa"; + readonly availableArchitectures: readonly "xtensa"[]; + } | { readonly format: "mach-o"; readonly architecture: "arm64_32"; @@ -534,6 +539,12 @@ const parseElf = ( architecture, availableArchitectures: [architecture], }); + if (architecture === "xtensa") + return ok({ + format: "elf", + architecture, + availableArchitectures: [architecture], + }); return ok({ format: "elf", architecture, diff --git a/src/domain/binaryTargetTypes.ts b/src/domain/binaryTargetTypes.ts index 755912e203..7e6ed84afa 100644 --- a/src/domain/binaryTargetTypes.ts +++ b/src/domain/binaryTargetTypes.ts @@ -14,6 +14,7 @@ export const BINARY_ARCHITECTURES = [ "powerpc", "m68k", "hexagon", + "xtensa", ] as const; export type BinaryArchitecture = (typeof BINARY_ARCHITECTURES)[number]; @@ -62,10 +63,19 @@ export const isElfMetadataArchitecture = ( ): architecture is ElfMetadataArchitecture => (ELF_METADATA_ARCHITECTURES as readonly string[]).includes(architecture); -/** Recognized family with no ELF metadata member; the header alone is the fact. */ +/** Families that require a specialized target variant instead of generic headers. */ +export const HEADER_ARCHITECTURE_EXCLUSIONS = [ + ...ELF_METADATA_ARCHITECTURES, + ...RAW_ONLY_ARCHITECTURES, + "hexagon", + "xtensa", + "arm64_32", +] as const satisfies readonly BinaryArchitecture[]; + +/** Recognized family with no specialized target member; the header alone is the fact. */ export type HeaderArchitecture = Exclude< BinaryArchitecture, - ElfMetadataArchitecture | RawOnlyArchitecture | "hexagon" | "arm64_32" + (typeof HEADER_ARCHITECTURE_EXCLUSIONS)[number] >; /** Raw ELF facts; family recognition does not establish ISA or ABI support. */ @@ -212,6 +222,14 @@ export type BinaryTarget = readonly executableRole?: never; readonly managed?: never; }) + | (BinaryTargetIdentity & { + readonly kind: "executable"; + readonly format: "elf"; + readonly architecture: "xtensa"; + readonly availableArchitectures: readonly "xtensa"[]; + readonly executableRole?: never; + readonly managed?: never; + }) | (BinaryTargetIdentity & { readonly kind: "executable"; readonly format: "mach-o"; diff --git a/src/domain/elfFixedHeader.ts b/src/domain/elfFixedHeader.ts index fdd44366d8..4fc5448059 100644 --- a/src/domain/elfFixedHeader.ts +++ b/src/domain/elfFixedHeader.ts @@ -130,6 +130,8 @@ export const elfArchitecture = ( return "arm"; case 164: // EM_HEXAGON: Qualcomm Hexagon DSP (QDSP6). return "hexagon"; + case 94: // EM_XTENSA: Tensilica Xtensa. + return "xtensa"; case 183: return "arm64"; case 243: // EM_RISCV: XLEN, byte order and e_flags remain separate ELF facts. diff --git a/src/domain/liveBinaryTargetSummary.ts b/src/domain/liveBinaryTargetSummary.ts index 06ba951fc7..5100532b1f 100644 --- a/src/domain/liveBinaryTargetSummary.ts +++ b/src/domain/liveBinaryTargetSummary.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { BINARY_ARCHITECTURES, - ELF_METADATA_ARCHITECTURES, + HEADER_ARCHITECTURE_EXCLUSIONS, RAW_ONLY_ARCHITECTURES, } from "./binaryTargetTypes.js"; import { digestSchema } from "./digests.js"; @@ -13,12 +13,9 @@ import { import { ZIP_PACKAGE_FORMATS } from "./zipPackageFormat.js"; const architectureSchema = z.enum(BINARY_ARCHITECTURES); -const headerArchitectureSchema = architectureSchema.exclude([ - ...ELF_METADATA_ARCHITECTURES, - ...RAW_ONLY_ARCHITECTURES, - "hexagon", - "arm64_32", -]); +const headerArchitectureSchema = architectureSchema.exclude( + HEADER_ARCHITECTURE_EXCLUSIONS, +); const identity = { path: z.string(), sha256: digestSchema }; const executable = { ...identity, kind: z.literal("executable") }; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 68dd3417e3..2bae9581f3 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -617,6 +617,35 @@ describe("Ghidra provider", () => { }); }); +describe("Ghidra Xtensa ELF support", () => { + it("admits parsed Xtensa targets through profile selection", async () => { + const parsed = parseExecutableHeader(elf(1, 1, 94), "x64"); + if (!parsed.ok) throw new Error("Expected valid Xtensa ELF target"); + const target: BinaryTarget = { + path: "/tmp/esp32s3.elf", + sha256: "a".repeat(64), + kind: "executable", + ...parsed.value, + }; + const ghidra = provider(); + expect(ghidra.inspectTargetSupport(target)).toMatchObject({ + status: "supported", + }); + const resolved = await ghidra.resolveAnalysisProfile(target); + expect(resolved.ok && resolved.value).toMatchObject({ + profile: { + parameters: { + target_format: "elf", + architecture: "xtensa", + loader: "auto-from-header", + language_id: "auto-from-header", + compiler_spec_id: "auto-default", + }, + }, + }); + }); +}); + describe("Ghidra raw-binary profile", () => { it("admits raw 6502 on Windows and binds its 16-bit language without mutation authority", async () => { const ghidra = provider(installationHost("win32")); @@ -749,6 +778,7 @@ describe("Ghidra Mach-O slice support", () => { "riscv", "powerpc", "hexagon", + "xtensa", "6502", "m68k", ]), diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index de473c0bfb..dbb7611403 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -107,6 +107,7 @@ const SUPPORTED_ARCHITECTURES = new Set([ "powerpc", "m68k", "hexagon", + "xtensa", ]); /** Names every admitted family, so the refusal cannot drift from the set. */ diff --git a/src/hopper/HopperAnalysisProfile.ts b/src/hopper/HopperAnalysisProfile.ts index 509d6e0737..a798265e64 100644 --- a/src/hopper/HopperAnalysisProfile.ts +++ b/src/hopper/HopperAnalysisProfile.ts @@ -207,6 +207,7 @@ const resolveHopperAnalysisProfileInScope = async ( export type HopperDeclinedArchitecture = | ElfMetadataArchitecture | "hexagon" + | "xtensa" | "arm64_32"; export const isHopperDeclinedArchitecture = ( @@ -214,6 +215,7 @@ export const isHopperDeclinedArchitecture = ( ): architecture is HopperDeclinedArchitecture => isElfMetadataArchitecture(architecture) || architecture === "hexagon" || + architecture === "xtensa" || architecture === "arm64_32"; /** diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index be44411491..d376bf0a2e 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -194,6 +194,27 @@ describe("Hopper provider target support", () => { }); }); +describe("Hopper Xtensa support", () => { + it("declines Xtensa with the Ghidra-directed reason", () => { + const config = parseConfig({}); + if (!config.ok) throw new Error("expected valid configuration"); + const provider = new HopperProvider(config.value, silentLogger, {}); + const target = { + path: "/tmp/esp32s3.elf", + sha256: "a".repeat(64), + kind: "executable", + format: "elf", + architecture: "xtensa", + availableArchitectures: ["xtensa"], + } as const satisfies BinaryTarget; + expect(provider.inspectTargetSupport(target)).toMatchObject({ + status: "unsupported", + code: "architecture_unsupported", + reason: hopperDeclinedArchitectureReason("xtensa"), + }); + }); +}); + describe("Hopper relationship coverage", () => { it.each([ ["procedure_callers", ["callers"]], diff --git a/tests/boundary/mcp/liveBinaryTargetSummary.test.ts b/tests/boundary/mcp/liveBinaryTargetSummary.test.ts index ddc799365a..f1e1963746 100644 --- a/tests/boundary/mcp/liveBinaryTargetSummary.test.ts +++ b/tests/boundary/mcp/liveBinaryTargetSummary.test.ts @@ -52,6 +52,12 @@ const targets: readonly BinaryTarget[] = [ architecture: "hexagon", availableArchitectures: ["hexagon"], }, + { + ...executable, + format: "elf", + architecture: "xtensa", + availableArchitectures: ["xtensa"], + }, { ...executable, format: "mach-o", @@ -111,6 +117,9 @@ const targets: readonly BinaryTarget[] = [ })), ]; const impossible = [ + ...(["pe", "mach-o", "mach-o-universal", "dos-mz", "dos-com"] as const).map( + (format) => ({ kind: "executable", format, architecture: "xtensa" }), + ), { kind: "database", format: "elf", architecture: "x86" }, { kind: "executable", format: "zip", architecture: "arm64" }, { kind: "archive", format: "zip", architecture: "arm64" }, @@ -237,16 +246,7 @@ it.each(["draft-2020-12", "draft-07"] as const)( toolContract("binary_session"), )["~standard"].jsonSchema.output({ target }); const validate = new AjvJsonSchemaValidator().getValidator(advertised); - // One representative per distinct projection shape; the complete target matrix is above. - for (const format of [ - "elf", - "raw-binary", - "gamecube-dol", - "zip", - ] as const) { - const selected = targets.find((candidate) => candidate.format === format); - if (selected === undefined) - throw new Error(`Missing canonical fixture: ${format}`); + for (const selected of targets) { const value = { result: { ...sessionFacts, ...binaryTargetSummary(selected) }, }; @@ -255,7 +255,7 @@ it.each(["draft-2020-12", "draft-07"] as const)( ); expect(validate(value).valid).toBe(true); } - for (const classification of impossible.slice(0, 4)) { + for (const classification of impossible) { const value = { result: { ...sessionFacts, ...identity, ...classification }, };