diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index fdafe661ac..05cbb01781 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -1288,6 +1288,12 @@ public final class ReaGhidraBridge extends HeadlessScript { if (component.getDataType() instanceof ghidra.program.model.data.BitFieldDataType bitfield) { field.addProperty("bit_size", bitfield.getBitSize()); field.addProperty("bit_offset", bitfield.getBitOffset()); + JsonObject bitField = new JsonObject(); + bitField.addProperty("base_type_id", bitfield.getBaseDataType().getPathName()); + bitField.addProperty("declared_bit_size", bitfield.getDeclaredBitSize()); + bitField.addProperty("component_type_lookup", typeLookupResult(bitfield)); + bitField.addProperty("base_type_lookup", typeLookupResult(bitfield.getBaseDataType())); + field.add("bit_field", bitField); } field.addProperty("status", "observed"); field.addProperty("flexible_tail", "unknown"); @@ -1318,7 +1324,7 @@ public final class ReaGhidraBridge extends HeadlessScript { result.add("members", members); result.addProperty("total_fields", totalFields); result.addProperty("truncated", totalFields > 20_000); - result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Nested and recursive types retain category-path identities for separate inspection.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation."))); + result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Component type paths do not necessarily identify separately registered types; bit-field lookups report whether the manager resolves equivalent types.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation."))); if (type != null) { JsonObject metadata = analysisExtensions.inspectDataType(currentProgram, type, address, monitor); if (metadata != null) result.add("metadata_recovery", metadata); @@ -1326,6 +1332,11 @@ public final class ReaGhidraBridge extends HeadlessScript { return result; } + private String typeLookupResult(DataType type) { + DataType resolved = currentProgram.getDataTypeManager().getDataType(type.getPathName()); + return resolved == null ? "not-found" : resolved.isEquivalent(type) ? "equivalent" : "different"; + } + private JsonObject instructionReference(Reference reference) { JsonObject item = new JsonObject(); JsonObject kind = referenceKind(reference); diff --git a/docs/native-investigation.md b/docs/native-investigation.md index bfe9747d3b..f3d5191b8c 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -128,6 +128,16 @@ rea inspect-native-data-type --type /MyStruct --provider ghidra rea inspect-native-data-type --address --provider ghidra ``` +Ghidra bit-field components may have a `type_id` that is not a registered +database type. When `bit_field` is present, `base_type_id` is the provider's +base type and the lookup fields report whether the manager resolves equivalent +types at those exact paths. `bit_size` is the effective width; +`declared_bit_size` can be larger. `bit_offset` is the least-significant-bit +offset within the base type, not a byte-order-independent offset from the start +of the containing structure. Account for the component's storage size and byte +order before applying that offset to a larger word. Missing `bit_field` details +are unreported, not proof that a component is not a bit-field. + Instruction facts include bytes, length, decoder text, ordered register/scalar/ address tokens, typed references and flow destinations. Mid-instruction, data, outside-memory and unlisted addresses have separate outcomes. For Ghidra, diff --git a/scripts/verify-real-ghidra-function.mjs b/scripts/verify-real-ghidra-function.mjs index 1e4f2c2cf9..c2764e4689 100644 --- a/scripts/verify-real-ghidra-function.mjs +++ b/scripts/verify-real-ghidra-function.mjs @@ -619,7 +619,35 @@ export async function verifyNativeTypeLayout(client, typeNames) { ) ) throw new Error("Nested union or enum layout drifted"); - return { layout, payload, state }; + const bitsSymbol = typeNames.find((item) => + matchesSymbol(item.value, "rea_ghidra_inventory_bits"), + ); + if (bitsSymbol === undefined) + throw new Error("Bit-field fixture symbol unavailable"); + const bits = await functionCall(client, "inspect_native_data_type", { + document: null, + address: bitsSymbol.address, + }); + const low = bits.fields.find((field) => field.name === "low"); + if ( + bits.kind !== "struct" || + low?.bit_size !== 3 || + low.bit_field?.declared_bit_size !== 3 || + low.bit_field.component_type_lookup !== "not-found" || + low.bit_field.base_type_lookup !== "equivalent" + ) + throw new Error(`Bit-field identity unavailable: ${JSON.stringify(bits)}`); + const component = await functionCall(client, "inspect_native_data_type", { + document: null, + type: low.type_id, + }); + const base = await functionCall(client, "inspect_native_data_type", { + document: null, + type: low.bit_field.base_type_id, + }); + if (component.status !== "unavailable" || base.status !== "available") + throw new Error("Bit-field component or base lookup drifted"); + return { layout, payload, state, bits, base }; } export async function verifyRelativeSwitch(client, procedures, entrySize) { diff --git a/scripts/verify-real-ghidra-ppc.mjs b/scripts/verify-real-ghidra-ppc.mjs index a8a99d9408..683601352a 100644 --- a/scripts/verify-real-ghidra-ppc.mjs +++ b/scripts/verify-real-ghidra-ppc.mjs @@ -110,6 +110,39 @@ try { // the plain loader would have chosen e500 for this header. const image = await call("inspect_native_load_image"); assertPowerPcLoadedImage(image.observations, fixture.sha256); + const names = await call("list_names"); + const bitsName = names.find((item) => item.value === "rea_ppc_bits"); + assert.ok(bitsName, "Big-endian bit-field fixture is missing"); + const bitsBytes = await call("read_bytes", { + address: bitsName.address, + length: 4, + }); + assert.equal(bitsBytes.bytes_hex, "22000000"); + const bits = await call("inspect_native_data_type", { + address: bitsName.address, + }); + assert.equal(bits.kind, "struct"); + const bitFields = bits.fields.filter((field) => field.bit_field); + assert.deepEqual( + bitFields.map((field) => [ + field.name, + field.bit_size, + field.bit_offset, + ]), + [ + ["first", 3, 5], + ["second", 5, 0], + ], + ); + for (const field of bitFields) { + assert.equal(field.bit_field.component_type_lookup, "not-found"); + assert.equal(field.bit_field.base_type_lookup, "equivalent"); + assert.equal(field.bit_field.declared_bit_size, field.bit_size); + const base = await call("inspect_native_data_type", { + type: field.bit_field.base_type_id, + }); + assert.equal(base.status, "available"); + } const procedures = await call("list_procedures"); const entry = procedures.find((p) => p.value === "rea_ppc_entry"); const leaf = procedures.find((p) => p.value === "rea_ppc_leaf"); @@ -245,6 +278,7 @@ try { "fixed instruction bytes and immediate", "conditional branch destination", "global word and marker bytes", + "big-endian bit-field storage and base type handoff", "CLI/MCP and snapshot identity", ], procedures: procedures.length, diff --git a/src/domain/native/nativeDataType.ts b/src/domain/native/nativeDataType.ts index 4556df5772..2380ae0db1 100644 --- a/src/domain/native/nativeDataType.ts +++ b/src/domain/native/nativeDataType.ts @@ -12,7 +12,7 @@ export const nativeDataTypeInputSchema = z (value) => (value.type === undefined) !== (value.address === undefined), "Supply exactly one of type or address", ); -/** Types remain analysis database observations; child identities can be inspected separately. */ +/** Types and component identities remain analysis database observations. */ export const nativeDataTypeSchema = z.strictObject({ status: z.enum(["available", "unavailable"]), reason: z.string().nullable(), @@ -47,6 +47,21 @@ export const nativeDataTypeSchema = z.strictObject({ type_id: z.string(), bit_size: z.int().nonnegative().nullable(), bit_offset: z.int().nonnegative().nullable(), + bit_field: z + .strictObject({ + base_type_id: z.string(), + declared_bit_size: z.int().nonnegative(), + component_type_lookup: z.enum([ + "equivalent", + "different", + "not-found", + ]), + base_type_lookup: z.enum(["equivalent", "different", "not-found"]), + }) + .optional() + .describe( + "Observed bit-field base and exact manager lookups. bit_size is the effective width; bit_offset is the least-significant-bit offset within the base type. Omission means bit-field details were not reported.", + ), status: z.enum(["observed", "unsupported"]), flexible_tail: z.literal("unknown"), }), diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index 6a9b5223f4..95eab2971a 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -62,6 +62,7 @@ const sharedAnalysisSemantics = { call_relationship_coverage: "function-body-call-coverage-v5", function_dossier_coverage: "observed-facets-and-call-relationships-v1", string_inventory_evidence: "defined-data-coverage-v1", + data_type_observations: "bit-field-base-and-lookup-v1", location_resolution: "explicit-address-exact-entry-symbol-first-v3", procedure_search: "primary-and-exact-entry-symbol-names-v1", procedure_file_offset_resolution: diff --git a/src/ghidra/GhidraRawProfileDigest.test.ts b/src/ghidra/GhidraRawProfileDigest.test.ts index 1636945ba0..c365dee098 100644 --- a/src/ghidra/GhidraRawProfileDigest.test.ts +++ b/src/ghidra/GhidraRawProfileDigest.test.ts @@ -7,12 +7,13 @@ import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixt import { GhidraProvider } from "./GhidraProvider.js"; // Digests commit to the current shared policy, including the instruction -// status, load-image relocation-symbol, procedure search and byte-search revisions that -// prevent reuse of snapshots with earlier results. +// status, load-image relocation-symbol, procedure search, byte-search and +// bit-field base-type revisions that prevent reuse of snapshots with earlier +// results. const RAW_DIGESTS = { - "6502": "8fed434477e505a56e8c991b4a986a02f3c536d7797e993e1bdc5db984946f64", + "6502": "6f904539c00521b04f76a3bfd1ca88275e080b5751f9e2305de66c20d0bd2fa0", "x86-disabled-analyzer": - "6f3adeb639f686cbf206b8020f6b8fa644d0cace68d9989614784dfda9ba3bd7", + "5a534d959baf34abdafaea64ae9fcb9126553802d42ffa5e4bc0426de5f0af8c", } as const; const configFor = (environment: NodeJS.ProcessEnv) => { @@ -49,6 +50,7 @@ describe("raw-binary analysis profile digest", () => { "source-mappings-entry-context-relocation-symbol-v3", instruction_status: "unlisted-address-without-decode-v1", byte_search_boundary: "contiguous-eligible-initialized-blocks-v1", + data_type_observations: "bit-field-base-and-lookup-v1", analyzer_preset: "ghidra-default", }); }); diff --git a/src/ghidra/GhidraSnapshotCache.test.ts b/src/ghidra/GhidraSnapshotCache.test.ts index 5a1d540b90..ad50ef37dc 100644 --- a/src/ghidra/GhidraSnapshotCache.test.ts +++ b/src/ghidra/GhidraSnapshotCache.test.ts @@ -307,6 +307,11 @@ describe("Ghidra relationship coverage replay", () => { describe("Ghidra snapshot profile admission", () => { it.each([ + { + parameter: "data_type_observations", + current: "bit-field-base-and-lookup-v1", + legacy: undefined, + }, { parameter: "byte_search_boundary", current: "contiguous-eligible-initialized-blocks-v1", diff --git a/tests/conformance/c/ppc.c b/tests/conformance/c/ppc.c index 3f62ca57e6..8959fde633 100644 --- a/tests/conformance/c/ppc.c +++ b/tests/conformance/c/ppc.c @@ -1,6 +1,8 @@ /* Freestanding, source-owned static-analysis fixture. Never executed by REA. */ volatile unsigned int rea_ppc_global = 7; const char rea_ppc_marker[] = "rea-ppc-source-owned-fixture"; +struct ReaPpcBits { unsigned int first : 3; unsigned int second : 5; }; +volatile struct ReaPpcBits rea_ppc_bits = {1, 2}; __attribute__((noinline)) unsigned int rea_ppc_leaf(unsigned int value) { diff --git a/tests/conformance/ghidra/inventory.c b/tests/conformance/ghidra/inventory.c index e82c7d9ddb..1c4413262e 100644 --- a/tests/conformance/ghidra/inventory.c +++ b/tests/conformance/ghidra/inventory.c @@ -39,6 +39,12 @@ struct ReaFixtureLayout { volatile struct ReaFixtureLayout rea_ghidra_inventory_layout = { REA_FIXTURE_READY, {.word = 5}, 0}; +struct ReaFixtureBits { + unsigned int low : 3; + unsigned int high : 5; +}; +volatile struct ReaFixtureBits rea_ghidra_inventory_bits = {1, 2}; + __attribute__((noinline, used)) int rea_ghidra_inventory_leaf(int value) { puts("REA_GHIDRA_LEAF_VALUE"); return value + rea_ghidra_inventory_global;