mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84f66cc672 | ||
|
|
ff00a77731 |
No files matched your search
@@ -95,3 +95,30 @@ jobs:
|
||||
decode cortex ARM:LE:32:Cortex mrs/4/msp bx/2/lr
|
||||
decode v8m ARM:LE:32:v8-m mrs/4/msplim bx/2/lr
|
||||
decode xtensa Xtensa:LE:32:default entry/3/a1 retw.n/2
|
||||
# Authored void, variadic and one-parameter signatures fix Ghidra's
|
||||
# HighFunction inputs; default signatures are recovered from uses. Only
|
||||
# the signatures are authored. The source-owned x86-64 bytes are never
|
||||
# executed.
|
||||
- name: Distinguish fixed from recovered HighFunction parameter lists
|
||||
shell: bash
|
||||
run: |
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
export JAVA_HOME="${!java_home_variable}"
|
||||
probe_root="${RUNNER_TEMP}/rea-parameter-list"
|
||||
mkdir -p "${probe_root}/project"
|
||||
{
|
||||
printf '\270\001\000\000\000\303\314\314' # 0x1000: mov eax, 1; ret (default)
|
||||
printf '\270\001\000\000\000\303\314\314' # 0x1008: int f(void)
|
||||
printf '\270\001\000\000\000\303\314\314' # 0x1010: int f(...)
|
||||
printf '\270\001\000\000\000\303\314\314' # 0x1018: int f(int x)
|
||||
printf '\215\004\017\303\314\314\314\314' # 0x1020: lea eax, [rdi + rcx]; ret (default)
|
||||
} > "${probe_root}/parameter-list.bin"
|
||||
test "$(wc -c < "${probe_root}/parameter-list.bin")" -eq 40
|
||||
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ParameterList \
|
||||
-import "${probe_root}/parameter-list.bin" -loader BinaryLoader -loader-baseAddr 0x1000 \
|
||||
-processor x86:LE:64:default -noanalysis -readOnly -deleteProject \
|
||||
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
-postScript ReaParameterListProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
|
||||
cat "${probe_root}/output.log"
|
||||
grep -F 'REA_PARAMETER_LIST_OK default=recovered void=signature varargs=variadic one=signature used=recovered' "${probe_root}/output.log"
|
||||
@@ -2495,6 +2495,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
|
||||
JsonArray parameters = new JsonArray();
|
||||
result.add("parameters", parameters);
|
||||
result.add("parameter_list", parameterList(function, prototype));
|
||||
|
||||
JsonArray jumpTables = new JsonArray();
|
||||
result.add("jump_tables", jumpTables);
|
||||
@@ -2552,6 +2553,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ghidra fixes prototype inputs from a non-default signature: an empty one
|
||||
* reaches the decompiler as a void-input lock (FunctionPrototype), and each
|
||||
* valid parameter is type-locked (LocalSymbolMap). Any other list was
|
||||
* recovered from observed uses and may omit parameters.
|
||||
*/
|
||||
private static JsonObject parameterList(Function function, FunctionPrototype prototype) {
|
||||
int count = prototype.getNumParams();
|
||||
boolean signature = count > 0 || (
|
||||
function.getSignatureSource() != SourceType.DEFAULT &&
|
||||
function.getParameterCount() == 0
|
||||
);
|
||||
for (int index = 0; signature && index < count; index += 1) {
|
||||
HighSymbol parameter = prototype.getParam(index);
|
||||
signature = parameter != null && parameter.isTypeLocked();
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("basis", signature ? "signature" : "recovered");
|
||||
result.addProperty("variadic", prototype.isVarArg());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonObject unavailableNativeValueFlow(String reason) {
|
||||
JsonObject unavailable = new JsonObject();
|
||||
unavailable.addProperty("available", false);
|
||||
|
||||
@@ -245,6 +245,7 @@ profile stages before retrying their retained descriptors.
|
||||
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
|
||||
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
|
||||
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
|
||||
| Parameter lists | `native_api.parameter_list.basis` is `signature` when Ghidra fixes HighFunction inputs from a non-default signature: an empty one as a void-input lock, or every valid parameter type-locked. Otherwise it is `recovered` and the decompiler may have omitted parameters. `variadic` is the HighFunction prototype's varargs flag. `signature_source` names the signature's origin, which may be analysis or an import; a signature basis is not source-level proof. Ghidra snapshots recorded without this fact are not replayed. |
|
||||
|
||||
Runtime updates apply to newly accepted operations on the existing Program.
|
||||
They preserve accepted budgets and do not clear cleanup blockers or change the
|
||||
|
||||
@@ -421,6 +421,15 @@ nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
A call to an analyzed callee with no recovered parameter nodes gets the unknown
|
||||
`Parameter bindings for <callee> are unavailable or omitted`, unless the
|
||||
callee's `native_api` reports no parameters and a `parameter_list` with
|
||||
`basis: "signature"` and `variadic: false`. That provider fact means the
|
||||
callee's signature fixes zero parameters, so the call has nothing to bind.
|
||||
Recovered, variadic or unreported parameter lists, and callees that were not
|
||||
analyzed, keep the unknown. Evidence recorded before `parameter_list` existed
|
||||
omits it, and its arity remains unknown.
|
||||
|
||||
Within one value trace, each exact procedure selector is attempted at most once,
|
||||
including failed analyses. A failed seed returns the provider's original typed
|
||||
failure and corrective guidance; it cannot establish a trace or target identity.
|
||||
|
||||
@@ -627,6 +627,7 @@ const PATH_SCOPES = {
|
||||
"scripts/verify-ghidra-bridge-sources.mjs",
|
||||
"tests/conformance/ghidra/ReaArmContextProbe.java",
|
||||
"tests/conformance/ghidra/ReaDecodeProbe.java",
|
||||
"tests/conformance/ghidra/ReaParameterListProbe.java",
|
||||
".github/workflows/ghidra-bridge-sources.yml",
|
||||
],
|
||||
website: [
|
||||
|
||||
@@ -42,13 +42,61 @@ export const pcodeOperation = (
|
||||
output: pcodeScalar,
|
||||
});
|
||||
|
||||
type ParameterFact = { ordinal: number; name: string; data_type: string };
|
||||
|
||||
/**
|
||||
* Provider parameter-list facts. Fixtures default to a decompiler-recovered
|
||||
* list; "unreported" omits the facts, as Evidence from older releases does.
|
||||
*/
|
||||
export type ParameterListFact =
|
||||
| { readonly basis: "signature" | "recovered"; readonly variadic: boolean }
|
||||
| "unreported";
|
||||
|
||||
const recoveredParameters: ParameterListFact = {
|
||||
basis: "recovered",
|
||||
variadic: false,
|
||||
};
|
||||
|
||||
/** An API boundary whose parameters and parameter-list facts match the flow. */
|
||||
const nativeApi = (
|
||||
base: ReturnType<typeof functionDossierSchema.parse>,
|
||||
parameters: readonly ParameterFact[],
|
||||
parameterList: ParameterListFact,
|
||||
) => {
|
||||
if (base.native_api?.available !== true) throw new Error("Invalid fixture");
|
||||
const boundary = {
|
||||
...base.native_api,
|
||||
parameters: parameters.map(({ ordinal, name, data_type }) => ({
|
||||
role: "parameter" as const,
|
||||
ordinal,
|
||||
name,
|
||||
data_type,
|
||||
size_bytes: 4,
|
||||
storage: null,
|
||||
confidence: "high" as const,
|
||||
evidence: [
|
||||
{
|
||||
kind: "decompiler-type" as const,
|
||||
source: "ghidra-high-function",
|
||||
detail: `HighFunction recovered data type ${data_type}.`,
|
||||
},
|
||||
],
|
||||
decompiler_artifacts: [],
|
||||
})),
|
||||
};
|
||||
if (parameterList === "unreported") delete boundary.parameter_list;
|
||||
else boundary.parameter_list = parameterList;
|
||||
return boundary;
|
||||
};
|
||||
|
||||
export const nativeFlowDossier = (
|
||||
base: ReturnType<typeof functionDossierSchema.parse>,
|
||||
facts: {
|
||||
address: string;
|
||||
name: string;
|
||||
operations: ReturnType<typeof pcodeOperation>[];
|
||||
parameters: { ordinal: number; name: string; data_type: string }[];
|
||||
parameters: ParameterFact[];
|
||||
parameterList?: ParameterListFact;
|
||||
},
|
||||
) => {
|
||||
const { address, name, operations, parameters } = facts;
|
||||
@@ -65,6 +113,11 @@ export const nativeFlowDossier = (
|
||||
signature: null,
|
||||
locals: [],
|
||||
},
|
||||
native_api: nativeApi(
|
||||
base,
|
||||
parameters,
|
||||
facts.parameterList ?? recoveredParameters,
|
||||
),
|
||||
native_value_flow: {
|
||||
available: true,
|
||||
provenance: "ghidra-high-pcode",
|
||||
@@ -84,7 +137,8 @@ export const nativeFlowDossier = (
|
||||
|
||||
interface StaticProcedure {
|
||||
readonly operations: ReturnType<typeof pcodeOperation>[];
|
||||
readonly parameters?: { ordinal: number; name: string; data_type: string }[];
|
||||
readonly parameters?: ParameterFact[];
|
||||
readonly parameterList?: ParameterListFact;
|
||||
readonly def_use?: { definition: string; use: string; input_index: number }[];
|
||||
readonly pcodeTruncated?: boolean;
|
||||
}
|
||||
@@ -154,6 +208,9 @@ export const staticValueGraph = (
|
||||
name: address,
|
||||
operations: facts.operations,
|
||||
parameters: facts.parameters ?? [],
|
||||
...(facts.parameterList === undefined
|
||||
? {}
|
||||
: { parameterList: facts.parameterList }),
|
||||
}),
|
||||
);
|
||||
if (!dossier.native_value_flow?.available)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
|
||||
import { ok, err } from "../../domain/result.js";
|
||||
import {
|
||||
nativeFlowDossier,
|
||||
type ParameterListFact,
|
||||
pcodeOperation as node,
|
||||
pcodeScalar as scalar,
|
||||
staticValueGraph,
|
||||
@@ -1327,3 +1328,177 @@ describe("native value traversal limits and policies", () => {
|
||||
expect(limited.analyzed).toEqual(["0x1000"]);
|
||||
});
|
||||
});
|
||||
|
||||
/** 0x1000 passes `argumentCount` arguments to 0x2000, which returns a value. */
|
||||
const singleCallGraph = (
|
||||
callee: {
|
||||
readonly parameters?: {
|
||||
ordinal: number;
|
||||
name: string;
|
||||
data_type: string;
|
||||
}[];
|
||||
readonly parameterList: ParameterListFact;
|
||||
},
|
||||
argumentCount = 0,
|
||||
) =>
|
||||
staticValueGraph(
|
||||
{
|
||||
"0x1000": {
|
||||
operations: [
|
||||
{
|
||||
...node("call", "0x1004", "CALL"),
|
||||
inputs: Array.from({ length: argumentCount + 1 }, () => scalar),
|
||||
},
|
||||
],
|
||||
},
|
||||
"0x2000": {
|
||||
operations: [node("return", "0x2008", "RETURN")],
|
||||
...callee,
|
||||
},
|
||||
},
|
||||
{ "0x1004": [{ destination: "0x2000", status: "direct" }] },
|
||||
);
|
||||
const parameterFacts = (count: number) =>
|
||||
Array.from({ length: count }, (_, ordinal) => ({
|
||||
ordinal,
|
||||
name: `param_${String(ordinal + 1)}`,
|
||||
data_type: "/int",
|
||||
}));
|
||||
const missingBindings = {
|
||||
procedure: "0x1000",
|
||||
address: null,
|
||||
reason: "Parameter bindings for 0x2000 are unavailable or omitted",
|
||||
};
|
||||
const edgeKinds = (trace: { edges: readonly { kind: string }[] }) =>
|
||||
trace.edges.map(({ kind }) => kind);
|
||||
|
||||
describe("native value parameter-list facts", () => {
|
||||
it("binds no parameters to a callee whose signature fixes none", async () => {
|
||||
const trace = await traced(
|
||||
singleCallGraph({
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
}).port,
|
||||
{ procedure: "0x1000" },
|
||||
);
|
||||
expect(trace).toMatchObject({
|
||||
traversal: { status: "complete" },
|
||||
truncated: false,
|
||||
unknowns: [],
|
||||
});
|
||||
expect(edgeKinds(trace)).toEqual(["call", "return-binding"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "a decompiler-recovered empty list",
|
||||
parameterList: { basis: "recovered", variadic: false },
|
||||
},
|
||||
{
|
||||
label: "a variadic signature without fixed parameters",
|
||||
parameterList: { basis: "signature", variadic: true },
|
||||
},
|
||||
{
|
||||
label: "a variadic recovered list",
|
||||
parameterList: { basis: "recovered", variadic: true },
|
||||
},
|
||||
{
|
||||
label: "parameter-list facts omitted by older Evidence",
|
||||
parameterList: "unreported",
|
||||
},
|
||||
] as const)(
|
||||
"keeps missing bindings unknown for $label",
|
||||
async ({ parameterList }) => {
|
||||
const trace = await traced(singleCallGraph({ parameterList }).port, {
|
||||
procedure: "0x1000",
|
||||
});
|
||||
expect(trace).toMatchObject({
|
||||
traversal: { status: "complete" },
|
||||
unknowns: [missingBindings],
|
||||
});
|
||||
expect(edgeKinds(trace)).toEqual(["call", "return-binding"]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps missing bindings unknown when the zero-parameter callee is not analyzed", async () => {
|
||||
const graph = singleCallGraph({
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
});
|
||||
expect(
|
||||
await traced(graph.port, { procedure: "0x1000", max_depth: 0 }),
|
||||
).toMatchObject({
|
||||
traversal: { status: "truncated", reasons: ["max_depth"] },
|
||||
unknowns: [
|
||||
{
|
||||
procedure: "0x1000",
|
||||
address: "0x1004",
|
||||
reason: "Call depth budget reached",
|
||||
},
|
||||
missingBindings,
|
||||
],
|
||||
});
|
||||
expect(graph.analyzed).toEqual(["0x1000"]);
|
||||
});
|
||||
|
||||
it("keeps missing bindings unknown when the provider has no API boundary", async () => {
|
||||
const graph = singleCallGraph({
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
});
|
||||
const port: AnalysisOperationPort = {
|
||||
execute: async (operation, parameters, options) => {
|
||||
const execution = await graph.port.execute(
|
||||
operation,
|
||||
parameters,
|
||||
options,
|
||||
);
|
||||
if (!execution.ok || parameters.procedure !== "0x2000")
|
||||
return execution;
|
||||
const dossier = functionDossierSchema.parse(execution.value.result);
|
||||
dossier.native_api = null;
|
||||
return ok({
|
||||
...execution.value,
|
||||
result: jsonValueSchema.parse(dossier),
|
||||
});
|
||||
},
|
||||
};
|
||||
expect(await traced(port, { procedure: "0x1000" })).toMatchObject({
|
||||
unknowns: [missingBindings],
|
||||
});
|
||||
});
|
||||
|
||||
it("binds every argument of a fixed signature and names one the call omits", async () => {
|
||||
const callee = {
|
||||
parameters: parameterFacts(2),
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
} as const;
|
||||
const complete = await traced(singleCallGraph(callee, 2).port, {
|
||||
procedure: "0x1000",
|
||||
});
|
||||
expect(complete).toMatchObject({ unknowns: [] });
|
||||
expect(
|
||||
complete.edges
|
||||
.filter(({ kind }) => kind === "argument-binding")
|
||||
.map(({ target, input_index }) => ({ target, input_index })),
|
||||
).toEqual([
|
||||
{ target: "0x2000/parameter:0", input_index: 1 },
|
||||
{ target: "0x2000/parameter:1", input_index: 2 },
|
||||
]);
|
||||
|
||||
const partial = await traced(singleCallGraph(callee, 1).port, {
|
||||
procedure: "0x1000",
|
||||
});
|
||||
expect(partial).toMatchObject({
|
||||
unknowns: [
|
||||
{
|
||||
procedure: "0x1000",
|
||||
address: null,
|
||||
reason: "Argument 1 for 0x2000 was not recovered",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(edgeKinds(partial)).toEqual([
|
||||
"call",
|
||||
"argument-binding",
|
||||
"return-binding",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
|
||||
import { takesNoParameters } from "../../domain/native/nativeApiBoundary.js";
|
||||
import { nativeCallTargetsSchema } from "../../domain/native/nativeInstruction.js";
|
||||
import {
|
||||
nativeValueTraceInputSchema,
|
||||
@@ -47,6 +48,9 @@ export const traceNativeValues = async (
|
||||
destination: string;
|
||||
reason: "max_depth" | "ambiguous_call_candidates";
|
||||
}[] = [];
|
||||
// Analyzed procedures whose provider fixed an empty, non-variadic parameter
|
||||
// list: calls to them have no parameter bindings to recover.
|
||||
const parameterless = new Set<string>();
|
||||
const budget = createValueTraceBudget(input, unknowns);
|
||||
const { nodes, edges } = budget;
|
||||
let targetSha256: string | undefined;
|
||||
@@ -146,6 +150,8 @@ export const traceNativeValues = async (
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (flow.parameters.length === 0 && takesNoParameters(dossier.native_api))
|
||||
parameterless.add(address);
|
||||
if (flow.truncated)
|
||||
budget.narrow("provider_pcode_truncated", {
|
||||
procedure: address,
|
||||
@@ -377,7 +383,7 @@ export const traceNativeValues = async (
|
||||
}
|
||||
for (const binding of callBindings) {
|
||||
const parameters = parametersByProcedure.get(binding.callee) ?? [];
|
||||
if (parameters.length === 0)
|
||||
if (parameters.length === 0 && !parameterless.has(binding.callee))
|
||||
unknowns.push({
|
||||
procedure: binding.caller,
|
||||
address: null,
|
||||
|
||||
@@ -88,6 +88,8 @@ export const ghidraNativeApiBoundary = () => ({
|
||||
],
|
||||
},
|
||||
parameters: [],
|
||||
// A non-default empty signature is Ghidra's void-input lock.
|
||||
parameter_list: { basis: "signature" as const, variadic: false },
|
||||
jump_tables: [
|
||||
{
|
||||
dispatch_address: "0x401010",
|
||||
|
||||
@@ -46,6 +46,20 @@ const inferredParameterTypeSchema = z.strictObject({
|
||||
ordinal: z.int().min(0),
|
||||
});
|
||||
|
||||
/** Provider basis of a reported parameter list, and whether it admits variadic arguments. */
|
||||
export const nativeParameterListSchema = z.strictObject({
|
||||
basis: z
|
||||
.enum(["signature", "recovered"])
|
||||
.describe(
|
||||
"signature: the provider fixed these parameters from the function's stored, non-default signature (see signature_source); that signature has no other fixed parameters. recovered: the decompiler inferred them from observed uses and may omit parameters.",
|
||||
),
|
||||
variadic: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"Whether the prototype accepts variable arguments after the listed parameters.",
|
||||
),
|
||||
});
|
||||
|
||||
const jumpTableDataSourceSchema = z
|
||||
.object({
|
||||
address: z.string().min(1),
|
||||
@@ -95,6 +109,11 @@ export const availableNativeApiBoundarySchema = z
|
||||
calling_convention: z.string().min(1),
|
||||
return_type: inferredReturnTypeSchema,
|
||||
parameters: z.array(inferredParameterTypeSchema),
|
||||
parameter_list: nativeParameterListSchema
|
||||
.exactOptional()
|
||||
.describe(
|
||||
"Absent when the provider did not report it, as in older Evidence; parameter completeness and variadic arguments then remain unknown.",
|
||||
),
|
||||
jump_tables: z.array(inferredJumpTableSchema),
|
||||
pseudocode: z
|
||||
.object({
|
||||
@@ -132,6 +151,16 @@ export const nativeApiBoundarySchema = z.discriminatedUnion("available", [
|
||||
/** One validated provider-neutral native API boundary observation. */
|
||||
export type NativeApiBoundary = z.infer<typeof nativeApiBoundarySchema>;
|
||||
|
||||
/**
|
||||
* True only when the provider fixed an empty, non-variadic parameter list from
|
||||
* a signature. A recovered, variadic or unreported list leaves arity unknown.
|
||||
*/
|
||||
export const takesNoParameters = (boundary: NativeApiBoundary | null) =>
|
||||
boundary?.available === true &&
|
||||
boundary.parameters.length === 0 &&
|
||||
boundary.parameter_list?.basis === "signature" &&
|
||||
!boundary.parameter_list.variadic;
|
||||
|
||||
const nativeApiInspectionSubstepSchema = z
|
||||
.object({
|
||||
operation: z.enum([
|
||||
|
||||
@@ -68,6 +68,7 @@ const sharedAnalysisSemantics = {
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
instruction_references: "exact-address-incoming-without-synthetic-entry-v2",
|
||||
function_boundary_observations: "ghidra-function-boundary-limitations-v4",
|
||||
native_api_parameter_lists: "signature-or-recovered-basis-varargs-v1",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -734,6 +734,63 @@ describe("Ghidra jump-table mapping contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra parameter-list facts", () => {
|
||||
const boundary = () => {
|
||||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||
if (dossier.native_api?.available !== true)
|
||||
throw new TypeError("Ghidra native API fixture is unavailable");
|
||||
return { dossier, boundary: dossier.native_api };
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ basis: "signature", variadic: false },
|
||||
{ basis: "signature", variadic: true },
|
||||
{ basis: "recovered", variadic: false },
|
||||
])(
|
||||
"admits the bridge's $basis parameter list (variadic $variadic)",
|
||||
(list) => {
|
||||
const { dossier, boundary: api } = boundary();
|
||||
const parsed = parseFunctionValueFixture("analyze_function", {
|
||||
...dossier,
|
||||
native_api: { ...api, parameter_list: list },
|
||||
});
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(
|
||||
functionDossierSchema.parse(parsed.value).native_api,
|
||||
).toMatchObject({ parameter_list: list });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ basis: "declared", variadic: false },
|
||||
{ basis: "signature" },
|
||||
{ basis: "signature", variadic: "false" },
|
||||
{ basis: "signature", variadic: false, complete: true },
|
||||
])("rejects malformed bridge parameter-list facts %j", (list) => {
|
||||
const { dossier, boundary: api } = boundary();
|
||||
expect(
|
||||
parseFunctionValueFixture(
|
||||
"analyze_function",
|
||||
jsonValueSchema.parse({
|
||||
...dossier,
|
||||
native_api: { ...api, parameter_list: list },
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisOutputError" } });
|
||||
});
|
||||
|
||||
it("requires the facts from the bridge while older retained dossiers stay readable", () => {
|
||||
const { dossier, boundary: api } = boundary();
|
||||
const { parameter_list: omitted, ...older } = api;
|
||||
expect(omitted).toEqual({ basis: "signature", variadic: false });
|
||||
const retained = { ...dossier, native_api: older };
|
||||
expect(
|
||||
parseFunctionValueFixture("analyze_function", retained),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisOutputError" } });
|
||||
expect(functionDossierSchema.parse(retained).native_api).toEqual(older);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra function-analysis malformed results", () => {
|
||||
it.each(malformedOutputs())(
|
||||
"rejects malformed %s output",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
|
||||
import {
|
||||
availableNativeApiBoundarySchema,
|
||||
nativeParameterListSchema,
|
||||
unavailableNativeApiBoundarySchema,
|
||||
} from "../domain/native/nativeApiBoundary.js";
|
||||
import {
|
||||
@@ -31,11 +32,15 @@ const ghidraJumpTable = jumpTable.extend({
|
||||
),
|
||||
});
|
||||
|
||||
/** Ghidra API facts carry canonical coordinates and HighFunction provenance. */
|
||||
/**
|
||||
* Ghidra API facts carry canonical coordinates and HighFunction provenance.
|
||||
* The packaged bridge always reports the HighFunction's parameter-list basis.
|
||||
*/
|
||||
export const ghidraNativeApiBoundary = z.discriminatedUnion("available", [
|
||||
unavailableNativeApiBoundarySchema,
|
||||
availableNativeApiBoundarySchema.extend({
|
||||
provenance: z.literal("ghidra-high-function"),
|
||||
parameter_list: nativeParameterListSchema,
|
||||
jump_tables: z.array(ghidraJumpTable),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -7,11 +7,12 @@ import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixt
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
|
||||
// Digests commit to the current shared policy, including the instruction status
|
||||
// revision that prevents reuse of snapshots with the previous ambiguous result.
|
||||
// revision that prevents reuse of snapshots with the previous ambiguous result
|
||||
// and the parameter-list revision that prevents reuse of dossiers without it.
|
||||
const RAW_DIGESTS = {
|
||||
"6502": "8e33776d1cc5c5d77d32923d5978c05ec2d989d00920dd458cc2dafb4c9c0cad",
|
||||
"6502": "8c5ec28bf23077d3f7cbdb033612c14b7314377d23962cadd70171c8adbf2b10",
|
||||
"x86-disabled-analyzer":
|
||||
"d17bddc8ac110c891d30fb989fc7906df421bd9bea5df4aac05a950f50c7bdaf",
|
||||
"71a355ce974a5c9629a6ac0ab24070b905026bd364a7f330cbbaa38538a12c15",
|
||||
} as const;
|
||||
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
|
||||
@@ -307,6 +307,11 @@ describe("Ghidra relationship coverage replay", () => {
|
||||
|
||||
describe("Ghidra snapshot profile admission", () => {
|
||||
it.each([
|
||||
{
|
||||
parameter: "native_api_parameter_lists",
|
||||
current: "signature-or-recovered-basis-varargs-v1",
|
||||
legacy: undefined,
|
||||
},
|
||||
{
|
||||
parameter: "instruction_status",
|
||||
current: "unlisted-address-without-decode-v1",
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js";
|
||||
import {
|
||||
pcodeOperation,
|
||||
pcodeScalar,
|
||||
staticValueGraph,
|
||||
valueTraceDiamond,
|
||||
valueTraceProvider,
|
||||
} from "../../../src/application/native/NativeValueTrace.fixture.js";
|
||||
@@ -34,8 +37,11 @@ const effects = {
|
||||
};
|
||||
|
||||
/** One-shot CLI analysis over the shared trace with a simulated provider. */
|
||||
const traceCli = async () => {
|
||||
const graph = valueTraceDiamond({ sessionSubject: true });
|
||||
const traceCli = async (
|
||||
graph: Pick<ReturnType<typeof staticValueGraph>, "port"> = valueTraceDiamond({
|
||||
sessionSubject: true,
|
||||
}),
|
||||
) => {
|
||||
const directory = await createTestTempDirectory("rea-value-trace-cli-");
|
||||
const path = join(directory, "fixture.hop");
|
||||
await writeFile(path, "synthetic analysis database");
|
||||
@@ -135,3 +141,48 @@ it("reports the CLI last page and its observed node budget separately", async ()
|
||||
reason: "Node budget reached",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports CLI missing bindings only for callees without a fixed empty parameter list", async () => {
|
||||
// 0x1000 calls 0x2000, whose signature fixes no parameters, and 0x3000,
|
||||
// whose empty list was recovered from uses; neither call passes arguments.
|
||||
const zeroArgumentCall = (id: string, address: string) => ({
|
||||
...pcodeOperation(id, address, "CALL"),
|
||||
inputs: [pcodeScalar],
|
||||
});
|
||||
const trace = await traceCli(
|
||||
staticValueGraph(
|
||||
{
|
||||
"0x1000": {
|
||||
operations: [
|
||||
zeroArgumentCall("call-fixed", "0x1004"),
|
||||
zeroArgumentCall("call-recovered", "0x1008"),
|
||||
],
|
||||
},
|
||||
"0x2000": {
|
||||
operations: [pcodeOperation("return", "0x2004", "RETURN")],
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
},
|
||||
"0x3000": {
|
||||
operations: [pcodeOperation("return", "0x3004", "RETURN")],
|
||||
parameterList: { basis: "recovered", variadic: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
"0x1004": [{ destination: "0x2000", status: "direct" }],
|
||||
"0x1008": [{ destination: "0x3000", status: "direct" }],
|
||||
},
|
||||
{ sessionSubject: true },
|
||||
),
|
||||
);
|
||||
expect(await trace(["--json"])).toMatchObject({
|
||||
traversal: { status: "complete" },
|
||||
truncated: false,
|
||||
unknowns: [
|
||||
{
|
||||
procedure: "0x1000",
|
||||
address: null,
|
||||
reason: "Parameter bindings for 0x3000 are unavailable or omitted",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -780,6 +780,10 @@ it.each([
|
||||
"tests/conformance/ghidra/ReaDecodeProbe.java",
|
||||
{ ghidra_bridge: true, runtime: false, package: false },
|
||||
],
|
||||
[
|
||||
"tests/conformance/ghidra/ReaParameterListProbe.java",
|
||||
{ ghidra_bridge: true, runtime: false, package: false },
|
||||
],
|
||||
[
|
||||
"bridge/ghidra/Example.java",
|
||||
{
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
|
||||
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
|
||||
import { nativeApiInspectionResultSchema } from "../../../src/domain/native/nativeApiBoundary.js";
|
||||
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
|
||||
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
|
||||
const caller = "0x401000";
|
||||
const callee = "0x401100";
|
||||
const register = {
|
||||
kind: "register",
|
||||
size_bytes: 4,
|
||||
location: "register:0x0",
|
||||
constant_hex: null,
|
||||
} as const;
|
||||
|
||||
/** A controlled bridge wire dossier; it never launches Ghidra. */
|
||||
const bridgeDossier = (
|
||||
address: string,
|
||||
name: string,
|
||||
operations: readonly Record<string, unknown>[],
|
||||
api: { readonly signatureSource: string; readonly parameterList: unknown },
|
||||
) => {
|
||||
const base = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||
if (
|
||||
base.native_api?.available !== true ||
|
||||
base.native_value_flow?.available !== true
|
||||
)
|
||||
throw new Error("Invalid Ghidra fixture");
|
||||
const end = `0x${(Number.parseInt(address, 16) + 5).toString(16)}`;
|
||||
const boundary: Record<string, unknown> = {
|
||||
...base.native_api,
|
||||
signature_source: api.signatureSource,
|
||||
parameters: [],
|
||||
parameter_list: api.parameterList,
|
||||
jump_tables: [],
|
||||
};
|
||||
if (api.parameterList === undefined) delete boundary.parameter_list;
|
||||
return jsonValueSchema.parse({
|
||||
...base,
|
||||
procedure: {
|
||||
...base.procedure,
|
||||
address,
|
||||
name,
|
||||
body: { ...base.procedure.body, ranges: [{ start: address, end }] },
|
||||
signature: null,
|
||||
},
|
||||
native_api: boundary,
|
||||
native_value_flow: {
|
||||
...base.native_value_flow,
|
||||
operations: operations.map((operation, sequence) => ({
|
||||
id: `${address}#${String(sequence)}`,
|
||||
sequence,
|
||||
is_dead: false,
|
||||
block_membership: "member",
|
||||
...operation,
|
||||
})),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** `audit_call_zero` calls `audit_zero`, which takes the callee's parameter facts. */
|
||||
const connect = (calleeApi: Parameters<typeof bridgeDossier>[3]) =>
|
||||
connectGhidraMcp("ghidra-parameter-list", async (operation, input) => {
|
||||
if (operation === "resolve_native_call_targets")
|
||||
return ok(
|
||||
jsonValueSchema.parse({
|
||||
call_site: "0x401004",
|
||||
procedure: callee,
|
||||
status: "direct",
|
||||
mechanism: "direct",
|
||||
targets: [
|
||||
{
|
||||
address: callee,
|
||||
procedure: callee,
|
||||
status: "direct",
|
||||
basis: "provider-reference",
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
limitations: [],
|
||||
}),
|
||||
);
|
||||
if (operation !== "analyze_function")
|
||||
throw new Error(`Unexpected controlled Ghidra operation: ${operation}`);
|
||||
const value =
|
||||
input.procedure === callee
|
||||
? bridgeDossier(
|
||||
callee,
|
||||
"audit_zero",
|
||||
[
|
||||
{
|
||||
address: "0x401104",
|
||||
opcode: "RETURN",
|
||||
inputs: [
|
||||
{
|
||||
kind: "constant",
|
||||
size_bytes: 8,
|
||||
location: null,
|
||||
constant_hex: "0",
|
||||
},
|
||||
register,
|
||||
],
|
||||
output: null,
|
||||
},
|
||||
],
|
||||
calleeApi,
|
||||
)
|
||||
: bridgeDossier(
|
||||
caller,
|
||||
"audit_call_zero",
|
||||
[
|
||||
{
|
||||
address: "0x401004",
|
||||
opcode: "CALL",
|
||||
inputs: [
|
||||
{
|
||||
kind: "address",
|
||||
size_bytes: 8,
|
||||
location: callee,
|
||||
constant_hex: null,
|
||||
},
|
||||
],
|
||||
output: register,
|
||||
},
|
||||
],
|
||||
{
|
||||
signatureSource: "default",
|
||||
parameterList: { basis: "recovered", variadic: false },
|
||||
},
|
||||
);
|
||||
return ok(jsonValueSchema.parse({ value, limitations: [] }));
|
||||
});
|
||||
|
||||
const missingBindings = `Parameter bindings for ${callee} are unavailable or omitted`;
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "a user-defined zero-parameter signature",
|
||||
signatureSource: "user_defined",
|
||||
parameterList: { basis: "signature", variadic: false },
|
||||
unknowns: [],
|
||||
},
|
||||
{
|
||||
label: "a decompiler-recovered empty list",
|
||||
signatureSource: "default",
|
||||
parameterList: { basis: "recovered", variadic: false },
|
||||
unknowns: [missingBindings],
|
||||
},
|
||||
{
|
||||
label: "a user-defined variadic signature without fixed parameters",
|
||||
signatureSource: "user_defined",
|
||||
parameterList: { basis: "signature", variadic: true },
|
||||
unknowns: [missingBindings],
|
||||
},
|
||||
] as const)(
|
||||
"traces $label through the Ghidra adapter and advertised MCP contracts",
|
||||
async ({ signatureSource, parameterList, unknowns }) => {
|
||||
const harness = await connect({ signatureSource, parameterList });
|
||||
try {
|
||||
const call = async (
|
||||
name: "trace_native_values" | "inspect_native_api",
|
||||
arguments_: Record<string, unknown>,
|
||||
) => {
|
||||
const { toolDefinition, ajv, outputSchema } =
|
||||
await advertisedMcpToolSchemas(harness.mcp, name);
|
||||
const reply = await harness.mcp.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ toolDefinition },
|
||||
);
|
||||
expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true);
|
||||
expect(
|
||||
ajv.validate(outputSchema, reply.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
return sessionEvidence(harness.session, reply.structuredContent)
|
||||
.normalized_result;
|
||||
};
|
||||
const trace = nativeValueTraceSchema.parse(
|
||||
await call("trace_native_values", { procedure: caller, max_depth: 1 }),
|
||||
);
|
||||
expect(trace).toMatchObject({
|
||||
traversal: { status: "complete" },
|
||||
truncated: false,
|
||||
});
|
||||
expect(trace.unknowns.map(({ reason }) => reason)).toEqual(unknowns);
|
||||
expect(trace.edges.map(({ kind }) => kind)).toEqual([
|
||||
"call",
|
||||
"return-binding",
|
||||
]);
|
||||
expect(
|
||||
nativeApiInspectionResultSchema.parse(
|
||||
await call("inspect_native_api", { procedure: callee }),
|
||||
).boundary,
|
||||
).toMatchObject({
|
||||
available: true,
|
||||
signature_source: signatureSource,
|
||||
parameters: [],
|
||||
parameter_list: parameterList,
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
it("refuses a bridge API boundary that omits its parameter-list basis", async () => {
|
||||
const harness = await connect({
|
||||
signatureSource: "user_defined",
|
||||
parameterList: undefined,
|
||||
});
|
||||
try {
|
||||
const reply = await harness.mcp.callTool({
|
||||
name: "inspect_native_api",
|
||||
arguments: { procedure: callee },
|
||||
});
|
||||
expect(reply.isError).toBe(true);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
}, 30_000);
|
||||
@@ -19,6 +19,13 @@ binaries and Ghidra projects are never committed.
|
||||
language's default context and checks each mnemonic, length and optional
|
||||
complete operand. The `ghidra-bridge-sources` CI job runs it for `ARM:LE:32:Cortex`,
|
||||
`ARM:LE:32:v8-m` and `Xtensa:LE:32:default`; the bytes are never executed.
|
||||
- `ReaParameterListProbe.java` decompiles source-owned x86-64 functions, two
|
||||
with Ghidra's default signature and three with authored `int f(void)`,
|
||||
`int f(...)` and `int f(int x)` signatures, through the production bridge.
|
||||
The `ghidra-bridge-sources` CI job checks each `native_api.parameter_list`
|
||||
basis and varargs flag, and that value-flow parameters match the boundary.
|
||||
It does not establish how analysis or imports set signatures; the bytes are
|
||||
never executed.
|
||||
- `inventory.c` produces separate x86-64 debug and stripped ELF executables.
|
||||
It covers imports, an external `puts` function, linker thunks, source symbols,
|
||||
direct calls, a targetless callback call, two referenced strings, and a
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//@category REA Verification
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.script.GhidraScriptUtil;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.IntegerDataType;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.ParameterImpl;
|
||||
import ghidra.program.model.symbol.SourceType;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Real x86-64 decoding, function signatures and decompilation. Two functions
|
||||
* keep Ghidra's default signature, one without and one with used argument
|
||||
* registers; three get user-defined int f(void), int f(...) and int f(int x)
|
||||
* signatures, as an analyst would author them. The production bridge must
|
||||
* report each HighFunction's parameter-list basis and varargs flag, with the
|
||||
* value-flow parameters matching the API boundary. The bytes are never executed.
|
||||
*/
|
||||
public class ReaParameterListProbe extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] args = getScriptArgs();
|
||||
require(args.length == 1, "Expected the exact production bridge directory");
|
||||
var source = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
|
||||
require(source != null && new File(source.getAbsolutePath()).getCanonicalFile().equals(
|
||||
new File(args[0], "ReaGhidraBridge.java").getCanonicalFile()), "Production bridge source");
|
||||
GhidraScript bridge = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter);
|
||||
bridge.set(getState(), getControls());
|
||||
require(currentProgram.getLanguageID().getIdAsString().equals("x86:LE:64:default"),
|
||||
"x86-64 fixture language");
|
||||
Address base = currentProgram.getMemory().getMinAddress();
|
||||
Function defaultEmpty = function(base); // mov eax, 1; ret
|
||||
Function userVoid = function(base.add(0x08)); // mov eax, 1; ret
|
||||
Function userVarargs = function(base.add(0x10)); // mov eax, 1; ret
|
||||
Function userOne = function(base.add(0x18)); // mov eax, 1; ret
|
||||
Function defaultUsed = function(base.add(0x20)); // lea eax, [rdi + rcx]; ret
|
||||
author(userVoid, false);
|
||||
author(userVarargs, true);
|
||||
author(userOne, false, new ParameterImpl("x", IntegerDataType.dataType, currentProgram));
|
||||
require(defaultEmpty.getSignatureSource() == SourceType.DEFAULT &&
|
||||
defaultUsed.getSignatureSource() == SourceType.DEFAULT, "Default fixture signatures");
|
||||
|
||||
Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
|
||||
space.setAccessible(true);
|
||||
space.set(null, base.getAddressSpace());
|
||||
Field timeout = bridge.getClass().getDeclaredField("decompileTimeoutSeconds");
|
||||
timeout.setAccessible(true);
|
||||
timeout.setInt(bridge, 60);
|
||||
Field decompiler = bridge.getClass().getDeclaredField("decompiler");
|
||||
decompiler.setAccessible(true);
|
||||
invoke(method(bridge, "initializeDecompiler"), bridge);
|
||||
Method decompile = method(bridge, "decompile", Function.class);
|
||||
Method boundary = method(bridge, "nativeApiBoundary", Function.class, DecompileResults.class);
|
||||
Method flow = method(bridge, "nativeValueFlow", DecompileResults.class);
|
||||
try {
|
||||
// -1 requires at least one recovered parameter; its basis is the fact under test.
|
||||
check(bridge, decompile, boundary, flow, defaultEmpty, "recovered", false, 0);
|
||||
check(bridge, decompile, boundary, flow, userVoid, "signature", false, 0);
|
||||
check(bridge, decompile, boundary, flow, userVarargs, "signature", true, 0);
|
||||
check(bridge, decompile, boundary, flow, userOne, "signature", false, 1);
|
||||
check(bridge, decompile, boundary, flow, defaultUsed, "recovered", false, -1);
|
||||
} finally {
|
||||
((DecompInterface) decompiler.get(bridge)).dispose();
|
||||
}
|
||||
println("REA_PARAMETER_LIST_OK default=recovered void=signature varargs=variadic one=signature used=recovered");
|
||||
}
|
||||
|
||||
private Function function(Address entry) throws Exception {
|
||||
if (getInstructionAt(entry) == null) disassemble(entry);
|
||||
require(getInstructionAt(entry) != null, "Decoded entry " + entry);
|
||||
Function function = createFunction(entry, null);
|
||||
require(function != null, "Created function at " + entry);
|
||||
return function;
|
||||
}
|
||||
|
||||
private static void author(Function function, boolean varargs, Parameter... parameters)
|
||||
throws Exception {
|
||||
function.setReturnType(IntegerDataType.dataType, SourceType.USER_DEFINED);
|
||||
function.replaceParameters(Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS, true,
|
||||
SourceType.USER_DEFINED, parameters);
|
||||
function.setVarArgs(varargs);
|
||||
require(function.getSignatureSource() == SourceType.USER_DEFINED &&
|
||||
function.getParameterCount() == parameters.length && function.hasVarArgs() == varargs,
|
||||
"Authored signature " + function.getSignature().getPrototypeString());
|
||||
}
|
||||
|
||||
private void check(Object bridge, Method decompile, Method boundary, Method flow,
|
||||
Function function, String basis, boolean variadic, int parameters) throws Exception {
|
||||
DecompileResults results = (DecompileResults) invoke(decompile, bridge, function);
|
||||
JsonObject api = (JsonObject) invoke(boundary, bridge, function, results);
|
||||
JsonObject values = (JsonObject) invoke(flow, bridge, results);
|
||||
String label = function.getSignature().getPrototypeString();
|
||||
println("REA_PARAMETER_LIST " + label + " " + api.get("parameter_list") +
|
||||
" parameters=" + api.getAsJsonArray("parameters").size());
|
||||
require(api.get("available").getAsBoolean(), "Available API boundary for " + label);
|
||||
JsonObject list = api.getAsJsonObject("parameter_list");
|
||||
require(list != null && list.get("basis").getAsString().equals(basis),
|
||||
"Parameter-list basis " + basis + " for " + label);
|
||||
require(list.get("variadic").getAsBoolean() == variadic,
|
||||
"Varargs flag " + variadic + " for " + label);
|
||||
int count = api.getAsJsonArray("parameters").size();
|
||||
require(parameters < 0 ? count > 0 : count == parameters, "Parameter count for " + label);
|
||||
require(values.get("available").getAsBoolean() &&
|
||||
values.getAsJsonArray("parameters").size() == count,
|
||||
"Value-flow parameters match the API boundary for " + label);
|
||||
}
|
||||
|
||||
private static Method method(Object owner, String name, Class<?>... types)
|
||||
throws NoSuchMethodException {
|
||||
Method method = owner.getClass().getDeclaredMethod(name, types);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static Object invoke(Method method, Object owner, Object... values) throws Exception {
|
||||
try { return method.invoke(owner, values); }
|
||||
catch (InvocationTargetException failure) {
|
||||
if (failure.getCause() instanceof Exception cause) throw cause;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String label) {
|
||||
if (!condition) throw new IllegalStateException(label);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user