Skip to content

fix(android): qualify the JNI library inventory by required splits - #10549

Merged
N0zoM1z0 merged 2 commits into
mainfrom
fix/jni-required-splits
Nov 14, 2026
Merged

N0zoM1z0 merged 2 commits into
mainfrom
fix/jni-required-splits

Conversation

@N0zoM1z0

@N0zoM1z0 N0zoM1z0 commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

inspect-android-jni-bindings reported library_inventory.status: "complete" with zero libraries for a base APK whose manifest requires ABI/density splits, where the native libraries actually live. It now says the inventory is partial because of required splits.

Fixes #9756

Change and scope

  • Bridge (identity 10 → 11). rea_native_declarations also decodes the root binary AndroidManifest.xml without a loaded session (JADX's BinaryXMLParser with a fresh RootNode, which resolves framework attribute names through their resource IDs). It reports manifest_splits: observed with required_split_types (from android:requiredSplitTypes) and split_required (from <application android:isSplitRequired>), or unknown with a reason (absent, ambiguous or unreadable manifest). The manifest is read with the same exact-size, CRC-checked member reader as DEX (8 MiB cap). Split facts are bounded (64 types of at most 256 characters, or unknown) and charged against the inventory's retained JSON allowance. manifestRoot is the one XML owner shared with manifestSummary.
  • Domain. native_inventory.manifest_splits is part of the schema and the report. When splits are required or their requirements are unknown, library_inventory becomes partial with an unknown that names the split types. A binding without a candidate is then unknown/partial instead of unmatched/complete. APKs without required splits are unchanged.
  • Credit: fix(android): report split-required manifests as a JNI library unknown #9769 (2SinceJuly) and fix: batch of small fixes for #9756 and #9880 #9887 (Phon) proposed TypeScript AXML readers. Those are not taken, since the bridge already owns manifest decoding, but fix(android): report split-required manifests as a JNI library unknown #9769's binary-manifest fixture was used for the real-JADX check below. Both are co-authors.

Contribution scope: ordinary fix within the existing JADX bridge and JNI report boundaries; maintainer scope decision not applicable.

  • Read existing files or captured data.

Evidence and regression coverage

Real JADX: the bridge was compiled with JDK 21.0.13 against the pinned jadx-headless-mcp-0.7.1-all.jar (SHA-256 6e5eacf5…ff75), and nativeDeclarations was run on six APKs built from #9769's binary-manifest fixture:

APK manifest_splits
requiredSplitTypes="base__abi,base__density", isSplitRequired="true" observed, [base__abi, base__density], true
same, with stripped attribute names (resource IDs only) same
no split attributes observed, [], false
no manifest unknown: absent
two AndroidManifest.xml entries unknown: ambiguous
non-AXML manifest bytes unknown: not well-formed XML

ReaJadxMetadataRegression.java gains split checks, which passed against the real JAR: manifestSplitFacts on namespaced vs unnamespaced and true vs false declarations; refusal of a 65-entry list; and, after review, a generated binary manifest decoded through nativeDeclarations with named and resource-ID-only attribute names. Unit tests cover the producing-boundary schema (AndroidNativeInventory.test.ts, including a bridge that omits the field) and the projection (androidJniBindings.test.ts: required, unknown, none).

Validation performed

  • npm run test:local -- on AndroidNativeInventory.test.ts and androidJniBindings.test.ts: 41 passed. npm run test:focused -- on six Android boundary test files: 71 passed.
  • npm run check:fast (pre-push): passed. oxfmt --check: clean.

Compatibility, safety, and release

  • The bridge identity changes to 11, so cached bridge builds are rebuilt. The live bridge contract requires manifest_splits. Saved JNI reports without it still parse, because the field is optional in the report schema.

Author readiness

Human owner: N0zoM1z0 (PR author, exempt from the declaration per CONTRIBUTING.md).

After merge, close #9769 (superseded; fixture credited) and note #9887 (already closed).

@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-14T14:29:38.708216Z
We couldn’t complete this review. Comment @codex review to try again.
f2f058d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@greptile-apps

greptile-apps Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5 Tier: plus

[Medium impact] The change affects conclusions from Android JNI inspection reports.

Hold merging until saved JNI reports remain readable and the existing response-size and binary-manifest test findings are addressed.

Findings

  1. P1 Saved JNI reports stop opening ▶
  2. P2 Security Split facts exceed response limits ▶
  3. P2 Binary manifest decoding lacks tests ▶
Fix with agent prompt
### Issue 1
src/domain/android/androidNativeDeclarations.ts:33-41
Making `manifest_splits` required also rejects saved JNI reports created before this change. `inspect_analysis_view` reads those reports through this schema, so users get an input error instead of a summary or collection page.

Keep the live bridge check strict, but let the saved-report reader represent missing split facts as unknown without changing the original Evidence.

### Issue 2
bridge/android/ReaJadxBridge.java:315-325
`manifestSplits` bypasses the existing `retainedBytes` budget. A crafted manifest below the 8 MiB input cap can contain enough comma-separated `requiredSplitTypes` values to exceed the 8 MiB MCP frame after JSON escaping. The host then closes the worker and loses the entire declaration result.

The repository requires accounting for representation expansion before allocation. Before merging, include split facts in the shared output budget and return a bounded unknown reason when they do not fit.

**How this was verified:** APK-controlled split values are copied into the response without a byte charge, while the receiving transport closes on frames above 8 MiB.

### Issue 3
tests/conformance/android/ReaJadxMetadataRegression.java:84-95
The new cases pass handwritten XML to `manifestSplitFacts`, so they never exercise `manifestSplitRequirements` or the fresh JADX parser. Resolving attribute names from resource IDs is central to this fix, but only the manual checks in the PR description cover it. These tests can stay green if that decoding breaks.

The repository requires parser fixes to cover the producing boundary. Before merging, add a checked-in binary-manifest regression through `nativeDeclarations`, including stripped attribute names and an unreadable-manifest control.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR reads root-manifest split requirements without loading the APK and carries them into JNI reports.

  • JNI reports account for libraries that may live in required APK splits.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Selected APK] --> B[Read and decode root manifest]
  B --> C{Split requirements}
  C -->|Required or unknown| D[Qualify library inventory as partial]
  C -->|No required splits| E[Keep existing library coverage]
  D --> F[Project JNI bindings]
  E --> F
  F --> G[Report, summary and collection pages]
