mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
fix(ghidra): stage NativeAOT report until recovery completes (#9611)
* fix(ghidra): stage nativeaot recovery report until complete * test(ghidra): exercise nativeaot rollback reporting
This commit is contained in:
1 parent
e304de99bc
commit
8c64eb511f
21 files changed
+306
-5
No files matched your search
@@ -78,14 +78,9 @@ public final class NativeAotExtension {
|
||||
new FrozenObjectAnnotator(program, manager).analyze(located.directory(), scan.getPointerLocations(), monitor, log);
|
||||
}
|
||||
monitor.checkCancelled();
|
||||
report.addProperty("method_tables", manager.getMethodTableCount());
|
||||
JsonObject coverage = auditFrozenObjects(program, scan.getPointerLocations(), monitor);
|
||||
report.add("coverage", coverage);
|
||||
if (coverage.get("frozen_objects_annotated").getAsInt() != coverage.get("frozen_object_candidates").getAsInt())
|
||||
diagnostics.add("Some frozen-object candidates lack committed instance annotations; inspect coverage counts and Ghidra diagnostics.");
|
||||
report.addProperty("status", diagnostics.isEmpty() ? "complete" : "partial");
|
||||
report.add("reason", JsonNull.INSTANCE);
|
||||
report.add("derived_memory", derivedRange.deepCopy());
|
||||
JsonArray tables = new JsonArray();
|
||||
for (var mt : manager.getMethodTables()) {
|
||||
monitor.checkCancelled();
|
||||
@@ -94,6 +89,13 @@ public final class NativeAotExtension {
|
||||
identity.addProperty("type", mt.getMTType().getPathName());
|
||||
tables.add(identity);
|
||||
}
|
||||
// Publish recovery facts only after every fallible recovery and audit step.
|
||||
// A failed transaction is rolled back, so it cannot report committed metadata.
|
||||
report.addProperty("method_tables", manager.getMethodTableCount());
|
||||
report.add("coverage", coverage);
|
||||
report.addProperty("status", diagnostics.isEmpty() ? "complete" : "partial");
|
||||
report.add("reason", JsonNull.INSTANCE);
|
||||
report.add("derived_memory", derivedRange.deepCopy());
|
||||
report.add("types", tables);
|
||||
commit = true;
|
||||
return finish(log);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { jsonObjectSchema } from "../../../domain/jsonValue.js";
|
||||
import { runOwnedCommand } from "../../../process/OwnedCommand.js";
|
||||
import { validateNativeAotReport } from "./NativeAotReport.js";
|
||||
|
||||
it.skipIf(!process.env.JAVA_HOME || !process.env.GSON_JAR)(
|
||||
"accepts transaction reports from the Java producer with controlled APIs",
|
||||
async () => {
|
||||
const output = await runOwnedCommand(
|
||||
{
|
||||
command: "python3",
|
||||
arguments: ["-S", "tests/fixtures/nativeaot-producer/run.py", "--json"],
|
||||
runId: "rea-nativeaot-producer-test",
|
||||
expectedCommand: null,
|
||||
},
|
||||
{ timeoutMs: 75_000, diagnosticBytes: 1024 * 1024 },
|
||||
);
|
||||
const reports = JSON.parse(output.stdout.text) as Array<{
|
||||
status: "failed" | "partial" | "complete";
|
||||
reason: string | null;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
expect(reports.map(({ status }) => status)).toEqual([
|
||||
"failed",
|
||||
"failed",
|
||||
"complete",
|
||||
"partial",
|
||||
]);
|
||||
for (const report of reports)
|
||||
expect(
|
||||
validateNativeAotReport({
|
||||
id: "nativeaot",
|
||||
sha256: "0".repeat(64),
|
||||
status: report.status,
|
||||
reason: report.reason,
|
||||
result: jsonObjectSchema.parse(report),
|
||||
}),
|
||||
).toBeNull();
|
||||
},
|
||||
90_000,
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
package rea.extensions.nativeaot;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
|
||||
/** Execute the actual producer against controlled APIs, not a real Ghidra runtime. */
|
||||
public final class NativeAotProducerProbe {
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
private static JsonObject failure(boolean audit) throws Exception {
|
||||
MethodTableManager.failAudit = audit;
|
||||
MethodTableManager.failTypes = !audit;
|
||||
Program program = new Program();
|
||||
JsonObject report = new NativeAotExtension().analyze(program, new TaskMonitor());
|
||||
require(!program.committed && program.ends == 1, "failure must roll back exactly once");
|
||||
require(report.get("status").getAsString().equals("failed"), "expected failed status");
|
||||
require(report.get("method_tables").getAsInt() == 0, "rolled-back tables must not be reported");
|
||||
for (String key : new String[]{"types", "coverage", "derived_memory"})
|
||||
require(!report.has(key), "rolled-back report contains " + key);
|
||||
require(report.get("reason").getAsString().contains(audit ? "audit failure" : "type failure"), "failure reason lost");
|
||||
MethodTableManager.failAudit = MethodTableManager.failTypes = false;
|
||||
return report;
|
||||
}
|
||||
private static JsonObject success(boolean partial) throws Exception {
|
||||
Program.partial = partial;
|
||||
Program program = new Program();
|
||||
JsonObject report = new NativeAotExtension().analyze(program, new TaskMonitor());
|
||||
require(program.committed && program.ends == 1, "successful analysis must commit exactly once");
|
||||
require(report.get("status").getAsString().equals(partial ? "partial" : "complete"), "wrong coverage status");
|
||||
require(report.get("method_tables").getAsInt() == 1, "missing method table");
|
||||
for (String key : new String[]{"types", "coverage", "derived_memory"})
|
||||
require(report.has(key), "recovery missing " + key);
|
||||
Program.partial = false;
|
||||
return report;
|
||||
}
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length > 0 && !args[0].equals("--json")) {
|
||||
failure(args[0].equals("audit"));
|
||||
return;
|
||||
}
|
||||
JsonArray reports = new JsonArray();
|
||||
reports.add(failure(true));
|
||||
reports.add(failure(false));
|
||||
reports.add(success(false));
|
||||
reports.add(success(true));
|
||||
for (int cancelAt : new int[]{1, 4}) {
|
||||
Program program = new Program();
|
||||
TaskMonitor cancelled = new TaskMonitor();
|
||||
cancelled.cancelAt = cancelAt;
|
||||
try {
|
||||
new NativeAotExtension().analyze(program, cancelled);
|
||||
throw new AssertionError("expected cancellation");
|
||||
} catch (CancelledException expected) {
|
||||
require(!program.committed && program.ends == 1, "cancellation must roll back exactly once");
|
||||
}
|
||||
}
|
||||
System.out.println(args.length > 0 ? reports : "NativeAOT producer transaction contract passed (controlled API doubles)");
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# NativeAOT producer transaction probe
|
||||
|
||||
This compiles the shipped Java producer with controlled Ghidra and upstream API
|
||||
doubles and real Gson. It checks late failures, success, partial recovery, and
|
||||
cancellation against the transaction and report contract. It does not test the
|
||||
real Ghidra APIs or engine. With JDK 21+ and a Gson jar already available:
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/path/to/jdk GSON_JAR=/path/to/gson.jar npm run test:local -- src/ghidra/extensions/nativeaot/NativeAotProducer.test.ts
|
||||
```
|
||||
|
||||
The test is skipped without both variables. To run the Java probe directly,
|
||||
use `python3 tests/fixtures/nativeaot-producer/run.py`. Add `--json` to emit the
|
||||
four reports checked by the TypeScript validator. For a baseline comparison,
|
||||
set `REA_NATIVEAOT_PRODUCER_SOURCE` to another `NativeAotExtension.java` and pass
|
||||
`audit` or `types` to select one failure case. Subprocesses have 30-second limits.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
"""Compile the shipped extension with controlled API doubles and run its transaction contract.
|
||||
|
||||
Requires JAVA_HOME (JDK 21+) and GSON_JAR. This does not validate real Ghidra APIs.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
JAVA = Path(os.environ['JAVA_HOME']) / 'bin'
|
||||
GSON = os.environ['GSON_JAR']
|
||||
with tempfile.TemporaryDirectory(prefix='rea-nativeaot-java-') as temp:
|
||||
root = Path(temp)
|
||||
sources = [str(p) for p in (Path(__file__).parent / "stubs").rglob("*.java")]
|
||||
sources += [os.environ.get("REA_NATIVEAOT_PRODUCER_SOURCE", str(ROOT / "bridge/nativeaot/NativeAotExtension.java")), str(Path(__file__).with_name('NativeAotProducerProbe.java'))]
|
||||
subprocess.run([str(JAVA / 'javac'), '--release', '21', '-cp', GSON, '-d', str(root), *sources], check=True, timeout=30)
|
||||
subprocess.run([str(JAVA / 'java'), '-cp', os.pathsep.join([str(root), GSON]), 'rea.extensions.nativeaot.NativeAotProducerProbe', *sys.argv[1:]], check=True, timeout=30)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ghidra.app.util.importer;
|
||||
public class MessageLog {
|
||||
public void appendMsg(String a,String b) {}
|
||||
public String toString() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ghidra.program.model.address;
|
||||
public class Address implements Comparable<Address> {
|
||||
public long value;
|
||||
public Address(long v) {value=v;}
|
||||
public long getOffset(){return value;}
|
||||
public Address getNewAddress(long n){return new Address(n);}
|
||||
public Address add(long n){return new Address(value+n);}
|
||||
public int compareTo(Address a){return Long.compare(value,a.value);}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ghidra.program.model.address;
|
||||
public class AddressSet {
|
||||
public AddressSet(Address a,Address b) {}
|
||||
public Object getFirstRange(){return null;}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package ghidra.program.model.data;
|
||||
public class DataType {
|
||||
public String getPathName(){return "T";}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package ghidra.program.model.listing;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.DataType;
|
||||
public class Program {
|
||||
public static boolean partial;
|
||||
public boolean committed;
|
||||
public int ends;
|
||||
public LanguageID getLanguageID(){return new LanguageID();}
|
||||
public int startTransaction(String s){return 1;}
|
||||
public void endTransaction(int id,boolean commit){committed=commit;
|
||||
ends++;}
|
||||
public Memory getMemory(){return new Memory();}
|
||||
public Listing getListing(){return new Listing();}
|
||||
public static class LanguageID {
|
||||
public String getIdAsString(){return "x86:LE:64";}
|
||||
}
|
||||
public static class Memory {
|
||||
public int getBytes(Address a,byte[] b,int o,int n){return n;}
|
||||
public long getLong(Address a){return 1;}
|
||||
}
|
||||
public static class Listing {
|
||||
public Data getDataAt(Address a){return new Data();}
|
||||
}
|
||||
public static class Data {
|
||||
public DataType getDataType(){return partial ? new DataType(){public String getPathName(){return "other";}} : new DataType();}
|
||||
} }
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
package ghidra.util.exception;
|
||||
public class CancelledException extends Exception {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ghidra.util.task;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
public class TaskMonitor {
|
||||
public int cancelAt, checks;
|
||||
public void checkCancelled() throws CancelledException {
|
||||
checks++;
|
||||
if(checks == cancelAt) throw new CancelledException();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package nativeaot.objectmodel;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import nativeaot.rtr.ReadyToRunDirectory;
|
||||
public class FrozenObjectAnnotator {
|
||||
public FrozenObjectAnnotator(Program p,MethodTableManager m) {}
|
||||
public void analyze(ReadyToRunDirectory d,Address[] a,TaskMonitor m,MessageLog l) {}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package nativeaot.objectmodel;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import nativeaot.rehydration.MetadataRehydratorNet80.Scan;
|
||||
public class MethodTableCrawler {
|
||||
public MethodTableCrawler(MethodTableManager m,Program p,Scan s) {}
|
||||
public void analyze(TaskMonitor m,MessageLog l) {}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package nativeaot.objectmodel;
|
||||
import java.util.List;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import nativeaot.rtr.ReadyToRunDirectory;
|
||||
public class MethodTableManager {
|
||||
public static boolean failAudit,failTypes;
|
||||
public static MethodTableManager createForDirectory(Program p,ReadyToRunDirectory d){return new MethodTableManager();}
|
||||
public Object getObjectMT(){return this;}
|
||||
public Object getStringMT(){return this;}
|
||||
public int getMethodTableCount(){return 1;}
|
||||
public Table getMethodTable(long n){if(failAudit) throw new IllegalStateException("audit failure");
|
||||
return new Table();}
|
||||
public List<Table> getMethodTables(){return List.of(new Table());}
|
||||
public static class Table {
|
||||
public Address getAddress(){return new Address(1);}
|
||||
public DataType getMTType(){if(failTypes) throw new IllegalStateException("type failure");
|
||||
return new DataType();}
|
||||
public DataType getInstanceType(){return new DataType();}
|
||||
} }
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package nativeaot.rehydration;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
public class MetadataRehydratorNet80 {
|
||||
public Scan rehydrate(Program p,Object r,TaskMonitor m,MessageLog l){return new Scan();}
|
||||
public static class Scan {
|
||||
public Range getScanningRange(){return new Range();}
|
||||
public Address[] getPointerLocations(){return new Address[]{new Address(1)};}
|
||||
}
|
||||
public static class Range {
|
||||
public long getLength(){return 2;}
|
||||
public Address getMinAddress(){return new Address(1);}
|
||||
} }
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package nativeaot.rtr;
|
||||
public class ReadyToRunDirectory {
|
||||
public int getMajorVersion(){return 9;}
|
||||
public int getMinorVersion(){return 1;}
|
||||
public ReadyToRunSection getSectionByType(int t){return new ReadyToRunSection();}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package nativeaot.rtr;
|
||||
import ghidra.program.model.address.Address;
|
||||
public class ReadyToRunSection {
|
||||
public static final int DEHYDRATED_DATA=1,FROZEN_OBJECT_REGION=2;
|
||||
public long getStart(){return 1;}
|
||||
public long getEnd(){return 3;}
|
||||
public Address[] getPointersInSection(Address[] a){return a;}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package rea.extensions.nativeaot;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import nativeaot.rtr.ReadyToRunDirectory;
|
||||
final class NativeAotDirectory {
|
||||
record Located(Address address,ReadyToRunDirectory directory,String discovery) {}
|
||||
static Located locate(Program p,TaskMonitor m,MessageLog l){return new Located(new Address(1),new ReadyToRunDirectory(),"symbol");}
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
package rea.extensions.nativeaot;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
final class NativeAotMethodConventions {
|
||||
static Object capture(Program p,TaskMonitor m){return null;}
|
||||
static void restore(Program p,MethodTableManager a,Object c,TaskMonitor m) {}
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
package rea.extensions.nativeaot;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
final class NativeAotTypeMetadata {
|
||||
static JsonObject describe(Program p,MethodTableManager m,NativeAotDirectory.Located l,DataType t,JsonObject r,JsonObject d,TaskMonitor x){return null;}
|
||||
}
|
||||
Reference in new issue
Block a user