From 59be5357df07fcf4a2dbab08ecbd100179a40785 Mon Sep 17 00:00:00 2001 From: Omisakin Joshua <117924167+thefolahan@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:07:40 +0100 Subject: [PATCH] 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. --- bridge/ghidra/ReaGhidraBridge.java | 20 ++++++++-- src/domain/functionComparison.test.ts | 37 +++++++++++++++++++ src/domain/functionComparisonNormalization.ts | 8 ++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index 17de12f503..b6fff9f258 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -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. diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index 629a2524db..6795749e49 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -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({ diff --git a/src/domain/functionComparisonNormalization.ts b/src/domain/functionComparisonNormalization.ts index 3b14b4d2da..e0458ffdd9 100644 --- a/src/domain/functionComparisonNormalization.ts +++ b/src/domain/functionComparisonNormalization.ts @@ -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(); 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 =