fix(ghidra): stop every flow at the literal budget and route omissions

Ending one flow from the evaluator leaves other queued flows running, so
the budget now records a shared stop that every flow checks before it
examines another instruction. A record that exceeds the budget is routed
by that capacity result rather than by whether text was decoded, so a
small literal over budget becomes an omission instead of a literal.

When coverage is partial, the per site comparison keeps each literal's
representation as well as its value, matching the full strings_names
projection.
This commit is contained in:
Omisakin Joshua committed 2026-10-12 12:07:40 +01:00
1 parent 560257b00e
commit 59be5357df
3 files changed
+59 -6

No files matched your search

+17 -3
View File
@@ -3350,6 +3350,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
propagator.flowConstants(function.getEntryPoint(), function.getBody(), new ContextEvaluatorAdapter() {
@Override
public boolean evaluateContextBefore(VarnodeContext context, Instruction instruction) {
// Returning true ends only the current flow, so every queued flow checks the shared stop.
if (budget.stopped()) return true;
for (Register[] pair : swiftStringPairs(function, instruction, registers)) {
Long word = registerConstant(context, pair[0]);
Long object = registerConstant(context, pair[1]);
@@ -3376,7 +3378,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
}
if (value == null && !overCapacity) continue;
JsonObject item = value == null
JsonObject item = overCapacity
? swiftStringOmission(layout, word, object)
: swiftStringItem(value, layout, word, object);
item.addProperty("source_address", canonicalAddress(instruction.getAddress()));
@@ -3384,8 +3386,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
pairRegisters.add(pair[0].getName());
pairRegisters.add(pair[1].getName());
item.add("registers", pairRegisters);
(value == null ? omitted : observed).put(key, item);
if (overCapacity) return true;
(overCapacity ? omitted : observed).put(key, item);
if (overCapacity) {
budget.stop();
return true;
}
}
return false;
}
@@ -3557,6 +3562,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
private static final long BYTES_PER_LITERAL_BYTE = 2 + 6 * 2 * 2 + 6 * 2 + 6;
private static final long BYTES_PER_RECORD = 4096;
private long remaining;
private boolean stopped;
SwiftLiteralBudget() {
Runtime runtime = Runtime.getRuntime();
@@ -3571,6 +3577,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
remaining -= BYTES_PER_RECORD + bytes * BYTES_PER_LITERAL_BYTE;
return true;
}
void stop() {
stopped = true;
}
boolean stopped() {
return stopped;
}
}
// Swift String values may contain any scalar, including controls; JSON escapes them.
+37
View File
@@ -645,7 +645,9 @@ describe("function comparison Swift string literals", () => {
stringsDimension("7", withLiterals("0x1000", []), "8", named),
).toMatchObject({ status: "changed" });
});
});
describe("function comparison partial Swift literal coverage", () => {
it("does not equate a partial literal record with a complete one", () => {
const partial = functionDossierSchema.parse({
...withLiterals("0x2000", ["SleepDisabled"]),
@@ -711,6 +713,41 @@ describe("function comparison Swift string literals", () => {
).toMatchObject({ status: "unknown" });
});
it("reports a representation change at a shared site despite partial coverage", () => {
const omission = {
source_address: "0x1004",
representation: "small",
address: null,
byte_length: 4,
reason: "capacity",
registers: ["x0", "x1"],
words: ["0x732f4220", "0xe400000000000000"],
} as const;
const small = functionDossierSchema.parse({
...withLiterals("0x1000", ["SleepDisabled"]),
swift_string_literal_omissions: [omission],
});
const large = functionDossierSchema.parse({
...dossier("return 0;", "0x2000"),
swift_string_literals: [
{
source_address: "0x2000",
value: "SleepDisabled",
representation: "large",
address: "0x3000",
registers: ["x0", "x1"],
words: ["0xd00000000000000d", "0x8000000000002fe0"],
},
],
swift_string_literal_omissions: [
{ ...omission, source_address: "0x2004" },
],
});
expect(stringsDimension("1", small, "2", large)).toMatchObject({
status: "changed",
});
});
it("reports a conflicting literal at a shared use site despite partial coverage", () => {
const partial = (base: "0x1000" | "0x2000", value: string) =>
functionDossierSchema.parse({
@@ -232,15 +232,17 @@ const swiftLiteralSites = (snapshot: FunctionSnapshot) => {
site(sourceAddress, registers),
),
);
// Keep the same literal facts the full strings_names projection compares.
const values = new Map<string, string[]>();
for (const literal of snapshot.dossier.swift_string_literals ?? []) {
const key = site(literal.source_address, literal.registers);
if (key === null) continue;
values.set(
key,
[...(values.get(key) ?? []), literal.value].sort(
compareUnicodeCodePoints,
),
[
...(values.get(key) ?? []),
canonicalJson([literal.value, literal.representation]),
].sort(compareUnicodeCodePoints),
);
}
const complete =