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:
1 parent
560257b00e
commit
59be5357df
3 files changed
+59
-6
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in new issue
Block a user