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:
morluto authored and GitHub committed 2026-11-09 19:39:51 +08:00
1 parent e304de99bc
commit 8c64eb511f
21 files changed
+306 -5

No files matched your search

+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)
@@ -0,0 +1,7 @@
package ghidra.app.util.importer;
public class MessageLog {
public void appendMsg(String a,String b) {}
public String toString() {
return "";
}
}
@@ -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);}
}
@@ -0,0 +1,5 @@
package ghidra.program.model.address;
public class AddressSet {
public AddressSet(Address a,Address b) {}
public Object getFirstRange(){return null;}
}
@@ -0,0 +1,4 @@
package ghidra.program.model.data;
public class DataType {
public String getPathName(){return "T";}
}
@@ -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();}
} }
@@ -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();
}
}
@@ -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) {}
}
@@ -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) {}
}
@@ -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();}
} }
@@ -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);}
} }
@@ -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;}
}
@@ -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");}
}
@@ -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) {}
}
@@ -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;}
}