#!/usr/bin/env node import { execFile } from "node:child_process"; import { access, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js"; import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; import { GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS } from "../dist/ghidra/GhidraDefaults.js"; import { parseGhidraFunctionInput, parseGhidraFunctionResult, } from "../dist/ghidra/GhidraFunctionValues.js"; import { parseGhidraInventoryResult } from "../dist/ghidra/GhidraInventoryValues.js"; import { inspectGhidraInstallation, SUPPORTED_GHIDRA_VERSION, } from "../dist/ghidra/GhidraInstallation.js"; import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js"; import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js"; import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js"; import { parseBinaryTarget } from "../dist/domain/binaryTarget.js"; const exec = promisify(execFile); const installDir = process.env.GHIDRA_INSTALL_DIR; if (installDir === undefined || !isAbsolute(installDir)) throw new Error( "Set GHIDRA_INSTALL_DIR to the absolute root of an extracted Ghidra 12.1.2 release.", ); const installation = inspectGhidraInstallation({ installDir, ...(process.env.JAVA_HOME === undefined ? {} : { javaHome: process.env.JAVA_HOME }), }); if (!installation.available || installation.analyzeHeadlessPath === null) throw new Error( `Ghidra installation is unavailable: ${JSON.stringify(installation)}`, ); if (installation.providerVersion !== SUPPORTED_GHIDRA_VERSION) throw new Error("Ghidra provider version commitment drifted"); const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-ghidra-fixtures-")); const sourcePath = fileURLToPath( new URL("../tests/conformance/ghidra/inventory.c", import.meta.url), ); const crossFormatSourcePath = fileURLToPath( new URL("../tests/conformance/ghidra/cross-format.c", import.meta.url), ); const debugPath = join(fixtureRoot, "rea-ghidra-inventory-debug"); const strippedPath = join(fixtureRoot, "rea-ghidra-inventory-stripped"); const arm64ElfPath = join(fixtureRoot, "rea-ghidra-cross-arm64"); const peObjectPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.obj"); const pePath = join(fixtureRoot, "rea-ghidra-cross-x86_64.exe"); const peImportLibraryPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.lib"); const machObjectPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.o"); const malformedPath = join(fixtureRoot, "rea-ghidra-malformed"); const compiler = process.env.REA_CC ?? "cc"; const clang = process.env.REA_CLANG ?? "clang"; const lldLink = process.env.REA_LLD_LINK ?? "lld-link"; try { const common = ["-O0", "-g", "-fno-inline", "-fno-pie", "-no-pie"]; await exec(compiler, [...common, sourcePath, "-o", debugPath]); await exec(compiler, [...common, "-s", sourcePath, "-o", strippedPath]); await exec(clang, [ "--target=aarch64-linux-gnu", "-O0", "-g", "-fno-inline", "-fno-pie", "-nostdlib", "-static", "-fuse-ld=lld", crossFormatSourcePath, "-Wl,-e,rea_cross_start", "-o", arm64ElfPath, ]); await exec(clang, [ "--target=x86_64-pc-windows-msvc", "-O0", "-gcodeview", "-fno-inline", "-c", crossFormatSourcePath, "-o", peObjectPath, ]); await exec(lldLink, [ "/entry:rea_cross_start", "/subsystem:console", "/nodefaultlib", "/export:rea_cross_entry", `/implib:${peImportLibraryPath}`, `/out:${pePath}`, peObjectPath, ]); await exec(clang, [ "--target=x86_64-apple-darwin", "-O0", "-g", "-fno-inline", "-c", crossFormatSourcePath, "-o", machObjectPath, ]); await writeFile(malformedPath, Buffer.from("not-a-binary\n", "utf8")); const debug = await verifyTarget(debugPath, "debug", { format: "elf", architecture: "x86_64", }); const stripped = await verifyTarget(strippedPath, "stripped", { format: "elf", architecture: "x86_64", }); const arm64Elf = await verifyTarget(arm64ElfPath, "cross-arm64-elf", { format: "elf", architecture: "arm64", }); const pe = await verifyTarget(pePath, "cross-x86_64-pe", { format: "pe", architecture: "x86_64", }); const machObject = await verifyTarget(machObjectPath, "cross-x86_64-mach-o", { format: "mach-o", architecture: "x86_64", }); assertDebugFixture(debug); assertStrippedFixture(stripped); for (const fixture of [arm64Elf, pe, machObject]) assertCrossFixture(fixture); await assertMalformedFixture(malformedPath); const customPath = process.env.GHIDRA_TARGET_PATH; const custom = customPath === undefined ? null : await verifyTarget(customPath, "custom"); process.stdout.write( `${JSON.stringify({ ok: true, provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION }, fixture_sources: [sourcePath, crossFormatSourcePath], fixtures: [ summary(debug), summary(stripped), summary(arm64Elf), summary(pe), summary(machObject), ], malformed_target: "rejected-before-provider-start", custom_target: custom === null ? null : summary(custom), cleanup: "complete", })}\n`, ); } finally { await rm(fixtureRoot, { recursive: true, force: true }); } async function verifyTarget(targetPath, variant, expectedTarget = null) { const parsedTarget = await parseBinaryTarget(targetPath); if (!parsedTarget.ok) throw parsedTarget.error; if ( expectedTarget !== null && (parsedTarget.value.format !== expectedTarget.format || parsedTarget.value.architecture !== expectedTarget.architecture) ) throw new Error( `Fixture header classification drifted for ${variant}: ${JSON.stringify(parsedTarget.value)}`, ); const profile = await resolveGhidraAnalysisProfile( parsedTarget.value, GHIDRA_PROVIDER_IDENTITY, installation, ); if (!profile.ok || profile.value.profile === null) throw new Error("Ghidra analysis profile could not be committed"); const client = new GhidraClient({ launcher: new GhidraHeadlessLauncher({ analyzeHeadlessPath: installation.analyzeHeadlessPath, ...(process.env.JAVA_HOME === undefined ? {} : { javaHome: process.env.JAVA_HOME }), bridgeScriptPath: fileURLToPath( new URL("../bridge/ghidra/ReaGhidraBridge.java", import.meta.url), ), platform: installation.platform, }), targetPath: parsedTarget.value.path, targetSha256: parsedTarget.value.sha256, providerVersion: SUPPORTED_GHIDRA_VERSION, profileDigest: profile.value.profile.digest, requestTimeoutMs: GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS, }); let runtimeCoordinates; try { const started = await client.start(); if (!started.ok) throw started.error; assertSession( started.value, profile.value.profile.digest, parsedTarget.value.sha256, ); const pinged = await client.ping(); if (!pinged.ok) throw pinged.error; assertSession( pinged.value, profile.value.profile.digest, parsedTarget.value.sha256, ); runtimeCoordinates = client.diagnostics(); assertRuntimeCoordinates(runtimeCoordinates); const documents = await call(client, "list_documents", {}); const segments = await call(client, "list_segments", { document: null }); const procedures = await allItems(client, "list_procedures", { document: null, }); const names = await allItems(client, "list_names", { document: null, address: null, }); const strings = await allItems(client, "list_strings", { document: null, address: null, }); const probes = variant === "custom" ? null : await verifyInventoryOperations({ client, variant, procedures, names, strings, }); return { variant, target: parsedTarget.value, profile: profile.value.profile, session: pinged.value, documents, segments, procedures, names, strings, probes, }; } finally { await client.close(); if (runtimeCoordinates !== undefined) await assertCleanup(runtimeCoordinates); } } async function verifyInventoryOperations({ client, variant, procedures, names, strings, }) { if (variant.startsWith("cross-")) return verifyCrossFormatOperations({ client, variant, procedures, names, strings, }); const rejections = variant === "debug" ? await verifyBridgeRejections(client) : null; const stringSearch = await call(client, "search_strings", { pattern: String.raw`^REA_GHIDRA_(?:INVENTORY_ENTRY|LEAF_VALUE)$`, mode: "regex", case_sensitive: true, offset: 0, limit: 100, document: null, }); if (stringSearch.total !== 2 || stringSearch.items.length !== 2) throw new Error("Ghidra bounded regex string search drifted"); const stringItem = strings.find( (item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY", ); if (stringItem === undefined) throw new Error("Ghidra string probe address is unavailable"); const filteredStrings = await call(client, "list_strings", { document: null, address: stringItem.address, offset: 0, limit: 500, }); if ( filteredStrings.total !== 1 || filteredStrings.items[0]?.address !== stringItem.address ) throw new Error("Ghidra exact-address string filter drifted"); if (variant === "stripped") { const functionAnalysis = await verifyStrippedFunctionOperations( client, stringItem, ); return { rejections, stringSearch, filteredStrings, functionAnalysis }; } const entry = findValue(procedures, "rea_ghidra_inventory_entry"); if (entry === undefined) throw new Error("Ghidra entry procedure probe is unavailable"); const addressName = await call(client, "address_name", { document: null, address: entry.address, }); if (symbolTail(addressName) !== "rea_ghidra_inventory_entry") throw new Error("Ghidra primary address-name resolution drifted"); const resolvedAddress = await call(client, "procedure_address", { document: null, procedure: entry.value, }); if (resolvedAddress !== entry.address) throw new Error("Ghidra procedure-name resolution drifted"); const interior = `0x${(BigInt(entry.address) + 1n).toString(16)}`; const containing = await call(client, "resolve_containing_procedure", { document: null, address: interior, }); if (!containing.found || containing.procedure.address !== entry.address) throw new Error("Ghidra containing-procedure resolution drifted"); const procedureSearch = await call(client, "search_procedures", { pattern: "rea_ghidra_inventory_", mode: "literal", case_sensitive: true, offset: 0, limit: 1, document: null, }); if ( procedureSearch.total < 2 || procedureSearch.items.length !== 1 || !procedureSearch.has_more || procedureSearch.next_offset !== 1 ) throw new Error("Ghidra literal procedure-search pagination drifted"); const name = names.find((item) => item.address === entry.address); if (name === undefined) throw new Error("Ghidra entry name probe is unavailable"); const filteredNames = await call(client, "list_names", { document: null, address: entry.address, offset: 0, limit: 500, }); if ( filteredNames.total < 1 || !filteredNames.items.some((item) => item.value === name.value) ) throw new Error("Ghidra exact-address name filter drifted"); const functionAnalysis = await verifyDebugFunctionOperations(client, { procedures, strings, entry, }); return { rejections, addressName, resolvedAddress, containing, procedureSearch, stringSearch, filteredNames, filteredStrings, functionAnalysis, }; } async function verifyDebugFunctionOperations( client, { procedures, strings, entry }, ) { const leaf = requireProcedure(procedures, "rea_ghidra_inventory_leaf"); const branch = requireProcedure(procedures, "rea_ghidra_inventory_branch"); const indirect = requireProcedure( procedures, "rea_ghidra_inventory_indirect", ); const main = requireProcedure(procedures, "main"); const entryString = strings.find( (item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY", ); if (entryString === undefined) throw new Error("Ghidra debug string function probe is unavailable"); const [info, assembly, pseudocode, callees, callers, outgoing, entryXrefs] = await Promise.all([ functionCall(client, "procedure_info", { document: null, procedure: entry.value, }), functionCall(client, "procedure_assembly", { document: null, procedure: entry.value, }), functionCall(client, "procedure_pseudo_code", { document: null, procedure: entry.value, }), functionCall(client, "procedure_callees", { document: null, procedure: entry.value, }), functionCall(client, "procedure_callers", { document: null, procedure: entry.value, }), functionCall(client, "procedure_references", { document: null, procedure: entry.value, direction: "outgoing", }), functionCall(client, "xrefs", { document: null, address: entry.address, }), ]); assertLocalProcedureInfo(info, entry.address); assertProviderText(assembly, pseudocode, entry.address); for (const expected of [branch.address, indirect.address]) if (!callees.includes(expected)) throw new Error(`Ghidra call graph missed ${expected}`); if (!callers.includes(main.address) || entryXrefs.length === 0) throw new Error("Ghidra caller or xref relation missed main"); const xrefOwners = []; const xrefOwnerFailures = []; for (const address of entryXrefs) { try { xrefOwners.push( await call(client, "resolve_containing_procedure", { document: null, address, }), ); } catch (cause) { xrefOwnerFailures.push({ address, message: cause instanceof Error ? cause.message : String(cause), }); } } if ( !xrefOwners.some( (owner) => owner.found && owner.procedure.address === main.address, ) ) throw new Error( `Ghidra entry xrefs were not attributable to main: ${JSON.stringify({ entryXrefs, callers, xrefOwners, xrefOwnerFailures })}`, ); assertReferenceMetadata(outgoing); const stringXrefs = await functionCall(client, "xrefs", { document: null, address: entryString.address, }); if (stringXrefs.length === 0) throw new Error("Ghidra xrefs missed the entry string"); const entryDossier = await functionCall(client, "analyze_function", { procedure: entry.address, include_assembly: true, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(entryDossier, { address: entry.address, expectedCallees: [branch.address, indirect.address], expectedString: "REA_GHIDRA_INVENTORY_ENTRY", requireAssembly: true, }); const branchDossier = await functionCall(client, "analyze_function", { procedure: branch.address, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(branchDossier, { address: branch.address, expectedCallees: [leaf.address], requireMultiBlock: true, }); const indirectDossier = await functionCall(client, "analyze_function", { procedure: indirect.address, include_assembly: true, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(indirectDossier, { address: indirect.address, requireAssembly: true, }); if ( indirectDossier.callees.items.some( ({ address }) => address === leaf.address, ) ) throw new Error( "Ghidra dossier falsely resolved a targetless callback as the fixture leaf", ); if (!/\bcall\b/iu.test(indirectDossier.assembly.items.join("\n"))) throw new Error("Ghidra indirect-call fixture lost its computed call site"); const aborted = new AbortController(); aborted.abort(); const cancelled = await client.callTool( "procedure_info", { document: null, procedure: entry.address }, { signal: aborted.signal }, ); if (cancelled.ok || cancelled.error.kind !== "cancelled") throw new Error("Ghidra established-request cancellation drifted"); const timedOut = await client.callTool( "procedure_info", { document: null, procedure: entry.address }, { timeoutMs: 0 }, ); if (timedOut.ok || timedOut.error.kind !== "timeout") throw new Error("Ghidra established-request deadline drifted"); const concurrent = await Promise.all([ functionCall(client, "procedure_info", { document: null, procedure: entry.address, }), functionCall(client, "xrefs", { document: null, address: leaf.address, }), ]); if (concurrent.length !== 2) throw new Error("Ghidra serial request queue lost a concurrent result"); return { entry: dossierSummary(entryDossier), branch: dossierSummary(branchDossier), indirect: dossierSummary(indirectDossier), cancellation: "cancelled-before-wire", timeout: "expired-before-wire", concurrency: "serialized-two-results", }; } async function verifyStrippedFunctionOperations(client, entryString) { const stringXrefs = await functionCall(client, "xrefs", { document: null, address: entryString.address, }); if (stringXrefs.length === 0) throw new Error("Ghidra stripped string xref probe is unavailable"); const containing = await call(client, "resolve_containing_procedure", { document: null, address: stringXrefs[0], }); if (!containing.found) throw new Error("Ghidra could not recover a stripped containing function"); const procedure = containing.procedure; const pseudocode = await functionCall(client, "procedure_pseudo_code", { document: null, procedure: procedure.address, }); if (typeof pseudocode !== "string" || pseudocode.trim().length === 0) throw new Error("Ghidra did not decompile the stripped function"); const dossier = await functionCall(client, "analyze_function", { procedure: procedure.address, include_assembly: true, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(dossier, { address: procedure.address, expectedString: "REA_GHIDRA_INVENTORY_ENTRY", requireAssembly: true, }); if (symbolTail(dossier.procedure.name) === "rea_ghidra_inventory_entry") throw new Error("Ghidra stripped dossier invented a source symbol"); return dossierSummary(dossier); } async function verifyCrossFormatOperations({ client, variant, procedures, names, strings, }) { const message = strings.find( (item) => item.value === "REA_GHIDRA_CROSS_FORMAT", ); if (message === undefined) throw new Error(`${variant} string inventory missed the source oracle`); const entry = procedures.find((item) => matchesSymbol(item.value, "rea_cross_entry")) ?? names.find((item) => matchesSymbol(item.value, "rea_cross_entry")); if (entry === undefined) throw new Error(`${variant} did not retain the exported entry symbol`); const entryAddress = entry.address; const info = await functionCall(client, "procedure_info", { document: null, procedure: entryAddress, }); assertLocalProcedureInfo(info, entryAddress); const [pseudocode, assembly, callees, messageXrefs] = await Promise.all([ functionCall(client, "procedure_pseudo_code", { document: null, procedure: entryAddress, }), functionCall(client, "procedure_assembly", { document: null, procedure: entryAddress, }), functionCall(client, "procedure_callees", { document: null, procedure: entryAddress, }), functionCall(client, "xrefs", { document: null, address: message.address, }), ]); assertProviderText(assembly, pseudocode, entryAddress); if (callees.length < 2) throw new Error(`${variant} call graph missed direct fixture calls`); if (messageXrefs.length === 0) throw new Error(`${variant} xrefs missed the fixture string`); let messageOwner = null; for (const address of messageXrefs) { const candidate = await call(client, "resolve_containing_procedure", { document: null, address, }); if (candidate.found) { messageOwner = candidate; break; } } if (messageOwner === null) throw new Error(`${variant} did not resolve the string-owning function`); const stringDossier = await functionCall(client, "analyze_function", { procedure: messageOwner.procedure.address, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(stringDossier, { address: messageOwner.procedure.address, expectedString: "REA_GHIDRA_CROSS_FORMAT", }); let branchAddress = null; for (const address of callees) { const candidate = await functionCall(client, "procedure_info", { document: null, procedure: address, }); if (candidate.basicblock_count > 1) { branchAddress = address; break; } } if (branchAddress === null) throw new Error(`${variant} did not recover the multi-block callee`); const branchDossier = await functionCall(client, "analyze_function", { procedure: branchAddress, limit: 500, max_pseudocode_chars: 100_000, max_instructions: 5_000, }); assertDossier(branchDossier, { address: branchAddress, requireMultiBlock: true, }); return { entry: entryAddress, exported_symbol: entry.value, string_owner: dossierSummary(stringDossier), branch: dossierSummary(branchDossier), }; } async function verifyBridgeRejections(client) { return { unknownDocument: await expectRemoteFailure( client, "list_segments", { document: "not-the-imported-program" }, "not_found", ), malformedParameter: await expectRemoteFailure( client, "procedure_address", { document: null, procedure: 17 }, "invalid_request", ), unboundedRegex: await expectRemoteFailure( client, "search_strings", { pattern: "a+", mode: "regex", case_sensitive: true, offset: 0, limit: 100, document: null, }, "invalid_request", ), undeclaredMutation: await expectRemoteFailure( client, "set_comment", {}, "method_unavailable", ), }; } async function expectRemoteFailure(client, operation, parameters, code) { const result = await client.callTool(operation, parameters); if ( result.ok || result.error.kind !== "remote" || result.error.remoteCode !== code || result.error.diagnostics.remote_code !== code ) throw new Error( `Ghidra ${operation} rejection drifted: ${JSON.stringify(result)}`, ); return { operation, code }; } async function call(client, operation, parameters) { const called = await client.callTool(operation, parameters); if (!called.ok) throw new Error( `Ghidra ${operation} failed for ${JSON.stringify(parameters)}: ${called.error.message}`, { cause: called.error }, ); const parsed = parseGhidraInventoryResult(operation, called.value); if (!parsed.ok) throw parsed.error; return parsed.value; } async function functionCall(client, operation, parameters) { const input = parseGhidraFunctionInput(operation, parameters); if (!input.ok) throw input.error; const called = await client.callTool(operation, input.value); if (!called.ok) throw new Error( `Ghidra ${operation} failed for ${JSON.stringify(input.value)}: ${called.error.message}`, { cause: called.error }, ); const parsed = parseGhidraFunctionResult(operation, called.value); if (!parsed.ok) throw parsed.error; return parsed.value; } function assertLocalProcedureInfo(info, expectedAddress) { if ( info.entrypoint !== expectedAddress || info.basicblock_count < 1 || info.length < 1 || typeof info.signature !== "string" || info.signature.length === 0 || info.classification.external || info.classification.provenance !== "ghidra-function-manager" ) throw new Error( `Ghidra procedure metadata drifted: ${JSON.stringify(info)}`, ); } function assertProviderText(assembly, pseudocode, expectedAddress) { if ( typeof assembly !== "string" || !assembly.includes(`${expectedAddress}:`) || assembly.trim().length === 0 ) throw new Error("Ghidra assembly rendering lost its address-bearing lines"); if (typeof pseudocode !== "string" || pseudocode.trim().length === 0) throw new Error("Ghidra local function pseudocode is unavailable"); } function assertReferenceMetadata(observed) { if ( observed.instructions_scanned < 1 || observed.instruction_scan_truncated || observed.references.items.length === 0 || !observed.references.items.every( ({ kind }) => kind.available && kind.provenance === "ghidra-reference-manager", ) || !observed.references.items.some(({ kind }) => kind.call) || !observed.references.items.some(({ kind }) => kind.data) ) throw new Error( `Ghidra typed reference metadata drifted: ${JSON.stringify(observed)}`, ); } function assertDossier( dossier, { address, expectedCallees = [], expectedString = null, requireAssembly = false, requireMultiBlock = false, }, ) { if ( dossier.procedure.address !== address || dossier.procedure.classification === null || dossier.procedure.classification.provenance !== "ghidra-function-manager" || dossier.procedure.classification.external || dossier.pseudocode.text.trim().length === 0 || dossier.pseudocode.returned_chars !== [...dossier.pseudocode.text].length || dossier.instruction_scan.scanned < 1 || dossier.instruction_scan.truncated || !dossier.limitations.some((value) => /indirect|computed/u.test(value)) || !dossier.limitations.some((value) => /Ghidra|Hopper|provider/u.test(value)) ) throw new Error( `Ghidra dossier truthfulness drifted: ${JSON.stringify(dossier)}`, ); if ( ![ ...dossier.incoming_references.items, ...dossier.outgoing_references.items, ].every( ({ kind }) => kind.available && kind.provenance === "ghidra-reference-manager", ) ) throw new Error("Ghidra dossier lost typed reference provenance"); for (const expected of expectedCallees) if (!dossier.callees.items.some(({ address: value }) => value === expected)) throw new Error(`Ghidra dossier missed callee ${expected}`); if ( expectedString !== null && !dossier.referenced_strings.items.some( ({ value }) => value === expectedString, ) ) throw new Error( `Ghidra dossier missed referenced string ${expectedString}`, ); if ( requireAssembly && (dossier.assembly.items.length === 0 || dossier.assembly.truncated) ) throw new Error("Ghidra dossier assembly was unavailable or truncated"); if ( requireMultiBlock && (dossier.basic_blocks.items.length < 2 || !dossier.basic_blocks.items.some( ({ successors }) => successors.length > 0, )) ) throw new Error( "Ghidra dossier CFG missed the multi-block branch structure", ); } function dossierSummary(dossier) { return { address: dossier.procedure.address, pseudocode_chars: dossier.pseudocode.total_chars, instructions: dossier.instruction_scan.scanned, callees: dossier.callees.returned, outgoing_references: dossier.outgoing_references.returned, referenced_strings: dossier.referenced_strings.returned, basic_blocks: dossier.basic_blocks.returned, }; } async function allItems(client, operation, parameters) { const items = []; let offset = 0; let total; while (true) { const page = await call(client, operation, { ...parameters, offset, limit: 500, }); if (typeof page !== "object" || page === null || Array.isArray(page)) throw new Error(`${operation} did not return a page`); items.push(...page.items); total ??= page.total; if (page.total !== total) throw new Error(`${operation} total changed during immutable paging`); if (!page.has_more) break; if (page.next_offset === null || page.next_offset <= offset) throw new Error(`${operation} returned a non-advancing page`); offset = page.next_offset; } if (items.length !== total) throw new Error(`${operation} pagination was not exhaustive`); return items; } function assertDebugFixture(observed) { if ( !Array.isArray(observed.documents) || observed.documents.length !== 1 || !observed.documents[0].includes("debug") ) throw new Error("Ghidra did not expose exactly one debug Program identity"); const entry = findValue(observed.procedures, "rea_ghidra_inventory_entry"); const leaf = findValue(observed.procedures, "rea_ghidra_inventory_leaf"); const external = observed.procedures.find( (item) => item.procedure.external && symbolTail(item.value) === "puts", ); if (entry === undefined || leaf === undefined || external === undefined) throw new Error( "Ghidra procedure inventory missed local or external functions", ); if ( !observed.procedures.some( (item) => item.procedure.thunk && item.procedure.thunk_target !== null, ) ) throw new Error( "Ghidra procedure inventory did not distinguish a resolved thunk", ); const entryName = findValue(observed.names, "rea_ghidra_inventory_entry"); if (entryName === undefined || !entryName.symbol.primary) throw new Error("Ghidra symbol inventory missed the primary entry symbol"); if (!observed.names.some((item) => item.symbol.external)) throw new Error("Ghidra symbol inventory missed external symbols"); assertStrings(observed.strings); assertSegments(observed.segments, observed.session.target.image_base); } function assertStrippedFixture(observed) { if ( observed.procedures.length === 0 || observed.names.length === 0 || findValue(observed.procedures, "rea_ghidra_inventory_entry") !== undefined ) throw new Error( "Stripped Ghidra fixture did not preserve stripped semantics", ); assertStrings(observed.strings); assertSegments(observed.segments, observed.session.target.image_base); } function assertCrossFixture(observed) { if ( observed.procedures.length === 0 || observed.names.length === 0 || observed.probes === null || typeof observed.probes.entry !== "string" || observed.probes.string_owner.referenced_strings < 1 || observed.probes.branch.basic_blocks < 2 ) throw new Error( `Cross-format Ghidra semantics drifted: ${JSON.stringify(summary(observed))}`, ); assertSegments(observed.segments, observed.session.target.image_base); } async function assertMalformedFixture(path) { const parsed = await parseBinaryTarget(path); if (parsed.ok || parsed.error._tag !== "BinaryTargetError") throw new Error( "Malformed executable was not rejected before provider start", ); } function assertStrings(strings) { for (const expected of [ "REA_GHIDRA_INVENTORY_ENTRY", "REA_GHIDRA_LEAF_VALUE", ]) { const item = strings.find((candidate) => candidate.value === expected); if ( item === undefined || item.value_truncated || typeof item.string.encoding !== "string" || item.string.byte_length < expected.length ) throw new Error(`Ghidra string inventory missed ${expected}`); } } function assertSegments(segments, imageBase) { if ( !Array.isArray(segments) || segments.length === 0 || !segments.some((item) => item.executable) || !segments.some((item) => item.writable) || !segments.every( (item) => item.permissions.available === true && item.permissions.source === "ghidra-memory-block" && item.image_base === imageBase, ) ) throw new Error("Ghidra memory-block permissions or image base drifted"); } function findValue(items, expected) { return items.find((item) => symbolTail(item.value) === expected); } function requireProcedure(items, expected) { const observed = findValue(items, expected); if (observed === undefined) throw new Error(`Ghidra procedure probe is unavailable: ${expected}`); return observed; } function matchesSymbol(value, expected) { const tail = symbolTail(value); return tail === expected || tail === `_${expected}`; } function symbolTail(value) { if (typeof value !== "string") return undefined; return value.split("::").at(-1); } function assertSession(session, expectedDigest, expectedTargetSha256) { if ( session.provider.id !== "ghidra" || session.provider.version !== SUPPORTED_GHIDRA_VERSION || session.profile_digest !== expectedDigest || session.target.sha256 !== expectedTargetSha256 || session.read_only !== true || session.analysis_complete !== true || session.analysis_timed_out !== false || session.capabilities.join(",") !== GHIDRA_SESSION_CAPABILITIES.join(",") || !/^0x[0-9a-f]+$/u.test(session.target.image_base) || session.target.default_address_space.length === 0 ) throw new Error( `Ghidra session commitment drifted: ${JSON.stringify(session)}`, ); } function assertRuntimeCoordinates(coordinates) { for (const name of ["runtime_root", "endpoint_path", "project_root"]) if (typeof coordinates[name] !== "string") throw new Error(`Ghidra verifier did not observe ${name}`); if (typeof coordinates.process_id !== "number") throw new Error("Ghidra verifier did not observe its process ID"); } async function assertCleanup(coordinates) { for (const name of ["runtime_root", "endpoint_path", "project_root"]) { const path = coordinates[name]; if (typeof path === "string" && (await exists(path))) throw new Error(`Ghidra cleanup left ${name}: ${path}`); } const pid = coordinates.process_id; if (typeof pid === "number" && processExists(pid)) throw new Error(`Ghidra cleanup left process ${String(pid)}`); } function summary(observed) { return { variant: observed.variant, target_path: observed.target.path, target_sha256: observed.target.sha256, analysis_profile_digest: observed.profile.digest, language_id: observed.session.target.language_id, compiler_spec_id: observed.session.target.compiler_spec_id, image_base: observed.session.target.image_base, procedures: observed.procedures.length, names: observed.names.length, strings: observed.strings.length, memory_blocks: observed.segments.length, }; } async function exists(path) { try { await access(path); return true; } catch { return false; } } function processExists(pid) { try { process.kill(pid, 0); return true; } catch (cause) { return !( cause instanceof Error && "code" in cause && cause.code === "ESRCH" ); } }