Loading

Reviews (2) · Last reviewed commit: "fix(android): qualify the JNI library in..." · Reviewed by Greptile

Comment on lines +33 to +41
/** Root manifest split requirements; required splits can carry native libraries. */
manifest_splits: z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("observed"),
required_split_types: z.array(z.string().min(1)),
split_required: z.boolean(),
}),
z.strictObject({ status: z.literal("unknown"), reason: z.string().min(1) }),
]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Saved JNI reports stop opening

Making manifest_splits required also rejects saved JNI reports created before this change. inspect_analysis_view reads those reports through this schema, so users get an input error instead of a summary or collection page.

Keep the live bridge check strict, but let the saved-report reader represent missing split facts as unknown without changing the original Evidence.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/android/androidNativeDeclarations.ts
Line: 33-41

Comment:
**Saved JNI reports stop opening**

Making `manifest_splits` required also rejects saved JNI reports created before this change. `inspect_analysis_view` reads those reports through this schema, so users get an input error instead of a summary or collection page.

Keep the live bridge check strict, but let the saved-report reader represent missing split facts as unknown without changing the original Evidence.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in "bound split facts and keep saved JNI reports readable": the live bridge contract (androidNativeDeclarationsSchema) still requires manifest_splits, while the report's native_inventory schema extends it with manifest_splits as exact-optional, so reports saved before this change still parse. A test drops the field from a projected report and parses it.

Comment on lines +315 to +325
for (String type : root.getAttributeNS(android, "requiredSplitTypes").split(",")) {
if (!type.isBlank()) types.add(type.strip());
}
boolean required = false;
for (Node child = root.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof Element element && element.getNamespaceURI() == null
&& element.getTagName().equals("application"))
required |= element.getAttributeNS(android, "isSplitRequired").equals("true");
}
return Map.of("status", "observed", "required_split_types", List.copyOf(types),
"split_required", required);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Split facts exceed response limits

manifestSplits bypasses the existing retainedBytes budget. A crafted manifest below the 8 MiB input cap can contain enough comma-separated requiredSplitTypes values to exceed the 8 MiB MCP frame after JSON escaping. The host then closes the worker and loses the entire declaration result.

The repository requires accounting for representation expansion before allocation. Before merging, include split facts in the shared output budget and return a bounded unknown reason when they do not fit.

How this was verified: APK-controlled split values are copied into the response without a byte charge, while the receiving transport closes on frames above 8 MiB.

Context Used: AGENTS.md (source)

Knowledge Base Used: Bridge implementations

Prompt To Fix With AI
This is a comment left during a code review.
Path: bridge/android/ReaJadxBridge.java
Line: 315-325

Comment:
**Split facts exceed response limits**

