[GH-ISSUE #6866] [Bug] Android package inspection can select an asset manifest before the root manifest #4204

Closed
opened 2026-10-31 00:47:42 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6866

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

Source-only finding: inspect_android_package can use assets/AndroidManifest.xml as the package's manifest when that ordinary asset precedes the root AndroidManifest.xml in ZIP entry order. The pinned manifest loader chooses the first resource whose lowercased name ends in androidmanifest.xml, without requiring the root path or manifest resource type. Both get_app_info and get_android_manifest use that cached selection, and REA publishes it as the package summary and complete manifest.

A source/adapter probe with an asset-first fixture normalized package org.example.asset and version asset-9; reversing only resource order returned the actual root package org.example.actual and version actual-1. This is not an executed Kotlin/JADX reproduction: resource selection was explicitly simulated from pinned source before invoking REA's real normalization.

Reproduction

The concrete source trigger is a standalone APK with this central-directory order and two readable XML resources:

1. assets/AndroidManifest.xml  -> package org.example.asset, version asset-9
2. AndroidManifest.xml         -> package org.example.actual, version actual-1

The asset is ordinary packaged data, not a second APK or application entry point. With a built checkout, save this exact executed source/adapter probe as /tmp/rea-manifest-order.mjs, then run REA_ROOT="$PWD" node /tmp/rea-manifest-order.mjs from the checkout:

import { pathToFileURL } from 'node:url';
const { analyzeJadxRequest } = await import(pathToFileURL(`${process.env.REA_ROOT}/dist/android/JadxAnalysis.js`));
const engine = { name: 'source-simulation', version: '0.7.1', artifact_sha256: 'a'.repeat(64), source_revision: null, worker_count: 1, heap_limit_mib: null };
const manifest = (pkg, version) => `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${pkg}" android:versionName="${version}" android:versionCode="7"><application android:hasCode="false"/></manifest>`;
const entries = [
  { path: 'assets/AndroidManifest.xml', xml: manifest('org.example.asset', 'asset-9') },
  { path: 'AndroidManifest.xml', xml: manifest('org.example.actual', 'actual-1') },
];
for (const [name, resources] of [['asset-first', entries], ['root-first-control', [...entries].reverse()]]) {
  // Mirror the pinned loadManifestText predicate, not an executed JVM loader.
  const selected = resources.find(({ path }) => path.toLowerCase().endsWith('androidmanifest.xml'));
  const xml = selected.xml;
  const app = { package: /<manifest\b[^>]*\bpackage="([^"]+)"/.exec(xml)?.[1] ?? '', version_name: /android:versionName="([^"]+)"/.exec(xml)?.[1] ?? '', version_code: '7', min_sdk: '', target_sdk: '', permissions: [] };
  const result = await analyzeJadxRequest({
    json: async () => app, text: async () => xml,
    listClasses: async () => { throw new Error('unused'); },
  }, { operation: 'inspect_android_package', input: { path: '/fixture.apk' } }, {
    state: 'LOADED', apk_path: '/fixture.apk', class_count: 0, resource_count: 2, threads: 1, resources: 'full',
  }, engine);
  console.log(JSON.stringify({ name, source_derived_selected_path: selected.path, package_name: result.package_name, version_name: result.version_name, manifest_status: result.manifest.status }));
}

The reduced probe ran once before drafting. An earlier probe created two self-authored ZIP/XML .apk fixtures in these orders, confirmed production parseBinaryTarget admitted both as apk, enumerated their real central-directory entry order, simulated the same selection, and observed the same normalized results. These synthetic ZIP/XML fixtures were not claimed to be installable Android applications or decoded by JADX. Exact-root-member reads returned org.example.actual from both.

The reachable production requests are CLI rea inspect-android-package <apk> and MCP inspect_android_package with {"path":"<apk>"}. Their full JVM execution was not run.

Expected behavior

Package inspection should bind its summary and returned manifest to the root AndroidManifest.xml, independent of unrelated asset order. Android defines the package manifest as the application manifest, and REA describes this operation as package declarations plus decoded manifest. JADX itself classifies only exact AndroidManifest.xml as MANIFEST; the asset remains an XML resource.

Impact and scope

The bounded source counterexample substitutes an unrelated asset's package/version and manifest for the root package facts. The root-first control succeeds. This is ordinary static artifact selection correctness; it asserts no authenticity or signing behavior. Real APK prevalence and JVM behavior were not measured. Exact ZIP-root-member inspection recovers the actual manifest in both synthetic fixtures; inspecting only the returned simulated manifest would show the selected asset instead.

REA version

Source 4526862728af01f1135714079c0d8a8c754eb364 (main, package 6.7.0). Relevant inspectPackage normalization is byte-identical to latest published tag rea-agents-6.7.0, commit 27dc42d10249f0410904a75c0997842eb03b931c; the release was source-compared, not separately executed. The audited producer is jadx-headless-mcp 0.7.1 at 5844800a486d2248fa949b74c9896285f5b54de2, with JADX 1.5.6. Relevant producer files match upstream release tag v0.7.1.

Runtime and platform

Executed adapter and ZIP/admission probes: Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64. No JDK/JADX runtime was available; no device or target code ran.

Target and artifact context

Two self-authored XML resource records and two equivalent ZIP/XML fixtures differing only in order. Both contain the root manifest plus a benign manifest-shaped asset. No private files, hostile inputs, signing manipulation or target application execution.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Actual reduced-probe stdout:

{"name":"asset-first","source_derived_selected_path":"assets/AndroidManifest.xml","package_name":"org.example.asset","version_name":"asset-9","manifest_status":"complete"}
{"name":"root-first-control","source_derived_selected_path":"AndroidManifest.xml","package_name":"org.example.actual","version_name":"actual-1","manifest_status":"complete"}

The source trace establishes the ordering and selection premises:

  1. REA requests resources: "full". The pinned ResourceMode.FULL branch keeps the resource list without filtering.
  2. The released JADX central-directory reader appends entries in directory order; ResourcesLoader appends both non-directory XML resources in that order. Its content loader returns both XML and MANIFEST resources as text; the XML parser explicitly accepts plain XML. No resources.arsc was present in the source fixture, so resource-table renaming does not change the premise.
  3. The pinned loadManifestText selects resources.firstOrNull by name suffix, then loads that one resource. The two upstream manifest tools consume this cached value.
  4. REA leaves those tools registered and normalizes their summary and XML without checking the selected archive member.

Primary evidence provenance: static source trace; observed REA normalization with a mirrored producer and observed synthetic ZIP/admission controls. Local verification: source/adapter probe reproduced, original JVM loader not run. Reproduction completeness: complete source/adapter counterexample; real APK/JVM reproduction is unavailable.

Typed outcome or recovery expected

Return the actual root package facts and root manifest for either archive ordering. A restoration check should keep the root-first control and show identical root package/manifest results for asset-first order, without hiding the asset from ordinary resource inventory. No fix or restored result is claimed.

Analysis or suspected cause

The producer uses a basename suffix as package-manifest identity even though the resource list contains unrelated XML files and the underlying decoder has an exact manifest resource type. REA forwards the result as package facts.

Additional context

The supplied 1,069-open-issue snapshot and open/closed GitHub searches for manifestText, AndroidManifest.xml assets, nested manifest, and manifest-first selection found no matching report. #6819 concerns namespace interpretation after manifest selection; this case uses conventional prefixes. #6460 concerns an inventory projection and encrypted root occurrence, not the JADX package inspector. No introducing commit or known-good version was established. Prepared with AI assistance.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The pinned source and executed source/adapter probe support a bounded substitution of unrelated asset metadata for root package facts, but the real JVM/APK pipeline was not executed.
  • Workaround status: available
  • Workaround basis: Exact ZIP-member reads of AndroidManifest.xml recover the actual root package from both synthetic archive-order fixtures; real Android decoder behavior remains outside the local verification.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6866 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior Source-only finding: `inspect_android_package` can use `assets/AndroidManifest.xml` as the package's manifest when that ordinary asset precedes the root `AndroidManifest.xml` in ZIP entry order. The pinned manifest loader chooses the first resource whose lowercased name ends in `androidmanifest.xml`, without requiring the root path or manifest resource type. Both `get_app_info` and `get_android_manifest` use that cached selection, and REA publishes it as the package summary and complete manifest. A source/adapter probe with an asset-first fixture normalized package `org.example.asset` and version `asset-9`; reversing only resource order returned the actual root package `org.example.actual` and version `actual-1`. This is not an executed Kotlin/JADX reproduction: resource selection was explicitly simulated from pinned source before invoking REA's real normalization. ### Reproduction The concrete source trigger is a standalone APK with this central-directory order and two readable XML resources: ```text 1. assets/AndroidManifest.xml -> package org.example.asset, version asset-9 2. AndroidManifest.xml -> package org.example.actual, version actual-1 ``` The asset is ordinary packaged data, not a second APK or application entry point. With a built checkout, save this exact executed source/adapter probe as `/tmp/rea-manifest-order.mjs`, then run `REA_ROOT="$PWD" node /tmp/rea-manifest-order.mjs` from the checkout: ```js import { pathToFileURL } from 'node:url'; const { analyzeJadxRequest } = await import(pathToFileURL(`${process.env.REA_ROOT}/dist/android/JadxAnalysis.js`)); const engine = { name: 'source-simulation', version: '0.7.1', artifact_sha256: 'a'.repeat(64), source_revision: null, worker_count: 1, heap_limit_mib: null }; const manifest = (pkg, version) => `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${pkg}" android:versionName="${version}" android:versionCode="7"><application android:hasCode="false"/></manifest>`; const entries = [ { path: 'assets/AndroidManifest.xml', xml: manifest('org.example.asset', 'asset-9') }, { path: 'AndroidManifest.xml', xml: manifest('org.example.actual', 'actual-1') }, ]; for (const [name, resources] of [['asset-first', entries], ['root-first-control', [...entries].reverse()]]) { // Mirror the pinned loadManifestText predicate, not an executed JVM loader. const selected = resources.find(({ path }) => path.toLowerCase().endsWith('androidmanifest.xml')); const xml = selected.xml; const app = { package: /<manifest\b[^>]*\bpackage="([^"]+)"/.exec(xml)?.[1] ?? '', version_name: /android:versionName="([^"]+)"/.exec(xml)?.[1] ?? '', version_code: '7', min_sdk: '', target_sdk: '', permissions: [] }; const result = await analyzeJadxRequest({ json: async () => app, text: async () => xml, listClasses: async () => { throw new Error('unused'); }, }, { operation: 'inspect_android_package', input: { path: '/fixture.apk' } }, { state: 'LOADED', apk_path: '/fixture.apk', class_count: 0, resource_count: 2, threads: 1, resources: 'full', }, engine); console.log(JSON.stringify({ name, source_derived_selected_path: selected.path, package_name: result.package_name, version_name: result.version_name, manifest_status: result.manifest.status })); } ``` The reduced probe ran once before drafting. An earlier probe created two self-authored ZIP/XML `.apk` fixtures in these orders, confirmed production `parseBinaryTarget` admitted both as `apk`, enumerated their real central-directory entry order, simulated the same selection, and observed the same normalized results. These synthetic ZIP/XML fixtures were not claimed to be installable Android applications or decoded by JADX. Exact-root-member reads returned `org.example.actual` from both. The reachable production requests are CLI `rea inspect-android-package <apk>` and MCP `inspect_android_package` with `{"path":"<apk>"}`. Their full JVM execution was not run. ### Expected behavior Package inspection should bind its summary and returned manifest to the root `AndroidManifest.xml`, independent of unrelated asset order. Android [defines the package manifest as the application manifest](https://developer.android.com/guide/topics/manifest/manifest-element), and REA [describes this operation as package declarations plus decoded manifest](https://github.com/morluto/rea/blob/4526862728af01f1135714079c0d8a8c754eb364/docs/android-analysis.md#interpret-the-result). JADX itself [classifies only exact `AndroidManifest.xml` as `MANIFEST`](https://github.com/skylot/jadx/blob/28ff15e4ae69950aebea110a13e5ab895d234dfc/jadx-core/src/main/java/jadx/api/ResourceType.java#L63-L78); the asset remains an XML resource. ### Impact and scope The bounded source counterexample substitutes an unrelated asset's package/version and manifest for the root package facts. The root-first control succeeds. This is ordinary static artifact selection correctness; it asserts no authenticity or signing behavior. Real APK prevalence and JVM behavior were not measured. Exact ZIP-root-member inspection recovers the actual manifest in both synthetic fixtures; inspecting only the returned simulated manifest would show the selected asset instead. ### REA version Source `4526862728af01f1135714079c0d8a8c754eb364` (`main`, package 6.7.0). Relevant `inspectPackage` normalization is byte-identical to latest published tag `rea-agents-6.7.0`, commit `27dc42d10249f0410904a75c0997842eb03b931c`; the release was source-compared, not separately executed. The [audited producer](https://github.com/morluto/rea/blob/4526862728af01f1135714079c0d8a8c754eb364/src/android/JadxRelease.ts#L2-L7) is jadx-headless-mcp 0.7.1 at `5844800a486d2248fa949b74c9896285f5b54de2`, with JADX 1.5.6. Relevant producer files match upstream release tag `v0.7.1`. ### Runtime and platform Executed adapter and ZIP/admission probes: Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64. No JDK/JADX runtime was available; no device or target code ran. ### Target and artifact context Two self-authored XML resource records and two equivalent ZIP/XML fixtures differing only in order. Both contain the root manifest plus a benign manifest-shaped asset. No private files, hostile inputs, signing manipulation or target application execution. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Actual reduced-probe stdout: ```jsonl {"name":"asset-first","source_derived_selected_path":"assets/AndroidManifest.xml","package_name":"org.example.asset","version_name":"asset-9","manifest_status":"complete"} {"name":"root-first-control","source_derived_selected_path":"AndroidManifest.xml","package_name":"org.example.actual","version_name":"actual-1","manifest_status":"complete"} ``` The source trace establishes the ordering and selection premises: 1. REA [requests `resources: "full"`](https://github.com/morluto/rea/blob/4526862728af01f1135714079c0d8a8c754eb364/src/android/JadxSession.ts#L133-L141). The pinned [`ResourceMode.FULL` branch](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/src/main/kotlin/com/atxx/jhmcp/JadxSession.kt#L76-L83) keeps the resource list without filtering. 2. The released JADX [central-directory reader](https://github.com/skylot/jadx/blob/28ff15e4ae69950aebea110a13e5ab895d234dfc/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java#L187-L195) appends entries in directory order; [`ResourcesLoader`](https://github.com/skylot/jadx/blob/28ff15e4ae69950aebea110a13e5ab895d234dfc/jadx-core/src/main/java/jadx/api/ResourcesLoader.java#L201-L229) appends both non-directory XML resources in that order. Its [content loader](https://github.com/skylot/jadx/blob/28ff15e4ae69950aebea110a13e5ab895d234dfc/jadx-core/src/main/java/jadx/api/ResourcesLoader.java#L133-L137) returns both XML and MANIFEST resources as text; the [XML parser explicitly accepts plain XML](https://github.com/skylot/jadx/blob/28ff15e4ae69950aebea110a13e5ab895d234dfc/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java#L68-L74). No resources.arsc was present in the source fixture, so resource-table renaming does not change the premise. 3. The pinned [`loadManifestText`](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/src/main/kotlin/com/atxx/jhmcp/JadxSession.kt#L599-L610) selects `resources.firstOrNull` by name suffix, then loads that one resource. The [two upstream manifest tools](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/src/main/kotlin/com/atxx/jhmcp/ManifestTools.kt#L12-L41) consume this cached value. 4. REA [leaves those tools registered](https://github.com/morluto/rea/blob/4526862728af01f1135714079c0d8a8c754eb364/bridge/android/ReaJadxBridge.java#L433-L445) and [normalizes their summary and XML without checking the selected archive member](https://github.com/morluto/rea/blob/4526862728af01f1135714079c0d8a8c754eb364/src/android/JadxAnalysis.ts#L112-L133). Primary evidence provenance: static source trace; observed REA normalization with a mirrored producer and observed synthetic ZIP/admission controls. Local verification: source/adapter probe reproduced, original JVM loader not run. Reproduction completeness: complete source/adapter counterexample; real APK/JVM reproduction is unavailable. ### Typed outcome or recovery expected Return the actual root package facts and root manifest for either archive ordering. A restoration check should keep the root-first control and show identical root package/manifest results for asset-first order, without hiding the asset from ordinary resource inventory. No fix or restored result is claimed. ### Analysis or suspected cause The producer uses a basename suffix as package-manifest identity even though the resource list contains unrelated XML files and the underlying decoder has an exact manifest resource type. REA forwards the result as package facts. ### Additional context The supplied 1,069-open-issue snapshot and open/closed GitHub searches for `manifestText`, `AndroidManifest.xml assets`, nested manifest, and manifest-first selection found no matching report. #6819 concerns namespace interpretation after manifest selection; this case uses conventional prefixes. #6460 concerns an inventory projection and encrypted root occurrence, not the JADX package inspector. No introducing commit or known-good version was established. Prepared with AI assistance. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The pinned source and executed source/adapter probe support a bounded substitution of unrelated asset metadata for root package facts, but the real JVM/APK pipeline was not executed. - Workaround status: available - Workaround basis: Exact ZIP-member reads of AndroidManifest.xml recover the actual root package from both synthetic archive-order fixtures; real Android decoder behavior remains outside the local verification.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4204
No description provided.