fix(android): bound split facts and keep saved JNI reports readable

Charge the manifest split facts against the native inventory's retained JSON
allowance and refuse more than 64 split types or 256-character names as an
unknown, so a crafted manifest cannot overflow the response. Reports saved
before split facts existed still parse: the report schema treats absent
facts as optional while the live bridge contract requires them. The JADX
regression now decodes a generated binary manifest through
nativeDeclarations, with named and resource-ID-only attributes.

Refs #9756

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
gensokyo-devandgensokyo-dev authored and Ysopwnable committed 2026-11-14 16:15:32 +01:00
1 parent 18b72ca461
commit 9943c1c8bc
5 files changed
+123 -17

No files matched your search

@@ -43,7 +43,7 @@ public class ReaJadxMetadataRegression {
return ((TextContent) response.getContent().get(0)).getText();
}
private static void verifyManifestSummary() {
private static void verifyManifestSummary() throws Exception {
for (String prefix : List.of("android", "a")) {
for (String encoded : List.of("one &amp; beta", "one &amp;amp; beta")) {
String xml = "<manifest xmlns:" + prefix + "=\"http://schemas.android.com/apk/res/android\""
@@ -81,7 +81,7 @@ public class ReaJadxMetadataRegression {
verifyManifestSplitFacts();
}
private static void verifyManifestSplitFacts() {
private static void verifyManifestSplitFacts() throws Exception {
String android = "xmlns:a=\"http://schemas.android.com/apk/res/android\"";
Map<String, Object> split = ReaJadxBridge.manifestSplitFacts("<manifest " + android
+ " a:requiredSplitTypes=\"base__abi, base__density\"><application a:isSplitRequired=\"true\"/>"
@@ -92,9 +92,83 @@ public class ReaJadxMetadataRegression {
+ " requiredSplitTypes=\"unnamespaced\"><application a:isSplitRequired=\"false\"/></manifest>");
require(plain.get("required_split_types").equals(List.of())
&& plain.get("split_required").equals(false),
"Split facts came from an unnamespaced or false declaration: " + plain);
"Split facts came from an unnamespaced or false declaration: " + plain); boolean refused = false;
try {
ReaJadxBridge.manifestSplitFacts("<manifest " + android + " a:requiredSplitTypes=\""
+ String.join(",", Collections.nCopies(65, "s")) + "\"/>");
} catch (IllegalArgumentException expected) { refused = true; }
require(refused, "An unbounded requiredSplitTypes list was admitted");
// The bridge decodes the binary manifest itself, without a session; framework
// attribute names must resolve through their resource IDs when names are stripped.
for (boolean named : List.of(true, false)) {
Path apk = Files.createTempFile("rea-split-manifest-", ".apk");
try {
try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(apk))) {
zip.putNextEntry(new ZipEntry("AndroidManifest.xml"));
zip.write(splitManifest(named));
zip.closeEntry();
}
Object facts = ReaJadxBridge.nativeDeclarations(apk).get("manifest_splits");
require(facts.equals(Map.of("status", "observed",
"required_split_types", List.of("base__abi", "base__density"), "split_required", true)),
"Binary manifest split facts were lost (named=" + named + "): " + facts);
} finally {
Files.deleteIfExists(apk);
}
}
}
/** A binary manifest: requiredSplitTypes on <manifest>, isSplitRequired on <application>. */
private static byte[] splitManifest(boolean namedAttributes) {
List<String> strings = List.of("http://schemas.android.com/apk/res/android", "manifest",
namedAttributes ? "requiredSplitTypes" : "", "base__abi,base__density", "application",
namedAttributes ? "isSplitRequired" : "x");
java.io.ByteArrayOutputStream data = new java.io.ByteArrayOutputStream();
java.nio.ByteBuffer offsets = littleEndian(4 * strings.size());
for (String value : strings) {
offsets.putInt(data.size());
byte[] utf16 = value.getBytes(StandardCharsets.UTF_16LE);
data.writeBytes(littleEndian(2).putShort((short) value.length()).array());
data.writeBytes(utf16);
data.writeBytes(new byte[2]);
}
byte[] pool = chunk(0x0001, 28, littleEndian(20 + offsets.capacity() + data.size())
.putInt(strings.size()).putInt(0).putInt(0).putInt(28 + offsets.capacity()).putInt(0)
.put(offsets.array()).put(data.toByteArray()).array());
java.nio.ByteBuffer ids = littleEndian(4 * 6);
for (int id : new int[] {0, 0, 0x0101064e, 0, 0, 0x01010591}) ids.putInt(id);
byte[] resources = chunk(0x0180, 8, ids.array());
byte[] manifest = startElement(1, 2, 3, 0x03, 3);
byte[] application = startElement(4, 5, -1, 0x12, -1);
java.io.ByteArrayOutputStream body = new java.io.ByteArrayOutputStream();
for (byte[] part : List.of(pool, resources, manifest, application, endElement(4), endElement(1)))
body.writeBytes(part);
return chunk(0x0003, 8, body.toByteArray());
}
private static java.nio.ByteBuffer littleEndian(int size) {
return java.nio.ByteBuffer.allocate(size).order(java.nio.ByteOrder.LITTLE_ENDIAN);
}
private static byte[] chunk(int type, int headerSize, byte[] body) {
return littleEndian(8 + body.length).putShort((short) type).putShort((short) headerSize)
.putInt(8 + body.length).put(body).array();
}
/** One element with one android-namespaced attribute row. */
private static byte[] startElement(int name, int attribute, int raw, int type, int value) {
return chunk(0x0102, 16, littleEndian(48).putInt(-1).putInt(-1).putInt(-1).putInt(name)
.putShort((short) 20).putShort((short) 20).putShort((short) 1).putShort((short) 0)
.putShort((short) 0).putShort((short) 0)
.putInt(0).putInt(attribute).putInt(raw).putShort((short) 8).put((byte) 0)
.put((byte) type).putInt(value).array());
}
private static byte[] endElement(int name) {
return chunk(0x0103, 16, littleEndian(16).putInt(-1).putInt(-1).putInt(-1).putInt(name).array());
}
private static void verifyRootManifests(SessionHolder holder, SessionConfig config,
Constructor<?> constructor, Method handle, Path output) throws Exception {
verifyManifestSummary();