fix(ghidra): expose bit-field base type identity (#8602)

* fix(ghidra): expose bit-field base type identity

* test(ghidra): verify bit-field type handoff

* test(ghidra): verify big-endian bit-field handoff

* fix(ghidra): bind bit-field facts to snapshot profile

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-11-05 04:03:43 +08:00
1 parent f1819f5cb2
commit 9ef70696dd
10 files changed
+121 -7

No files matched your search

+12 -1
View File
@@ -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);
+10
View File
@@ -128,6 +128,16 @@ rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
rea inspect-native-data-type <binary> --address <typed-data-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,
+29 -1
View File
@@ -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) {
+34
View File
@@ -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,
+16 -1
View File
@@ -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"),
}),
+1
View File
@@ -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:
+6 -4
View File
@@ -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",
});
});
+5
View File
@@ -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",
+2
View File
@@ -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)
{
+6
View File
@@ -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;