fix(android): preserve typed JVM memory failures during loading
Translate actual OutOfMemoryError at the bridge boundary before upstream stringifies it. Preserve observed heap bytes, missing reasons, coroutine context and cancellation; retain SessionHolder ownership and load options. Restrict heap-specific remediation to known heap reasons and parse named bridge failure contracts once. Coordinate bridge protocol v4, clarify doctor configuration versus target capacity, and preserve the memory primary across cleanup failure/retry. Verify controlled immediate/late/inline continuation outcomes with the pinned JAR and real JDK, focused tests, and tiny real CLI/MCP method workflows. Actual heap exhaustion, the reported APK, pre-handshake failures, Linux and Windows remain unverified; no stress or profiling run. Fixes #3517
This commit is contained in:
1 parent
9fa6d2b254
commit
3fa972af7c
11 files changed
+384
-19
No files matched your search
@@ -15,7 +15,11 @@ import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
import kotlin.Unit;
|
||||
import kotlin.ResultKt;
|
||||
import kotlin.coroutines.Continuation;
|
||||
import kotlin.coroutines.CoroutineContext;
|
||||
import kotlin.coroutines.EmptyCoroutineContext;
|
||||
import kotlin.coroutines.intrinsics.IntrinsicsKt;
|
||||
import kotlinx.coroutines.BuildersKt;
|
||||
import kotlinx.io.CoreKt;
|
||||
import kotlinx.io.JvmCoreKt;
|
||||
@@ -44,6 +48,84 @@ class ReaJadxBridge {
|
||||
|
||||
private static CallToolResult json(Object value) { return text(JSON.toJson(value), false); }
|
||||
|
||||
/** Translate the typed load outcome before upstream drops its exception identity. */
|
||||
static Object loadResult(Object result) {
|
||||
try { ResultKt.throwOnFailure(result); }
|
||||
catch (Throwable failure) {
|
||||
if (failure instanceof java.util.concurrent.CancellationException
|
||||
|| failure instanceof Error && !(failure instanceof OutOfMemoryError)) return result;
|
||||
if (failure instanceof OutOfMemoryError) {
|
||||
String reason = failure.getMessage();
|
||||
boolean heap = "Java heap space".equals(reason) || "GC overhead limit exceeded".equals(reason);
|
||||
Map<String, Object> reply = new LinkedHashMap<>();
|
||||
reply.put("kind", "jvm-memory-failure");
|
||||
reply.put("memory_area", heap ? "heap" : "unknown");
|
||||
reply.put("exception_type", "java.lang.OutOfMemoryError");
|
||||
if (reason != null) reply.put("message", reason);
|
||||
reply.put("max_heap_bytes", Runtime.getRuntime().maxMemory());
|
||||
return text(JSON.toJson(reply), true);
|
||||
}
|
||||
return text("load failed: " + failure.getMessage(), true);
|
||||
}
|
||||
SessionHolder.LoadResult loaded = (SessionHolder.LoadResult) result;
|
||||
Map<String, Object> reply = new LinkedHashMap<>();
|
||||
reply.put("state", "LOADED");
|
||||
reply.put("apk_path", loaded.getApkPath());
|
||||
reply.put("class_count", loaded.getClassCount());
|
||||
reply.put("raw_class_count", loaded.getRawClassCount());
|
||||
reply.put("resource_count", loaded.getResourceCount());
|
||||
reply.put("load_duration_ms", loaded.getLoadDurationMs());
|
||||
reply.put("decompile_timeout_ms", loaded.getDecompileTimeoutMs());
|
||||
reply.put("threads", loaded.getThreads());
|
||||
reply.put("code_cache_size", loaded.getCodeCacheSize());
|
||||
reply.put("resources", loaded.getResourceMode());
|
||||
if (!loaded.getIncludePackages().isEmpty()) reply.put("include_packages", loaded.getIncludePackages());
|
||||
if (!loaded.getExcludePackages().isEmpty()) reply.put("exclude_packages", loaded.getExcludePackages());
|
||||
return json(reply);
|
||||
}
|
||||
|
||||
private Object load(kotlinx.serialization.json.JsonObject arguments, SessionConfig defaults,
|
||||
Continuation<? super CallToolResult> continuation) {
|
||||
JsonObject args = JsonParser.parseString(arguments.toString()).getAsJsonObject();
|
||||
String path = string(args, "path");
|
||||
SessionConfig options = defaults.withLoadOverrides(
|
||||
optionalString(args, "include_packages"), optionalString(args, "exclude_packages"),
|
||||
args.has("threads") ? integer(args, "threads", 1) : null,
|
||||
args.has("code_cache_size") ? integer(args, "code_cache_size", 64) : null,
|
||||
optionalString(args, "resources"));
|
||||
return loadCall(next -> holder.load(path, options, next), continuation);
|
||||
}
|
||||
|
||||
static Object loadCall(kotlin.jvm.functions.Function1<Continuation<SessionHolder.LoadResult>, Object> load,
|
||||
Continuation<? super CallToolResult> continuation) {
|
||||
var forwarded = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
Continuation<SessionHolder.LoadResult> resumed = new Continuation<>() {
|
||||
public CoroutineContext getContext() { return continuation.getContext(); }
|
||||
public void resumeWith(Object result) {
|
||||
if (!forwarded.compareAndSet(false, true)) throw new IllegalStateException("Load resumed twice");
|
||||
Object translated;
|
||||
try { translated = loadResult(result); }
|
||||
catch (Throwable failure) { translated = ResultKt.createFailure(failure); }
|
||||
// Keep the consumer outside the translation catch, including inline completion.
|
||||
continuation.resumeWith(translated);
|
||||
}
|
||||
};
|
||||
Object result;
|
||||
try { result = load.invoke(resumed); }
|
||||
catch (Throwable failure) {
|
||||
if (forwarded.get()) ResultKt.throwOnFailure(ResultKt.createFailure(failure));
|
||||
result = ResultKt.createFailure(failure);
|
||||
}
|
||||
if (result == IntrinsicsKt.getCOROUTINE_SUSPENDED()) return result;
|
||||
Object translated = loadResult(result);
|
||||
ResultKt.throwOnFailure(translated);
|
||||
return translated;
|
||||
}
|
||||
|
||||
private static String optionalString(JsonObject args, String name) {
|
||||
return args.has(name) ? string(args, name) : null;
|
||||
}
|
||||
|
||||
private static String string(JsonObject args, String name) {
|
||||
JsonElement value = args.get(name);
|
||||
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
|
||||
@@ -341,11 +423,12 @@ class ReaJadxBridge {
|
||||
public static void main(String[] args) throws Exception {
|
||||
PrintStream protocolOut = System.out;
|
||||
System.setOut(System.err);
|
||||
SessionHolder holder = new SessionHolder(new SessionConfig(
|
||||
1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL));
|
||||
SessionConfig defaults = new SessionConfig(
|
||||
1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
|
||||
SessionHolder holder = new SessionHolder(defaults);
|
||||
ServerCapabilities capabilities = new ServerCapabilities(
|
||||
new ServerCapabilities.Tools(null), null, null, null, null, null, null, Map.of());
|
||||
Server server = new Server(new Implementation("rea-jadx-bridge", "3", null, null, null),
|
||||
Server server = new Server(new Implementation("rea-jadx-bridge", "4", null, null, null),
|
||||
new ServerOptions(capabilities), "", ignored -> Unit.INSTANCE);
|
||||
SessionToolsKt.registerSessionTools(server, holder);
|
||||
ManifestToolsKt.registerManifestTools(server, holder);
|
||||
@@ -353,6 +436,10 @@ class ReaJadxBridge {
|
||||
MethodToolsKt.registerMethodTools(server, holder);
|
||||
XrefToolsKt.registerXrefTools(server, holder);
|
||||
ReaJadxBridge bridge = new ReaJadxBridge(holder);
|
||||
Tool loadTool = server.getTools().get("load_apk").getTool();
|
||||
server.removeTool("load_apk");
|
||||
server.addTool(loadTool, (connection, request, continuation) ->
|
||||
bridge.load(request.getArguments(), defaults, continuation));
|
||||
Set<String> consumed = Set.of("load_apk", "get_app_info", "get_android_manifest",
|
||||
"list_classes", "get_class_summary", "get_method_body",
|
||||
"get_xrefs_to_class", "get_xrefs_to_method");
|
||||
|
||||
@@ -317,7 +317,15 @@ settings. Evidence reports the worker's observed `Runtime.maxMemory()` rounded
|
||||
down to MiB and retains exact bytes, Java version and visible processors in its
|
||||
raw runtime response. The observed allocatable heap can differ from configured
|
||||
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
|
||||
uses native memory. Independent REA instances have independent queues. Whole
|
||||
uses native memory. A typed JVM memory allocation failure during APK loading
|
||||
reports `resource_constraint` with the observed maximum heap in bytes. Known
|
||||
heap failures name `REA_JADX_HEAP_MIB` in remediation; other memory regions
|
||||
remain unknown. A healthy doctor configuration check does not establish that
|
||||
any particular APK fits. Failures before the bridge can report a typed result
|
||||
and upstream errors from other operations retain their existing diagnostics;
|
||||
error text alone does not establish memory exhaustion.
|
||||
|
||||
Independent REA instances have independent queues. Whole
|
||||
operations have a **120-second** execution deadline by default, starting after
|
||||
they reach the queue's front. For a known slow cold load, such as a large
|
||||
multidex APK, select another finite deadline:
|
||||
|
||||
@@ -65,6 +65,10 @@ export const verifyAndroidMetadata = async ({
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/LOAD_FAILURE_REGRESSION_PASS typed=true decoy=true suspension=true cancellation=true/u,
|
||||
);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/METADATA_REGRESSION_PASS classes=\d+ overloads=3/u,
|
||||
|
||||
@@ -79,7 +79,7 @@ const configurationCheck = (
|
||||
name: "configuration",
|
||||
ok: true,
|
||||
code: null,
|
||||
detail: `JADX MCP bridge JAR validated at ${jarPath}; its path is recorded for agent registrations.`,
|
||||
detail: `JADX MCP bridge JAR validated at ${jarPath}; its path is recorded for agent registrations. This checks configuration, not whether a selected APK fits in the worker's memory.`,
|
||||
remediation: null,
|
||||
classification: "config_drift",
|
||||
...(typeof operationTimeout === "number"
|
||||
|
||||
@@ -5,12 +5,38 @@ import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
import { dexDescriptorSchema } from "../domain/android/androidAnalysis.js";
|
||||
|
||||
const count = z.number().int().nonnegative();
|
||||
/** Typed allocation failures authored before upstream erases the JVM exception. */
|
||||
const jadxMemoryFailureSchema = z.strictObject({
|
||||
kind: z.literal("jvm-memory-failure"),
|
||||
memory_area: z.enum(["heap", "unknown"]),
|
||||
exception_type: z.literal("java.lang.OutOfMemoryError"),
|
||||
message: z.string().optional(),
|
||||
max_heap_bytes: z.number().int().positive().safe(),
|
||||
});
|
||||
/** Selector failures authored by REA's bridge, separate from arbitrary engine errors. */
|
||||
export const jadxInputFailureSchema = z.strictObject({
|
||||
const jadxInputFailureSchema = z.strictObject({
|
||||
kind: z.literal("invalid-input"),
|
||||
field: z.enum(["class_name", "method_name", "overload_index"]),
|
||||
message: z.string().min(1),
|
||||
});
|
||||
const bridgeFailureSchema = z.discriminatedUnion("kind", [
|
||||
jadxInputFailureSchema,
|
||||
jadxMemoryFailureSchema,
|
||||
]);
|
||||
|
||||
/** Arbitrary upstream failure text stays text unless it matches a named bridge contract. */
|
||||
export const parseJadxFailure = (
|
||||
text: string,
|
||||
): z.infer<typeof bridgeFailureSchema> | undefined => {
|
||||
let candidate: unknown;
|
||||
try {
|
||||
candidate = JSON.parse(text);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = bridgeFailureSchema.safeParse(candidate);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
};
|
||||
/** Values observed inside the exact JVM producing the analysis. */
|
||||
export const jadxRuntimeSchema = z.object({
|
||||
engine_reported_version: z.string().min(1),
|
||||
|
||||
@@ -9,7 +9,7 @@ export const JADX_RELEASE = Object.freeze({
|
||||
/** Identity of REA's metadata bridge, distinct from the producing engine. */
|
||||
export const JADX_BRIDGE_IDENTITY = Object.freeze({
|
||||
name: "rea-jadx-bridge",
|
||||
version: "3",
|
||||
version: "4",
|
||||
});
|
||||
|
||||
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -18,7 +19,7 @@ import { analyzeJadxRequest, type JadxToolPort } from "./JadxAnalysis.js";
|
||||
import { JadxMcpTransport, type JadxLauncher } from "./JadxMcpTransport.js";
|
||||
import {
|
||||
jadxLoadSchema,
|
||||
jadxInputFailureSchema,
|
||||
parseJadxFailure,
|
||||
jadxRuntimeSchema,
|
||||
parseJadxEnvelope,
|
||||
parseJadxJson,
|
||||
@@ -225,16 +226,38 @@ export class JadxSession {
|
||||
this.#observedCalls.push(call);
|
||||
const envelope = parseJadxEnvelope(response, request.operation);
|
||||
if (envelope.failed) {
|
||||
if (envelope.text.startsWith("{")) {
|
||||
const failure = jadxInputFailureSchema.safeParse(
|
||||
parseJadxJson(envelope.text, request.operation),
|
||||
);
|
||||
if (failure.success)
|
||||
const failure = parseJadxFailure(envelope.text);
|
||||
if (failure !== undefined) {
|
||||
if (name === "load_apk" && failure.kind === "jvm-memory-failure")
|
||||
throw new AnalysisResourceConstraintError(
|
||||
request.operation,
|
||||
"memory",
|
||||
`The JADX JVM could not allocate memory during ${name}${failure.message ? `: ${failure.message}` : ""}`,
|
||||
{
|
||||
observed_max_heap_bytes: failure.max_heap_bytes,
|
||||
memory_area: failure.memory_area,
|
||||
},
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction:
|
||||
failure.memory_area === "heap"
|
||||
? "Increase REA_JADX_HEAP_MIB (MiB) in the environment that starts REA, within the host or container memory budget. Review JAVA_TOOL_OPTIONS and other JVM heap overrides. The observed heap is this run's limit, not the heap the APK requires."
|
||||
: "Review the JVM allocation failure and the host or container memory limits. The observed maximum heap does not establish which memory region was exhausted; increasing heap alone may not help.",
|
||||
diagnostics: {
|
||||
upstream_operation: name,
|
||||
reason: failure.message ?? null,
|
||||
exception_type: failure.exception_type,
|
||||
target_path: target.path,
|
||||
target_sha256: target.sha256,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (failure.kind === "invalid-input")
|
||||
throw new AnalysisInputError(request.operation, undefined, [
|
||||
{
|
||||
path: [failure.data.field],
|
||||
path: [failure.field],
|
||||
reason: "invalid_value",
|
||||
message: failure.data.message,
|
||||
message: failure.message,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,33 @@ import {
|
||||
parseJadxEnvelope,
|
||||
parseJadxJson,
|
||||
normalizeJadxText,
|
||||
parseJadxFailure,
|
||||
} from "../../../src/android/JadxProtocol.js";
|
||||
import { androidInputSchemas } from "../../../src/domain/android/androidAnalysis.js";
|
||||
|
||||
it("recognizes named bridge failures without reinterpreting arbitrary engine text", () => {
|
||||
const failure = {
|
||||
kind: "jvm-memory-failure",
|
||||
memory_area: "heap",
|
||||
exception_type: "java.lang.OutOfMemoryError",
|
||||
message: "Java heap space",
|
||||
max_heap_bytes: 67108864,
|
||||
};
|
||||
expect(parseJadxFailure(JSON.stringify(failure))).toEqual(failure);
|
||||
for (const value of [
|
||||
"load failed: cannot read /inputs/Java heap space.apk",
|
||||
"{malformed Java heap space",
|
||||
JSON.stringify({
|
||||
...failure,
|
||||
exception_type: "java.lang.IllegalArgumentException",
|
||||
}),
|
||||
JSON.stringify({ ...failure, max_heap_bytes: -1 }),
|
||||
JSON.stringify({ ...failure, memory_area: "native" }),
|
||||
JSON.stringify({ ...failure, arbitrary: true }),
|
||||
])
|
||||
expect(parseJadxFailure(value)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates provider text envelopes and rejects unsupported content rather than dropping it", () => {
|
||||
expect(
|
||||
parseJadxEnvelope(
|
||||
|
||||
@@ -834,7 +834,7 @@ it("reports missing explicit tools without trying to install or launch them", as
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["tool-error", "frame-overflow", "body-error"])(
|
||||
it.each(["tool-error", "frame-overflow", "body-error", "heap-decoy"])(
|
||||
"preserves the actual failed provider constraint for %s",
|
||||
async (mode) => {
|
||||
const { service, apk, launches, provider } = await setup(mode);
|
||||
@@ -855,11 +855,17 @@ it.each(["tool-error", "frame-overflow", "body-error"])(
|
||||
_tag: "ProviderAdapterError",
|
||||
diagnostics: {
|
||||
upstream_operation:
|
||||
mode === "body-error" ? "get_method_body" : "get_android_manifest",
|
||||
mode === "body-error"
|
||||
? "get_method_body"
|
||||
: mode === "heap-decoy"
|
||||
? "load_apk"
|
||||
: "get_android_manifest",
|
||||
reason:
|
||||
mode === "body-error"
|
||||
? "// ERROR: decompile timed out or failed"
|
||||
: "manifest decoder rejected malformed binary XML",
|
||||
: mode === "heap-decoy"
|
||||
? "load failed: cannot read /inputs/Java heap space.apk"
|
||||
: "manifest decoder rejected malformed binary XML",
|
||||
},
|
||||
});
|
||||
else
|
||||
@@ -872,6 +878,88 @@ it.each(["tool-error", "frame-overflow", "body-error"])(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["heap-failure", "other-memory-failure"])(
|
||||
"projects typed JVM memory failure and cleans its process for %s",
|
||||
async (mode) => {
|
||||
const { service, apk, launches, provider } = await setup(mode);
|
||||
const outcome = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
});
|
||||
if (outcome.ok) throw new Error("Expected memory allocation failure");
|
||||
expect(outcome.error).toMatchObject({
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
resource: "memory",
|
||||
retryable: false,
|
||||
reportedLimits: {
|
||||
observed_max_heap_bytes: 67108864,
|
||||
memory_area: mode === "heap-failure" ? "heap" : "unknown",
|
||||
},
|
||||
diagnostics: { upstream_operation: "load_apk", target_path: apk },
|
||||
});
|
||||
const projected = projectAnalysisError(outcome.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: false,
|
||||
});
|
||||
expect(projected.remediation.action).toContain(
|
||||
mode === "heap-failure"
|
||||
? "REA_JADX_HEAP_MIB"
|
||||
: "increasing heap alone may not help",
|
||||
);
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains typed memory failure through failed cleanup and restores it after retry", async () => {
|
||||
let fails = true;
|
||||
const { service, apk, launches, provider } = await setup(
|
||||
"heap-failure",
|
||||
() => fails,
|
||||
);
|
||||
const rootClose = vi
|
||||
.spyOn(PrivateRuntimeRoot.prototype, "close")
|
||||
.mockRejectedValueOnce(new Error("fixture workspace close failure"));
|
||||
try {
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
});
|
||||
if (result.ok) throw new Error("Expected failed memory cleanup");
|
||||
const failure = result.error;
|
||||
expect(failure.cleanupIncomplete).toBe(true);
|
||||
expect(projectAnalysisError(failure)).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: {
|
||||
diagnostics: { primary_error: { code: "resource_constraint" } },
|
||||
},
|
||||
});
|
||||
const workspace = launches[0]?.cwd;
|
||||
expect(failure.cleanupResources).toContain(workspace);
|
||||
fails = false;
|
||||
await expect(provider.close()).rejects.toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
});
|
||||
expect(failure.cleanupResources).toEqual([workspace]);
|
||||
await provider.close();
|
||||
expect(failure.cleanupIncomplete).toBe(false);
|
||||
expect(failure.cleanupResources).toEqual([]);
|
||||
expect(projectAnalysisError(failure)).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: false,
|
||||
remediation: { action: expect.stringContaining("REA_JADX_HEAP_MIB") },
|
||||
details: {
|
||||
resource: "memory",
|
||||
reported_limits: { observed_max_heap_bytes: 67108864 },
|
||||
},
|
||||
});
|
||||
await verifyCleanup(launches);
|
||||
} finally {
|
||||
rootClose.mockRestore();
|
||||
fails = false;
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects bytes changed after target admission without launching an engine", async () => {
|
||||
const { provider, apk, launches } = await setup();
|
||||
const target = await parseBinaryTarget(apk);
|
||||
|
||||
@@ -330,7 +330,93 @@ public class ReaJadxMetadataRegression {
|
||||
System.out.println("NO_BODY_REGRESSION_PASS native=true abstract=true frozen=true");
|
||||
}
|
||||
|
||||
private static void verifyLoadFailures() {
|
||||
for (String reason : new String[]{"Java heap space", "GC overhead limit exceeded", "Metaspace", "", null}) {
|
||||
Object projected = ReaJadxBridge.loadResult(kotlin.ResultKt.createFailure(new OutOfMemoryError(reason)));
|
||||
CallToolResult response = (CallToolResult) projected;
|
||||
require(Boolean.TRUE.equals(response.isError()), "Memory failure became success");
|
||||
JsonObject failure = JsonParser.parseString(((TextContent) response.getContent().get(0)).getText())
|
||||
.getAsJsonObject();
|
||||
require(failure.get("kind").getAsString().equals("jvm-memory-failure"), "Lost memory tag");
|
||||
require(reason == null ? !failure.has("message") : failure.get("message").getAsString().equals(reason),
|
||||
"Lost JVM reason or invented one");
|
||||
require(failure.get("max_heap_bytes").getAsLong() == Runtime.getRuntime().maxMemory(),
|
||||
"Configured heap substituted for observed heap");
|
||||
boolean heap = "Java heap space".equals(reason) || "GC overhead limit exceeded".equals(reason);
|
||||
require(failure.get("memory_area").getAsString().equals(heap ? "heap" : "unknown"),
|
||||
"Unknown memory region became heap");
|
||||
}
|
||||
Object cancellation = kotlin.ResultKt.createFailure(new java.util.concurrent.CancellationException("cancel"));
|
||||
require(ReaJadxBridge.loadResult(cancellation) == cancellation, "Cancellation was stringified");
|
||||
Object fatal = kotlin.ResultKt.createFailure(new AssertionError("fatal"));
|
||||
require(ReaJadxBridge.loadResult(fatal) == fatal, "Fatal error was stringified");
|
||||
String decoy = "cannot read /inputs/Java heap space.apk";
|
||||
CallToolResult ordinary = (CallToolResult) ReaJadxBridge.loadResult(
|
||||
kotlin.ResultKt.createFailure(new IllegalArgumentException(decoy)));
|
||||
require(((TextContent) ordinary.getContent().get(0)).getText().equals("load failed: " + decoy),
|
||||
"Ordinary exception text became memory evidence");
|
||||
var loaded = new SessionHolder.LoadResult("/selected.apk", 2, 3, 4, 5L, 90000L,
|
||||
1, 64, "full", List.of("selected"), List.of("excluded"));
|
||||
var completions = new ArrayList<Object>();
|
||||
kotlin.coroutines.Continuation<CallToolResult> receiver = new kotlin.coroutines.Continuation<>() {
|
||||
public kotlin.coroutines.CoroutineContext getContext() { return EmptyCoroutineContext.INSTANCE; }
|
||||
public void resumeWith(Object result) { completions.add(result); }
|
||||
};
|
||||
Object immediate = ReaJadxBridge.loadCall(next -> loaded, receiver);
|
||||
require(immediate instanceof CallToolResult && completions.isEmpty(), "Immediate completion resumed twice");
|
||||
JsonObject success = JsonParser.parseString(((TextContent) ((CallToolResult) immediate)
|
||||
.getContent().get(0)).getText()).getAsJsonObject();
|
||||
require(success.get("apk_path").getAsString().equals("/selected.apk")
|
||||
&& success.get("raw_class_count").getAsInt() == 3
|
||||
&& success.getAsJsonArray("include_packages").get(0).getAsString().equals("selected")
|
||||
&& success.getAsJsonArray("exclude_packages").get(0).getAsString().equals("excluded"),
|
||||
"Load response lost source facts or selected filters");
|
||||
Object thrown = ReaJadxBridge.loadCall(next -> { throw new OutOfMemoryError("Java heap space"); }, receiver);
|
||||
require(thrown instanceof CallToolResult && completions.isEmpty(), "Synchronous failure resumed twice");
|
||||
var pending = new ArrayList<kotlin.coroutines.Continuation<SessionHolder.LoadResult>>();
|
||||
Object suspended = ReaJadxBridge.loadCall(next -> {
|
||||
require(next.getContext() == receiver.getContext(), "Coroutine context changed");
|
||||
pending.add(next);
|
||||
return kotlin.coroutines.intrinsics.IntrinsicsKt.getCOROUTINE_SUSPENDED();
|
||||
}, receiver);
|
||||
require(suspended == kotlin.coroutines.intrinsics.IntrinsicsKt.getCOROUTINE_SUSPENDED(),
|
||||
"Suspension became completion");
|
||||
require(completions.isEmpty(), "Suspension completed early");
|
||||
pending.get(0).resumeWith(cancellation);
|
||||
require(completions.size() == 1 && completions.get(0) == cancellation, "Late cancellation changed");
|
||||
for (Object outcome : List.of(loaded,
|
||||
kotlin.ResultKt.createFailure(new OutOfMemoryError("Java heap space")))) {
|
||||
pending.clear();
|
||||
completions.clear();
|
||||
ReaJadxBridge.loadCall(next -> {
|
||||
pending.add(next);
|
||||
return kotlin.coroutines.intrinsics.IntrinsicsKt.getCOROUTINE_SUSPENDED();
|
||||
}, receiver);
|
||||
pending.get(0).resumeWith(outcome);
|
||||
require(completions.size() == 1 && completions.get(0) instanceof CallToolResult,
|
||||
"Late success or memory failure was not delivered once");
|
||||
}
|
||||
int[] callbacks = {0};
|
||||
var downstreamFailure = new IllegalStateException("downstream rejected completion");
|
||||
kotlin.coroutines.Continuation<CallToolResult> throwing = new kotlin.coroutines.Continuation<>() {
|
||||
public kotlin.coroutines.CoroutineContext getContext() { return EmptyCoroutineContext.INSTANCE; }
|
||||
public void resumeWith(Object result) { callbacks[0]++; throw downstreamFailure; }
|
||||
};
|
||||
try {
|
||||
ReaJadxBridge.loadCall(next -> {
|
||||
next.resumeWith(loaded);
|
||||
return kotlin.coroutines.intrinsics.IntrinsicsKt.getCOROUTINE_SUSPENDED();
|
||||
}, throwing);
|
||||
throw new AssertionError("Inline consumer exception became a second completion");
|
||||
} catch (IllegalStateException failure) {
|
||||
require(failure == downstreamFailure && callbacks[0] == 1, "Inline continuation resumed twice");
|
||||
}
|
||||
System.out.println("LOAD_FAILURE_REGRESSION_PASS typed=true decoy=true suspension=true cancellation=true");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
verifyLoadFailures();
|
||||
if (args.length == 1 && args[0].equals("--load-failures")) return;
|
||||
SessionConfig config = new SessionConfig(1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
|
||||
SessionHolder holder = new SessionHolder(config);
|
||||
try {
|
||||
|
||||
Vendored
+20
-1
@@ -63,7 +63,10 @@ const register = (name, handler) =>
|
||||
register("rea_jvm_status", () =>
|
||||
json({
|
||||
engine_reported_version: mode === "engine-version" ? "0.7.0" : "0.7.1",
|
||||
max_heap_bytes: 8 * 1024 * 1024 * 1024,
|
||||
max_heap_bytes:
|
||||
mode === "heap-failure" || mode === "other-memory-failure"
|
||||
? 67108864
|
||||
: 8 * 1024 * 1024 * 1024,
|
||||
available_processors: 4,
|
||||
java_version: "21-fixture",
|
||||
metadata_scope: "parsed_members",
|
||||
@@ -71,6 +74,22 @@ register("rea_jvm_status", () =>
|
||||
);
|
||||
register("load_apk", async ({ path }) => {
|
||||
if (mode === "stall") await new Promise(() => {});
|
||||
if (mode === "heap-failure" || mode === "other-memory-failure")
|
||||
return {
|
||||
...json({
|
||||
kind: "jvm-memory-failure",
|
||||
memory_area: mode === "heap-failure" ? "heap" : "unknown",
|
||||
exception_type: "java.lang.OutOfMemoryError",
|
||||
message: mode === "heap-failure" ? "Java heap space" : "Metaspace",
|
||||
max_heap_bytes: 67108864,
|
||||
}),
|
||||
isError: true,
|
||||
};
|
||||
if (mode === "heap-decoy")
|
||||
return {
|
||||
...text("load failed: cannot read /inputs/Java heap space.apk"),
|
||||
isError: true,
|
||||
};
|
||||
return json({
|
||||
state: "LOADED",
|
||||
apk_path: mode === "wrong-apk" ? "/different.apk" : path,
|
||||
|
||||
Reference in new issue
Block a user