test(ghidra): cover Swift decoder rejection and capacity branches
This commit is contained in:
parent
f8f18e0bc7
commit
27f1b367b9
|
|
@ -3436,13 +3436,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
* The canonical empty string is a small literal with a zero count.
|
||||
*/
|
||||
private SwiftStringLiteralDecode swiftStringLiterals(Function function) throws Exception {
|
||||
return swiftStringLiterals(function, new SwiftLiteralBudget());
|
||||
}
|
||||
|
||||
// The budget is owned by one decode run; an explicit budget also permits deterministic producer regressions.
|
||||
private SwiftStringLiteralDecode swiftStringLiterals(Function function, SwiftLiteralBudget budget) throws Exception {
|
||||
Register[] registers = new Register[31];
|
||||
for (int index = 0; index < registers.length; index++) {
|
||||
registers[index] = currentProgram.getRegister("x" + index);
|
||||
}
|
||||
TreeMap<String, JsonObject> observed = new TreeMap<>();
|
||||
TreeMap<String, JsonObject> omitted = new TreeMap<>();
|
||||
SwiftLiteralBudget budget = new SwiftLiteralBudget();
|
||||
Map<String, Boolean> selectedValues = new HashMap<>();
|
||||
SymbolicPropogator propagator = new SymbolicPropogator(currentProgram, false);
|
||||
propagator.flowConstants(function.getEntryPoint(), function.getBody(), new ContextEvaluatorAdapter() {
|
||||
|
|
@ -3687,6 +3691,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
remaining = Math.max(0, (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / 2);
|
||||
}
|
||||
|
||||
SwiftLiteralBudget(long remaining) {
|
||||
if (remaining < 0) throw new IllegalArgumentException("Swift literal budget must be nonnegative");
|
||||
this.remaining = remaining;
|
||||
}
|
||||
|
||||
boolean takeState() {
|
||||
if (remaining < BYTES_PER_RECORD) return false;
|
||||
remaining -= BYTES_PER_RECORD;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import {
|
||||
copyFile,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
|
@ -50,6 +57,10 @@ const literalSource = fileURLToPath(
|
|||
);
|
||||
const binary = join(root, "swift-inventory");
|
||||
const literalBinary = join(root, "swift-literals");
|
||||
const literalAssembly = fileURLToPath(
|
||||
new URL("../tests/conformance/swift/decoder-safeguards.S", import.meta.url),
|
||||
);
|
||||
const literalObject = join(root, "decoder-safeguards.o");
|
||||
const reports = [];
|
||||
let literalReport;
|
||||
try {
|
||||
|
|
@ -59,6 +70,19 @@ try {
|
|||
await mkdir(join(root, "copied"));
|
||||
const copied = join(root, "copied", "swift-inventory");
|
||||
await copyFile(binary, copied);
|
||||
await execute(
|
||||
"xcrun",
|
||||
[
|
||||
"clang",
|
||||
"-target",
|
||||
"arm64-apple-macosx13.0",
|
||||
"-c",
|
||||
literalAssembly,
|
||||
"-o",
|
||||
literalObject,
|
||||
],
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
// Decoding covers AArch64 register pairs, so build that slice on any Mac host.
|
||||
await execute(
|
||||
"swiftc",
|
||||
|
|
@ -67,11 +91,13 @@ try {
|
|||
"-target",
|
||||
"arm64-apple-macosx13.0",
|
||||
literalSource,
|
||||
literalObject,
|
||||
"-o",
|
||||
literalBinary,
|
||||
],
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await verifyDecoderSafeguards();
|
||||
await client.connect(transport);
|
||||
for (const [variant, path] of [
|
||||
["debug", binary],
|
||||
|
|
@ -227,3 +253,54 @@ async function verifyStringLiterals() {
|
|||
await call("close_binary", {});
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise the actual Java producer inside a disposable Ghidra database. The
|
||||
// explicit private budget boundary makes exhaustion deterministic, without a
|
||||
// production CLI/MCP option or dependence on the verification host's free heap.
|
||||
async function verifyDecoderSafeguards() {
|
||||
const scripts = join(root, "decoder-scripts");
|
||||
await mkdir(scripts);
|
||||
const bridgeSources = fileURLToPath(
|
||||
new URL("../bridge/ghidra/", import.meta.url),
|
||||
);
|
||||
for (const name of await readdir(bridgeSources)) {
|
||||
if (name.endsWith(".java"))
|
||||
await copyFile(join(bridgeSources, name), join(scripts, name));
|
||||
}
|
||||
await copyFile(
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/swift/ReaSwiftDecoderRegressions.java",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
join(scripts, "ReaSwiftDecoderRegressions.java"),
|
||||
);
|
||||
const report = join(root, "decoder-safeguards.txt");
|
||||
await execute(
|
||||
installation.analyzeHeadlessPath,
|
||||
[
|
||||
root,
|
||||
"decoder-regressions",
|
||||
"-import",
|
||||
literalBinary,
|
||||
"-scriptPath",
|
||||
scripts,
|
||||
"-postScript",
|
||||
"ReaSwiftDecoderRegressions.java",
|
||||
report,
|
||||
"-deleteProject",
|
||||
"-max-cpu",
|
||||
"2",
|
||||
],
|
||||
{
|
||||
env: { ...process.env, JAVA_HOME: installation.javaHome },
|
||||
timeout: 120_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
await readFile(report, "utf8"),
|
||||
"REA_SWIFT_DECODER_SAFEGUARDS_OK\n",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
// Producer regressions run inside a real, ephemeral Ghidra analysis of the
|
||||
// source-owned AArch64 fixture. No target code is executed and no decoded JSON
|
||||
// fixture substitutes for the Java decoder under test.
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.app.util.headless.HeadlessScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionIterator;
|
||||
import ghidra.program.model.listing.Instruction;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
|
||||
public class ReaSwiftDecoderRegressions extends HeadlessScript {
|
||||
private ReaGhidraBridge decoder;
|
||||
private Class<?> budgetClass;
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
decoder = new ReaGhidraBridge();
|
||||
setInherited(decoder, "currentProgram", currentProgram);
|
||||
setInherited(decoder, "monitor", monitor);
|
||||
setInherited(decoder, "sessionDefaultAddressSpace", currentProgram.getAddressFactory().getDefaultAddressSpace());
|
||||
budgetClass = Class.forName("ReaGhidraBridge$SwiftLiteralBudget", true, decoder.getClass().getClassLoader());
|
||||
Function copied = function("rea_swift_decoder_copied");
|
||||
JsonArray copiedValues = literals(decode(copied, 65536));
|
||||
require(copiedValues.size() > 0, "copied constant pair was not decoded");
|
||||
for (var element : copiedValues) {
|
||||
require(element.getAsJsonObject().get("value").getAsString().equals("ok"), "copied words invented text");
|
||||
}
|
||||
for (String name : new String[] {"select", "selected_copy", "padding", "utf8"}) {
|
||||
Object result = decode(function("rea_swift_decoder_" + name), 65536);
|
||||
require(literals(result).isEmpty(), name + " produced an invented Swift literal");
|
||||
require(omissions(result).isEmpty(), name + " was confused with capacity exhaustion");
|
||||
}
|
||||
for (String name : new String[] {"select", "selected_copy"}) {
|
||||
Function selected = function("rea_swift_decoder_" + name);
|
||||
require((Boolean) invoke("selectedValue", new Class<?>[] {Function.class, Instruction.class, Register.class}, selected, returnInstruction(selected), currentProgram.getRegister("x0")),
|
||||
name + " did not retain conditional-select provenance");
|
||||
}
|
||||
require(invoke("swiftLiteralLayout", new Class<?>[] {long.class, long.class}, 0x6261L, 0xe100000000000000L) == null,
|
||||
"nonzero small-string padding was accepted");
|
||||
require(invoke("swiftLiteralLayout", new Class<?>[] {long.class, long.class}, 0xc0L, 0xa100000000000000L) == null,
|
||||
"malformed small-string UTF-8 was accepted");
|
||||
rejectLarge("rea_swift_decoder_bad_terminator", 0xd000000000000003L);
|
||||
rejectLarge("rea_swift_decoder_bad_large_utf8", 0x1000000000000001L);
|
||||
Object exhausted = decode(copied, 0);
|
||||
require(literals(exhausted).isEmpty(), "capacity failure published decoded small text");
|
||||
JsonArray omitted = omissions(exhausted);
|
||||
require(omitted.size() == 1, "capacity failure did not stop subsequent use sites");
|
||||
JsonObject omission = omitted.get(0).getAsJsonObject();
|
||||
require(omission.get("reason").getAsString().equals("capacity"), "capacity omission lost its reason");
|
||||
require(!omission.has("value"), "omission claimed a retained literal value");
|
||||
require(getScriptArgs().length == 1, "Expected owned result path");
|
||||
Files.writeString(Path.of(getScriptArgs()[0]), "REA_SWIFT_DECODER_SAFEGUARDS_OK\n");
|
||||
}
|
||||
|
||||
private Object decode(Function function, long remaining) throws Exception {
|
||||
Constructor<?> constructor = budgetClass.getDeclaredConstructor(long.class);
|
||||
constructor.setAccessible(true);
|
||||
return invoke("swiftStringLiterals", new Class<?>[] {Function.class, budgetClass}, function, constructor.newInstance(remaining));
|
||||
}
|
||||
|
||||
private void rejectLarge(String name, long word) throws Exception {
|
||||
Address address = symbol(name).getAddress();
|
||||
long object = 0x8000000000000000L | ((address.getOffset() - 0x20) & 0x00ffffffffffffffL);
|
||||
Object layout = invoke("swiftLiteralLayout", new Class<?>[] {long.class, long.class}, word, object);
|
||||
require(layout != null, "large negative fixture did not reach the memory-text decoder");
|
||||
require(invoke("swiftLargeLiteralText", new Class<?>[] {layout.getClass()}, layout) == null,
|
||||
name + " was accepted as literal text");
|
||||
}
|
||||
|
||||
private Instruction returnInstruction(Function function) {
|
||||
InstructionIterator instructions = currentProgram.getListing().getInstructions(function.getBody(), true);
|
||||
while (instructions.hasNext()) {
|
||||
Instruction instruction = instructions.next();
|
||||
if (instruction.getMnemonicString().equalsIgnoreCase("ret")) return instruction;
|
||||
}
|
||||
throw new AssertionError("Fixture has no return instruction");
|
||||
}
|
||||
|
||||
private Function function(String name) {
|
||||
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||
Function found = null;
|
||||
while (functions.hasNext()) {
|
||||
Function candidate = functions.next();
|
||||
if (!candidate.getName().equals(name) && !candidate.getName().equals("_" + name)) continue;
|
||||
require(found == null, "ambiguous fixture function " + name);
|
||||
found = candidate;
|
||||
}
|
||||
require(found != null, "missing fixture function " + name);
|
||||
return found;
|
||||
}
|
||||
|
||||
private Symbol symbol(String name) {
|
||||
SymbolIterator symbols = currentProgram.getSymbolTable().getAllSymbols(true);
|
||||
Symbol found = null;
|
||||
while (symbols.hasNext()) {
|
||||
Symbol candidate = symbols.next();
|
||||
if (!candidate.getName().equals(name) && !candidate.getName().equals("_" + name)) continue;
|
||||
require(found == null, "ambiguous fixture symbol " + name);
|
||||
found = candidate;
|
||||
}
|
||||
require(found != null, "missing fixture symbol " + name);
|
||||
return found;
|
||||
}
|
||||
|
||||
private Object invoke(String name, Class<?>[] types, Object... arguments) throws Exception {
|
||||
Method method = ReaGhidraBridge.class.getDeclaredMethod(name, types);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(decoder, arguments);
|
||||
}
|
||||
|
||||
private JsonArray literals(Object result) throws Exception { return array(result, "literals"); }
|
||||
private JsonArray omissions(Object result) throws Exception { return array(result, "omissions"); }
|
||||
private JsonArray array(Object result, String name) throws Exception {
|
||||
Method method = result.getClass().getDeclaredMethod(name);
|
||||
method.setAccessible(true);
|
||||
return (JsonArray) method.invoke(result);
|
||||
}
|
||||
|
||||
private void setInherited(Object target, String name, Object value) throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||||
try {
|
||||
Field field = type.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException nextOwner) { }
|
||||
}
|
||||
throw new AssertionError("Missing Ghidra state field " + name);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/* Static-analysis fixture only. Never executed. Deliberate malformed ABI words
|
||||
* cannot be produced by a well-formed Swift source literal. */
|
||||
.text
|
||||
.p2align 2
|
||||
.globl _rea_swift_decoder_copied
|
||||
_rea_swift_decoder_copied:
|
||||
mov x5, #0x6b6f
|
||||
movz x6, #0xe200, lsl #48
|
||||
mov x0, x5
|
||||
mov x1, x6
|
||||
stp x0, x1, [sp, #-16]!
|
||||
ldp x0, x1, [sp], #16
|
||||
ret
|
||||
|
||||
.p2align 2
|
||||
.globl _rea_swift_decoder_select
|
||||
_rea_swift_decoder_select:
|
||||
mov x2, #0x61
|
||||
mov x3, #0x6262
|
||||
movz x6, #0xe100, lsl #48
|
||||
movz x7, #0xe200, lsl #48
|
||||
cmp w4, #0
|
||||
csel x0, x2, x3, eq
|
||||
csel x1, x6, x7, eq
|
||||
ret
|
||||
|
||||
.p2align 2
|
||||
.globl _rea_swift_decoder_selected_copy
|
||||
_rea_swift_decoder_selected_copy:
|
||||
mov x2, #0x61
|
||||
mov x3, #0x62
|
||||
cmp w4, #0
|
||||
csel x5, x2, x3, eq
|
||||
mov x0, x5
|
||||
movz x1, #0xe100, lsl #48
|
||||
ret
|
||||
|
||||
.p2align 2
|
||||
.globl _rea_swift_decoder_padding
|
||||
_rea_swift_decoder_padding:
|
||||
mov x0, #0x6261
|
||||
movz x1, #0xe100, lsl #48
|
||||
ret
|
||||
|
||||
.p2align 2
|
||||
.globl _rea_swift_decoder_utf8
|
||||
_rea_swift_decoder_utf8:
|
||||
mov x0, #0xc0
|
||||
movz x1, #0xa100, lsl #48
|
||||
ret
|
||||
|
||||
.section __DATA,__const
|
||||
.globl _rea_swift_decoder_bad_terminator
|
||||
_rea_swift_decoder_bad_terminator:
|
||||
.byte 0x61, 0x62, 0x63, 0x78
|
||||
.globl _rea_swift_decoder_bad_large_utf8
|
||||
_rea_swift_decoder_bad_large_utf8:
|
||||
.byte 0xc0, 0
|
||||
Loading…
Reference in New Issue