`manifestSplits` bypasses the existing `retainedBytes` budget. A crafted manifest below the 8 MiB input cap can contain enough comma-separated `requiredSplitTypes` values to exceed the 8 MiB MCP frame after JSON escaping. The host then closes the worker and loses the entire declaration result.

The repository requires accounting for representation expansion before allocation. Before merging, include split facts in the shared output budget and return a bounded unknown reason when they do not fit.

**How this was verified:** APK-controlled split values are copied into the response without a byte charge, while the receiving transport closes on frames above 8 MiB.

**Context Used:** AGENTS.md ([source](https://github.com/morluto/rea/blob/main/AGENTS.md))

**Knowledge Base Used:** [Bridge implementations](https://app.greptile.com/-/switch-tenant?tenant=pre&next=%2Fcustom-context%2Fknowledge-base%2Fmorluto%2Frea%2F-%2Fdocs%2Fbridge-implementations.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Split facts are now bounded at 64 types of at most 256 characters each; beyond that, the facts are unknown with a reason, which is itself capped at 512 characters. The facts are also charged against the inventory's retainedBytes (6x for JSON escaping) before any DEX row is retained, so they share the existing 4 MiB allowance. The Java regression checks that a 65-entry list is refused.

Comment on lines +84 to +95
private static void verifyManifestSplitFacts() {
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\"/>"
+ "<other a:isSplitRequired=\"true\"/></manifest>");
require(split.get("required_split_types").equals(List.of("base__abi", "base__density"))
&& split.get("split_required").equals(true), "Required split facts were lost: " + split);
Map<String, Object> plain = ReaJadxBridge.manifestSplitFacts("<manifest " + android
+ " 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Binary manifest decoding lacks tests

The new cases pass handwritten XML to manifestSplitFacts, so they never exercise manifestSplitRequirements or the fresh JADX parser. Resolving attribute names from resource IDs is central to this fix, but only the manual checks in the PR description cover it. These tests can stay green if that decoding breaks.

The repository requires parser fixes to cover the producing boundary. Before merging, add a checked-in binary-manifest regression through nativeDeclarations, including stripped attribute names and an unreadable-manifest control.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/conformance/android/ReaJadxMetadataRegression.java
Line: 84-95

Comment:
**Binary manifest decoding lacks tests**

The new cases pass handwritten XML to `manifestSplitFacts`, so they never exercise `manifestSplitRequirements` or the fresh JADX parser. Resolving attribute names from resource IDs is central to this fix, but only the manual checks in the PR description cover it. These tests can stay green if that decoding breaks.

The repository requires parser fixes to cover the producing boundary. Before merging, add a checked-in binary-manifest regression through `nativeDeclarations`, including stripped attribute names and an unreadable-manifest control.

**Context Used:** AGENTS.md ([source](https://github.com/morluto/rea/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. The regression now generates a binary AndroidManifest.xml (string pool, resource-ID map, <manifest> and <application> elements) with named and resource-ID-only attribute names, zips it, and runs ReaJadxBridge.nativeDeclarations. That covers manifestSplitRequirements and the fresh-RootNode BinaryXMLParser decode. Both variants report observed, [base__abi, base__density], true against the real jadx-headless-mcp-0.7.1-all.jar on JDK 21.

gensokyo-dev and others added 2 commits November 14, 2026 14:42
A base APK whose manifest requires ABI or density splits keeps its native
libraries in those splits, yet inspect_android_jni_bindings reported a
complete library inventory with zero libraries. The bridge's native
declaration inventory now also decodes the root binary manifest without a
session and reports its requiredSplitTypes and isSplitRequired facts, or why
they are unknown (bridge identity 11). When splits are required or unknown,
the library inventory is partial with an unknown that says so, and a binding
without a candidate is unknown instead of unmatched.

The binary manifest fixture from #9769 was used to check the bridge with the
real jadx-headless-mcp 0.7.1 JAR, including stripped attribute names that
resolve through framework resource IDs.

Fixes #9756

Co-Authored-By: 2SinceJuly <[email protected]>
Co-Authored-By: Phon <[email protected]>
Co-Authored-By: gensokyo-dev <[email protected]>
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 <[email protected]>
@gensokyo-dev
gensokyo-dev force-pushed the fix/jni-required-splits branch from f2f058d to 1ce1c22 Compare November 14, 2026 14:42
@N0zoM1z0
N0zoM1z0 merged commit 9943c1c into main Nov 14, 2026
52 of 54 checks passed
@N0zoM1z0
N0zoM1z0 deleted the fix/jni-required-splits branch November 14, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] inspect-android-jni-bindings reports complete with zero libraries for a base APK whose native libraries are in required splits

2 participants