mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
feat(android): trace callers of external DEX methods (#8154)
Resolve exact external method targets from directly encoded DEX invokes. Preserve complete caller identity, bounded position evidence and follow-up selectors. Report aggregate position coverage independently of the caller inventory, and reuse bounded prototype facts within the exact session. Absorbs #8096. Fixes #7811. Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
1 parent
675d4076de
commit
0ffed7a48c
20 files changed
+1472
-76
No files matched your search
@@ -691,6 +691,9 @@ it.each([
|
||||
name: "get_class_summary",
|
||||
class_name: "fixture.Target",
|
||||
...(values ? { dex_static_values: true } : {}),
|
||||
...(operation === "trace_android_references" && "method_name" in input
|
||||
? { reference_target: true, method_name: input.method_name }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -881,6 +884,12 @@ it.each([
|
||||
async (fields, expected) => {
|
||||
const result = await traceWithReference(fields as Record<string, unknown>);
|
||||
expect(result).toMatchObject({
|
||||
direct_invokes_coverage: {
|
||||
complete_using_nodes: 0,
|
||||
partial_using_nodes: 0,
|
||||
unknown_using_nodes: 1,
|
||||
recorded_sites: 0,
|
||||
},
|
||||
references: [
|
||||
{ direct_invokes: expected, direct_invokes_exhaustive: null },
|
||||
],
|
||||
@@ -910,6 +919,12 @@ it("preserves bounded instruction coverage separately from using-node coverage",
|
||||
expect(result).toMatchObject({
|
||||
coverage: "complete",
|
||||
reference_unit: "using_node",
|
||||
direct_invokes_coverage: {
|
||||
complete_using_nodes: 0,
|
||||
partial_using_nodes: 1,
|
||||
unknown_using_nodes: 0,
|
||||
recorded_sites: 1,
|
||||
},
|
||||
references: [
|
||||
{ direct_invokes: [invokeSite], direct_invokes_exhaustive: false },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
analyzeJadxRequest,
|
||||
type JadxToolPort,
|
||||
} from "../../../src/android/JadxAnalysis.js";
|
||||
import { androidResultSchemas } from "../../../src/domain/android/androidAnalysis.js";
|
||||
|
||||
const engine = {
|
||||
name: "jadx",
|
||||
version: "0.7.1",
|
||||
artifact_sha256: "a".repeat(64),
|
||||
source_revision: null,
|
||||
worker_count: 1,
|
||||
heap_limit_mib: null,
|
||||
};
|
||||
const selector = {
|
||||
class_name: "fixture.Caller",
|
||||
method_name: "notify0",
|
||||
overload_index: 0,
|
||||
};
|
||||
const summary = {
|
||||
class_name: "android.app.NotificationManager",
|
||||
reference_target: "external",
|
||||
observed_method_count: 1,
|
||||
prototypes: [
|
||||
{
|
||||
name: "notify",
|
||||
raw_name: "notify",
|
||||
signature: "notify(int, android.app.Notification): void",
|
||||
dex_descriptor: "(ILandroid/app/Notification;)V",
|
||||
is_constructor: false,
|
||||
def_pos: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
const xrefs = {
|
||||
target: "android.app.NotificationManager.notify",
|
||||
reference_target: "external",
|
||||
reference_scope: "direct_dex_invokes",
|
||||
overload_index: 0,
|
||||
reported_signature: summary.prototypes[0]!.signature,
|
||||
dex_descriptor: summary.prototypes[0]!.dex_descriptor,
|
||||
total: 1,
|
||||
count: 1,
|
||||
resolve_line: false,
|
||||
items: [
|
||||
{
|
||||
kind: "method",
|
||||
name: selector.method_name,
|
||||
full_name: "fixture.Caller.notify0",
|
||||
containing_class: selector.class_name,
|
||||
top_class: selector.class_name,
|
||||
def_pos: 0,
|
||||
source_selector: selector,
|
||||
direct_invokes_exhaustive: true,
|
||||
direct_invokes: [0, 3].map((offset) => ({
|
||||
dex_entry: "classes2.dex",
|
||||
source_raw_method_id:
|
||||
"fixture.Caller.notify0(Landroid/app/NotificationManager;Landroid/app/Notification;)V",
|
||||
dex_code_unit_offset: offset,
|
||||
dex_method_index: 5,
|
||||
opcode: "INVOKE_VIRTUAL",
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const analyze = (
|
||||
target = summary,
|
||||
references = xrefs,
|
||||
operation:
|
||||
| "trace_android_references"
|
||||
| "inspect_android_class" = "trace_android_references",
|
||||
) => {
|
||||
const tools: JadxToolPort = {
|
||||
listClasses: () => Promise.reject(new Error("unexpected class inventory")),
|
||||
text: () => Promise.reject(new Error("unexpected source generation")),
|
||||
json: (name) =>
|
||||
Promise.resolve(name === "get_class_summary" ? target : references),
|
||||
};
|
||||
return analyzeJadxRequest(
|
||||
tools,
|
||||
{
|
||||
operation,
|
||||
input: {
|
||||
path: "/fixture.apk",
|
||||
class_name: summary.class_name,
|
||||
...(operation === "trace_android_references"
|
||||
? { method_name: "notify" }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
{ loaded: {} as never, engine },
|
||||
);
|
||||
};
|
||||
|
||||
describe("external DEX method-reference projection", () => {
|
||||
it("separates partial instruction positions from complete caller coverage", async () => {
|
||||
const bounded = structuredClone(xrefs);
|
||||
bounded.items[0]!.direct_invokes_exhaustive = false;
|
||||
bounded.items[0]!.direct_invokes = bounded.items[0]!.direct_invokes.slice(
|
||||
0,
|
||||
1,
|
||||
);
|
||||
expect(await analyze(summary, bounded)).toMatchObject({
|
||||
total: 1,
|
||||
coverage: "complete",
|
||||
reference_unit: "using_node",
|
||||
direct_invokes_coverage: {
|
||||
complete_using_nodes: 0,
|
||||
partial_using_nodes: 1,
|
||||
unknown_using_nodes: 0,
|
||||
recorded_sites: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("counts complete positions and preserves historical aggregate omission", async () => {
|
||||
const result = androidResultSchemas.trace_android_references.parse(
|
||||
await analyze(),
|
||||
);
|
||||
expect(result.direct_invokes_coverage).toEqual({
|
||||
complete_using_nodes: 1,
|
||||
partial_using_nodes: 0,
|
||||
unknown_using_nodes: 0,
|
||||
recorded_sites: 2,
|
||||
});
|
||||
const { direct_invokes_coverage: _coverage, ...historical } = result;
|
||||
expect(
|
||||
androidResultSchemas.trace_android_references.parse(historical),
|
||||
).not.toHaveProperty("direct_invokes_coverage");
|
||||
});
|
||||
|
||||
it("lists exact observed DEX descriptors when an external overload is ambiguous", async () => {
|
||||
const descriptors = [
|
||||
"(ILandroid/app/Notification;)V",
|
||||
"(Ljava/lang/String;ILandroid/app/Notification;)V",
|
||||
];
|
||||
const ambiguous = {
|
||||
...summary,
|
||||
observed_method_count: 2,
|
||||
prototypes: descriptors.map((dex_descriptor) => ({
|
||||
...summary.prototypes[0]!,
|
||||
dex_descriptor,
|
||||
})),
|
||||
};
|
||||
const failure = analyze(ambiguous);
|
||||
await expect(failure).rejects.toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [
|
||||
{
|
||||
path: ["overload_index"],
|
||||
reason: "invalid_value",
|
||||
},
|
||||
],
|
||||
});
|
||||
for (const descriptor of descriptors)
|
||||
await expect(failure).rejects.toMatchObject({
|
||||
issues: [{ message: expect.stringContaining(descriptor) }],
|
||||
});
|
||||
for (const index of descriptors.keys())
|
||||
await expect(failure).rejects.toMatchObject({
|
||||
issues: [
|
||||
{ message: expect.stringMatching(new RegExp(`\\b${index}\\b`, "u")) },
|
||||
],
|
||||
});
|
||||
await expect(failure).rejects.toMatchObject({
|
||||
issues: [
|
||||
{ message: expect.stringContaining(summary.prototypes[0]!.signature) },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("retains an external target, direct-only scope and an inspectable caller", async () => {
|
||||
const result = androidResultSchemas.trace_android_references.parse(
|
||||
await analyze(),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
target_definition: "external",
|
||||
reference_scope: "direct_dex_invokes",
|
||||
class_name: summary.class_name,
|
||||
dex_descriptor: summary.prototypes[0]!.dex_descriptor,
|
||||
total: 1,
|
||||
reference_unit: "using_node",
|
||||
references: [{ source_selector: selector }],
|
||||
});
|
||||
expect(
|
||||
result.references[0]!.direct_invokes?.map(
|
||||
(site) => site.dex_code_unit_offset,
|
||||
),
|
||||
).toEqual([0, 3]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "prototype", changed: { dex_descriptor: "()V" } },
|
||||
{ name: "definition", changed: { reference_target: undefined } },
|
||||
{ name: "scope", changed: { reference_scope: undefined } },
|
||||
{
|
||||
name: "caller selector",
|
||||
changed: { items: [{ ...xrefs.items[0]!, source_selector: undefined }] },
|
||||
},
|
||||
])(
|
||||
"refuses a changed external $name instead of reporting coverage",
|
||||
async ({ changed }) => {
|
||||
await expect(
|
||||
analyze(summary, { ...xrefs, ...changed } as typeof xrefs),
|
||||
).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it("does not accept reference-only metadata as an APK class definition", async () => {
|
||||
await expect(
|
||||
analyze(summary, xrefs, "inspect_android_class"),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,24 @@ it("recognizes named bridge failures without reinterpreting arbitrary engine tex
|
||||
expect(parseJadxFailure(value)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("recognizes only complete external-reference allowance diagnostics", () => {
|
||||
const failure = {
|
||||
kind: "reference-output-limit",
|
||||
stage: "caller_inventory",
|
||||
limit_bytes: 1_048_448,
|
||||
};
|
||||
expect(parseJadxFailure(JSON.stringify(failure))).toEqual(failure);
|
||||
for (const changed of [
|
||||
{ stage: "arbitrary" },
|
||||
{ limit_bytes: 0 },
|
||||
{ limit_bytes: 0.5 },
|
||||
{ extra: "unrelated" },
|
||||
])
|
||||
expect(
|
||||
parseJadxFailure(JSON.stringify({ ...failure, ...changed })),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates provider text envelopes and rejects unsupported content rather than dropping it", () => {
|
||||
expect(
|
||||
parseJadxEnvelope(
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import com.atxx.jhmcp.*;
|
||||
import com.android.tools.smali.smali.Smali;
|
||||
import com.android.tools.smali.smali.SmaliOptions;
|
||||
import com.google.gson.*;
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult;
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextContent;
|
||||
import jadx.core.dex.nodes.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.*;
|
||||
import kotlin.coroutines.EmptyCoroutineContext;
|
||||
import kotlinx.coroutines.BuildersKt;
|
||||
|
||||
/** Audited engine, real DEX instructions, no APK execution or simulated provider. */
|
||||
public class ReaJadxExternalReferencesRegression {
|
||||
private static final String API = "android.app.NotificationManager";
|
||||
private static final String INT_PROTO = "(ILandroid/app/Notification;)V";
|
||||
private static final String STRING_PROTO = "(Ljava/lang/String;ILandroid/app/Notification;)V";
|
||||
|
||||
private static void require(boolean value, String reason) {
|
||||
if (!value) throw new AssertionError(reason);
|
||||
}
|
||||
|
||||
private static JsonObject args(String className, String methodName, Integer index) {
|
||||
JsonObject args = new JsonObject();
|
||||
args.addProperty("class_name", className);
|
||||
if (methodName != null) args.addProperty("method_name", methodName);
|
||||
if (index != null) args.addProperty("overload_index", index);
|
||||
return args;
|
||||
}
|
||||
|
||||
private static JsonObject external(Integer index) {
|
||||
JsonObject value = args(API, "notify", index);
|
||||
value.addProperty("reference_target", true);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static JsonObject call(Method handle, Object bridge, String name, JsonObject args)
|
||||
throws Exception {
|
||||
var input = (kotlinx.serialization.json.JsonObject)
|
||||
kotlinx.serialization.json.Json.Default.parseToJsonElement(args.toString());
|
||||
CallToolResult result = (CallToolResult) handle.invoke(bridge, name, input);
|
||||
require(!Boolean.TRUE.equals(result.isError()), name + " returned error");
|
||||
require(result.getContent().size() == 1 && result.getContent().get(0) instanceof TextContent,
|
||||
"Unexpected bridge envelope");
|
||||
return JsonParser.parseString(((TextContent) result.getContent().get(0)).getText()).getAsJsonObject();
|
||||
}
|
||||
|
||||
private static void refused(Method handle, Object bridge, String name, JsonObject args,
|
||||
String field) throws Exception {
|
||||
try {
|
||||
call(handle, bridge, name, args);
|
||||
throw new AssertionError("Unexpected accepted selector: " + args);
|
||||
} catch (InvocationTargetException expected) {
|
||||
Throwable cause = expected.getCause();
|
||||
require(cause.getClass().getSimpleName().equals("InputFailure"), "Not an input failure: " + cause);
|
||||
Field actual = cause.getClass().getDeclaredField("field");
|
||||
actual.setAccessible(true);
|
||||
require(field.equals(actual.get(cause)), "Wrong selector failure field");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assemble(Path output, List<Path> sources) throws Exception {
|
||||
SmaliOptions options = new SmaliOptions();
|
||||
options.apiLevel = 28;
|
||||
options.jobs = 1;
|
||||
options.outputDexFile = output.toString();
|
||||
require(Smali.assemble(options, sources.stream().map(Path::toString).toList()), "Assembly failed");
|
||||
}
|
||||
|
||||
private static Path apk(Path output, String name, List<Path> dexFiles) throws Exception {
|
||||
Path apk = output.resolve(name);
|
||||
try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(apk))) {
|
||||
for (int index = 0; index < dexFiles.size(); index++) {
|
||||
ZipEntry entry = new ZipEntry(index == 0 ? "classes.dex" : "classes" + (index + 1) + ".dex");
|
||||
entry.setTime(0);
|
||||
zip.putNextEntry(entry);
|
||||
Files.copy(dexFiles.get(index), zip);
|
||||
zip.closeEntry();
|
||||
}
|
||||
}
|
||||
return apk;
|
||||
}
|
||||
|
||||
private static List<Path> fixtures(Path sources, Path output) throws Exception {
|
||||
Path single = output.resolve("external-single.dex");
|
||||
Path primary = output.resolve("external-primary.dex");
|
||||
Path secondary = output.resolve("external-secondary.dex");
|
||||
assemble(single, List.of(sources.resolve("Target.smali"), sources.resolve("Outer.smali"),
|
||||
sources.resolve("Caller.smali"), sources.resolve("Nested.smali")));
|
||||
assemble(primary, List.of(sources.resolve("Target.smali"), sources.resolve("Outer.smali")));
|
||||
assemble(secondary, List.of(sources.resolve("Caller.smali"), sources.resolve("Nested.smali")));
|
||||
return List.of(apk(output, "external-invokes.apk", List.of(single)),
|
||||
apk(output, "external-invokes-multidex.apk", List.of(primary, secondary)));
|
||||
}
|
||||
|
||||
private static void verify(SessionHolder holder, SessionConfig config, Constructor<?> constructor,
|
||||
Method handle, Path apk, String dexEntry) throws Exception {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), config, continuation));
|
||||
Object bridge = constructor.newInstance(holder);
|
||||
var root = holder.current().getClasses().get(0).getClassNode().root();
|
||||
IdentityHashMap<ClassNode, ProcessState> states = new IdentityHashMap<>();
|
||||
for (ClassNode cls : root.getClasses(true)) states.put(cls, cls.getState());
|
||||
JsonObject summary = call(handle, bridge, "get_class_summary", external(null));
|
||||
require(summary.get("observed_method_count").getAsInt() == 2, "Lost observed prototypes");
|
||||
require(!summary.has("fields") && !summary.has("field_count"), "External class definition was invented");
|
||||
JsonArray prototypes = summary.getAsJsonArray("prototypes");
|
||||
Method inventory = bridge.getClass().getDeclaredMethod("externalMethods", JsonObject.class);
|
||||
inventory.setAccessible(true);
|
||||
Object retained = inventory.invoke(bridge, external(null));
|
||||
require(retained == inventory.invoke(bridge, external(null)),
|
||||
"External summary and references rebuilt the same validated prototype inventory");
|
||||
require(prototypes.get(0).getAsJsonObject().get("dex_descriptor").getAsString().equals(INT_PROTO)
|
||||
&& prototypes.get(1).getAsJsonObject().get("dex_descriptor").getAsString().equals(STRING_PROTO),
|
||||
"Observed overload identities or order changed");
|
||||
refused(handle, bridge, "get_xrefs_to_method", external(null), "overload_index");
|
||||
refused(handle, bridge, "get_xrefs_to_method", external(2), "overload_index");
|
||||
refused(handle, bridge, "get_class_summary", args(API, null, null), "class_name");
|
||||
JsonObject missing = external(null);
|
||||
missing.addProperty("method_name", "notPresent");
|
||||
refused(handle, bridge, "get_class_summary", missing, "method_name");
|
||||
JsonObject references = call(handle, bridge, "get_xrefs_to_method", external(0));
|
||||
require(references.get("count").getAsInt() == 3, "Wrong using-node inventory");
|
||||
require(references.get("reference_scope").getAsString().equals("direct_dex_invokes"), "Missing scope");
|
||||
Set<Integer> callerOverloads = new HashSet<>();
|
||||
List<JsonObject> selectors = new ArrayList<>();
|
||||
int sites = 0;
|
||||
for (JsonElement item : references.getAsJsonArray("items")) {
|
||||
JsonObject caller = item.getAsJsonObject();
|
||||
JsonObject selector = caller.getAsJsonObject("source_selector");
|
||||
selectors.add(selector);
|
||||
if (selector.get("class_name").getAsString().equals("fixture.Caller"))
|
||||
callerOverloads.add(selector.get("overload_index").getAsInt());
|
||||
require(caller.get("direct_invokes_exhaustive").getAsBoolean(), "Fixture offsets were truncated");
|
||||
for (JsonElement direct : caller.getAsJsonArray("direct_invokes")) {
|
||||
sites++;
|
||||
JsonObject site = direct.getAsJsonObject();
|
||||
require(site.get("dex_entry").getAsString().equals(dexEntry), "Lost source DEX carrier");
|
||||
require(site.get("source_raw_method_id").getAsString().contains("notify"), "Lost raw caller identity");
|
||||
}
|
||||
}
|
||||
require(sites == 4 && callerOverloads.equals(Set.of(0, 1)), "Repeated or overloaded callers lost");
|
||||
for (var state : states.entrySet()) require(state.getKey().getState() == state.getValue(),
|
||||
"External reference scan generated source: " + state.getKey().getRawName());
|
||||
for (JsonObject selector : selectors) {
|
||||
JsonObject body = call(handle, bridge, "get_method_body", selector);
|
||||
require(!body.get("body").getAsString().isEmpty(), "Caller selector did not produce a body");
|
||||
}
|
||||
require(references.equals(call(handle, bridge, "get_xrefs_to_method", external(0))),
|
||||
"Caller identities changed after source generation");
|
||||
require(call(handle, bridge, "get_xrefs_to_method", external(1)).get("count").getAsInt() == 1,
|
||||
"Other prototype references mixed with int overload");
|
||||
JsonObject control = call(handle, bridge, "get_xrefs_to_method", args("fixture.Target", "mark", 0));
|
||||
require(control.get("count").getAsInt() == 1, "Defined target reference behavior changed");
|
||||
Object priorSessionInventory = inventory.invoke(bridge, external(null));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), config, continuation));
|
||||
require(priorSessionInventory != inventory.invoke(bridge, external(null)),
|
||||
"Replaced session reused a prior APK prototype inventory");
|
||||
require(references.equals(call(handle, bridge, "get_xrefs_to_method", external(0))),
|
||||
"Session replacement changed exact caller facts");
|
||||
}
|
||||
|
||||
private static Path largeFixture(Path output) throws Exception {
|
||||
Path source = output.resolve("LargeExternalCaller.smali");
|
||||
StringBuilder text = new StringBuilder(".class public Lfixture/LargeExternalCaller;\n.super Ljava/lang/Object;\n");
|
||||
for (int index = 0; index < 220; index++) {
|
||||
text.append(".method public static call").append(index).append("_").append("x".repeat(2000))
|
||||
.append("(Landroid/app/NotificationManager;Landroid/app/Notification;I)V\n.registers 3\n")
|
||||
.append("invoke-virtual {p0, p2, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V\n")
|
||||
.append("return-void\n.end method\n");
|
||||
}
|
||||
Files.writeString(source, text, StandardCharsets.UTF_8);
|
||||
Path dex = output.resolve("external-large.dex");
|
||||
assemble(dex, List.of(source));
|
||||
return apk(output, "external-invokes-large.apk", List.of(dex));
|
||||
}
|
||||
|
||||
private static Path siteHeavyFixture(Path output) throws Exception {
|
||||
String invoke = "invoke-virtual {p0, p2, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V\n";
|
||||
String parameters = "(Landroid/app/NotificationManager;Landroid/app/Notification;I)V\n.registers 3\n";
|
||||
Path heavy = output.resolve("AHeavy.smali");
|
||||
Files.writeString(heavy, ".class public Lfixture/AHeavy;\n.super Ljava/lang/Object;\n"
|
||||
+ ".method public static call" + parameters + invoke.repeat(10000)
|
||||
+ "return-void\n.end method\n", StandardCharsets.UTF_8);
|
||||
Path shortCallers = output.resolve("ZShort.smali");
|
||||
StringBuilder text = new StringBuilder(".class public Lfixture/ZShort;\n.super Ljava/lang/Object;\n");
|
||||
for (int index = 0; index < 10; index++)
|
||||
text.append(".method public static tail").append(index).append(parameters).append(invoke)
|
||||
.append("return-void\n.end method\n");
|
||||
Files.writeString(shortCallers, text, StandardCharsets.UTF_8);
|
||||
Path dex = output.resolve("external-site-heavy.dex");
|
||||
assemble(dex, List.of(heavy, shortCallers));
|
||||
return apk(output, "external-invokes-site-heavy.apk", List.of(dex));
|
||||
}
|
||||
|
||||
private static Path oversizedPrototypeFixture(Path output) throws Exception {
|
||||
Path source = output.resolve("OversizedPrototype.smali");
|
||||
String parameter = "Lexternal/" + "a".repeat(12000) + ";";
|
||||
Files.writeString(source, ".class public Lfixture/OversizedPrototype;\n.super Ljava/lang/Object;\n"
|
||||
+ ".method public static call()V\n.registers 240\n"
|
||||
+ "invoke-static/range {v0 .. v239}, Lexternal/Library;->wide(" + parameter.repeat(240) + ")V\n"
|
||||
+ "return-void\n.end method\n", StandardCharsets.UTF_8);
|
||||
Path dex = output.resolve("external-oversized-prototype.dex");
|
||||
assemble(dex, List.of(source));
|
||||
return apk(output, "external-oversized-prototype.apk", List.of(dex));
|
||||
}
|
||||
|
||||
private static void verifyPrototypeLimit(SessionHolder holder, SessionConfig config,
|
||||
Constructor<?> constructor, Method handle, Path apk) throws Exception {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), config, continuation));
|
||||
Object bridge = constructor.newInstance(holder);
|
||||
JsonObject selection = args("external.Library", "wide", null);
|
||||
selection.addProperty("reference_target", true);
|
||||
try {
|
||||
call(handle, bridge, "get_class_summary", selection);
|
||||
throw new AssertionError("Oversized external prototype was accepted");
|
||||
} catch (InvocationTargetException expected) {
|
||||
Throwable cause = expected.getCause();
|
||||
require(cause.getClass().getSimpleName().equals("ReferenceLimit"), "Not a typed prototype limit: " + cause);
|
||||
Field stage = cause.getClass().getDeclaredField("stage");
|
||||
stage.setAccessible(true);
|
||||
require("method_inventory".equals(stage.get(cause)), "Wrong prototype limit stage");
|
||||
}
|
||||
System.out.println("EXTERNAL_PROTOTYPE_LIMIT_PASS single_prototype=true typed_method_inventory=true");
|
||||
}
|
||||
|
||||
private static void verifySiteBudget(SessionHolder holder, SessionConfig config,
|
||||
Constructor<?> constructor, Method handle, Path apk) throws Exception {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), config, continuation));
|
||||
Object bridge = constructor.newInstance(holder);
|
||||
JsonObject references = call(handle, bridge, "get_xrefs_to_method", external(0));
|
||||
require(references.get("count").getAsInt() == 11, "Optional sites displaced mandatory caller rows");
|
||||
JsonArray callers = references.getAsJsonArray("items");
|
||||
JsonObject heavy = callers.get(0).getAsJsonObject();
|
||||
require(!heavy.get("direct_invokes_exhaustive").getAsBoolean()
|
||||
&& heavy.getAsJsonArray("direct_invokes").size() < 10000, "Site-heavy caller was not partial");
|
||||
for (int index = 1; index < callers.size(); index++)
|
||||
require(callers.get(index).getAsJsonObject().get("containing_class").getAsString()
|
||||
.equals("fixture.ZShort"), "Later short caller identity was lost");
|
||||
require(references.toString().getBytes(StandardCharsets.UTF_8).length < 1048576,
|
||||
"External representation exceeded its reserved frame share");
|
||||
require(references.equals(call(handle, bridge, "get_xrefs_to_method", external(0))),
|
||||
"Partial-site replay changed the caller inventory");
|
||||
System.out.println("EXTERNAL_SITE_BUDGET_PASS callers=11 partial_sites=true bounded=true replay=true");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Path output = Path.of(args[1]);
|
||||
List<Path> fixtures = fixtures(Path.of(args[0]), output);
|
||||
largeFixture(output);
|
||||
Path siteHeavy = siteHeavyFixture(output);
|
||||
Path oversizedPrototype = oversizedPrototypeFixture(output);
|
||||
SessionConfig config = new SessionConfig(1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
|
||||
SessionHolder holder = new SessionHolder(config);
|
||||
Class<?> type = Class.forName("ReaJadxBridge");
|
||||
Constructor<?> constructor = type.getDeclaredConstructor(SessionHolder.class);
|
||||
constructor.setAccessible(true);
|
||||
Method handle = type.getDeclaredMethod("handle", String.class, kotlinx.serialization.json.JsonObject.class);
|
||||
handle.setAccessible(true);
|
||||
try {
|
||||
verify(holder, config, constructor, handle, fixtures.get(0), "classes.dex");
|
||||
verify(holder, config, constructor, handle, fixtures.get(1), "classes2.dex");
|
||||
verifySiteBudget(holder, config, constructor, handle, siteHeavy);
|
||||
verifyPrototypeLimit(holder, config, constructor, handle, oversizedPrototype);
|
||||
System.out.println("EXTERNAL_REFERENCE_REGRESSION_PASS prototypes=2 using_nodes=3 sites=4 overloaded_callers=true nested=true multidex=true no_source=true post_codegen=true");
|
||||
} finally {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.class public Lfixture/Caller;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public static notify0(Landroid/app/NotificationManager;Landroid/app/Notification;)V
|
||||
.registers 3
|
||||
const/4 v0, 0x1
|
||||
invoke-virtual {p0, v0, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V
|
||||
invoke-virtual {p0, v0, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V
|
||||
invoke-static {}, Lfixture/Target;->mark()V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static notify0(Landroid/app/NotificationManager;Landroid/app/Notification;I)V
|
||||
.registers 3
|
||||
invoke-virtual {p0, p2, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static notify1(Landroid/app/NotificationManager;Ljava/lang/String;Landroid/app/Notification;)V
|
||||
.registers 4
|
||||
const/4 v0, 0x2
|
||||
invoke-virtual {p0, p1, v0, p2}, Landroid/app/NotificationManager;->notify(Ljava/lang/String;ILandroid/app/Notification;)V
|
||||
return-void
|
||||
.end method
|
||||
@@ -0,0 +1,16 @@
|
||||
.class public Lfixture/Outer$Worker;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.annotation system Ldalvik/annotation/EnclosingClass;
|
||||
value = Lfixture/Outer;
|
||||
.end annotation
|
||||
.annotation system Ldalvik/annotation/InnerClass;
|
||||
accessFlags = 0x9
|
||||
name = "Worker"
|
||||
.end annotation
|
||||
|
||||
.method public static notifyNested(Landroid/app/NotificationManager;Landroid/app/Notification;I)V
|
||||
.registers 3
|
||||
invoke-virtual {p0, p2, p1}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V
|
||||
return-void
|
||||
.end method
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public Lfixture/Outer;
|
||||
.super Ljava/lang/Object;
|
||||
@@ -0,0 +1,7 @@
|
||||
.class public Lfixture/Target;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public static mark()V
|
||||
.registers 0
|
||||
return-void
|
||||
.end method
|
||||
Reference in new issue
Block a user