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:
1 parent
f1819f5cb2
commit
9ef70696dd
10 files changed
+121
-7
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
}),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user