[GH-ISSUE #6863] [Bug] Android package inspection returns XML entity spelling as the version name #4201

Closed
opened 2026-10-31 00:47:40 +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/6863

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-derived producer finding with an executed simulated-boundary reproduction: inspect_android_package publishes version_name: "1 & beta" for a decoded manifest whose attribute value is 1 & beta. A literal 1 & beta becomes 1 & beta. The actual REA adapter forwards the pinned producer's captured XML substring without decoding entities.

The producer was simulated using its verbatim published regex; Kotlin, JADX, and a binary-XML APK were not executed. The real REA adapter and XML parser ran locally. The retained XML remains complete and permits recovery of the correct attribute value.

Reproduction

From the source checkout with locked dependencies, save this actually executed reduced probe as /tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts:

const root = process.env.REA_ROOT!;
const { analyzeJadxRequest } = await import(`${root}/src/android/JadxAnalysis.ts`);
const { parseXmlDocument } = await import(`${root}/src/domain/xmlDocument.ts`);
const engine = {name:'jadx',version:'0.7.1',artifact_sha256:'a'.repeat(64),source_revision:null,worker_count:1,heap_limit_mib:null};
const loaded = {state:'LOADED',apk_path:'/inert.apk',class_count:0,resource_count:1,threads:1,resources:'full'};
for (const encoded of ['1.0', '1 & beta', '1 & beta']) {
  const xml = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.example.fixture" android:versionName="${encoded}" android:versionCode="7"/>`;
  const parsed = parseXmlDocument(xml, {doctype:'reject', lineEndings:'preserve'});
  if (!parsed.ok) throw parsed.error;
  const expected = parsed.value.documentElement.getAttributeNS('http://schemas.android.com/apk/res/android','versionName');
  // Verbatim VERSION_NAME_RE from pinned ManifestParsing.kt, evaluated in JS.
  const app = {package:'org.example.fixture',version_name:/android:versionName="([^"]+)"/u.exec(xml)![1],version_code:'7',min_sdk:'',target_sdk:'',permissions:[]};
  const result:any = await analyzeJadxRequest({json:async()=>app,text:async()=>xml,listClasses:async()=>{throw Error('unused')}},
    {operation:'inspect_android_package',input:{path:'/inert.apk'}}, loaded, engine);
  console.log(JSON.stringify({expected,actual:result.version_name,manifest_status:result.manifest.status,xml_retained:result.manifest.text===xml}));
}

Run with the pinned Node.js toolchain:

REA_ROOT="$PWD" node_modules/.bin/tsx /tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts

The reduced probe ran once before this draft. An earlier five-case probe extracted the producer expressions directly from its pinned Kotlin source and also demonstrated preserved entity spelling for quotes and apostrophes. Neither probe executes that Kotlin implementation; this limitation is part of the reproduction, not an end-to-end claim.

Expected behavior

The structured version_name should report the decoded manifest attribute value: 1 & beta for android:versionName="1 &amp; beta". For android:versionName="1 &amp;amp; beta", the value is the literal text 1 &amp; beta, so decoding must preserve that distinction. The executed REA XML parser establishes these expected values independently of the simulated producer.

The package-inspection contract calls these summary declarations and retains the decoded XML for comparison. The source XML spelling should remain in the manifest, while the structured declaration should preserve its value.

Impact and scope

The boundary result contains incorrect structured version metadata for entity-bearing version names, while the plain control remains correct. The source-derived reach includes the pinned manifest summary parser; actual APK frequency and full binary-XML/JVM behavior remain unverified. No execution, signature, security, or authenticity conclusion is involved.

A workaround is available at the reproduced boundary: parse the retained manifest XML and read the Android namespace's versionName attribute. The probe does this with REA's actual XML parser and retains the complete original XML for every case.

REA version

6.7.0; immutable source revision 96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9. The audited producer is jadx-headless-mcp 0.7.1 at 5844800a486d2248fa949b74c9896285f5b54de2, declared by JadxRelease.ts.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; locked tsx 4.23.15. No Kotlin/JVM, Android SDK/device, native provider, or APK execution was performed. JADX versions below describe inspected source, not an executed engine.

Target and artifact context

Benign authored XML manifests with the conventional android namespace prefix and three versionName spellings. The provider response is simulated from the pinned upstream expression. There is no real APK in this reduced probe, and no private target data.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The reduced adapter-boundary probe exited 0 and printed:

{"expected":"1.0","actual":"1.0","manifest_status":"complete","xml_retained":true}
{"expected":"1 & beta","actual":"1 &amp; beta","manifest_status":"complete","xml_retained":true}
{"expected":"1 &amp; beta","actual":"1 &amp;amp; beta","manifest_status":"complete","xml_retained":true}
  • Reduced probe/output: /tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts and probe-manifest-entities.out.
  • Earlier source-extracted five-case probe/output: /tmp/agent-work.0TqT4J/android-manifest/manifest-entities.mts and manifest-entities.stdout.jsonl; its stderr was empty.
  • Evidence provenance: source-derived producer behavior; observed REA projection from a simulated producer plus an independently parsed XML attribute value.
  • Local verification: plain control and entity-bearing fixtures exercised the actual REA adapter and XML parser.
  • Reproduction completeness: complete for the demonstrated simulated boundary; actual Kotlin parser execution and a full real-APK/JADX workflow remain NOT_ENOUGH_INFO.

The inspected source chain is:

  1. The pinned producer uses JADX 1.5.6.
  2. Its decoder escapes decoded attribute values, with ampersand/quote/apostrophe entities.
  3. ManifestParsing.kt captures the XML substring and publishes it directly as version_name.
  4. REA registers that upstream manifest tool and forwards app.version_name.

Typed outcome or recovery expected

Preserve the semantic version-name attribute value alongside the exact decoded XML, or explicitly identify an uninterpreted declaration instead of publishing XML serialization as the value. The plain control and literal-entity distinction should remain intact.

Analysis or suspected cause

Source-derived: the pinned producer parses escaped XML with a regex and returns the captured substring, while REA accepts the summary unchanged. The adapter-boundary run confirms REA's forwarding, not the binary decoder or Kotlin runtime. The inspected decoder source makes the escaping chain plausible; a full binary-XML reproduction was not run.

Additional context

Explicit open and closed searches covered JADX escape, manifest entity, ManifestParsing, "version_name" "amp", and version-name titles; the supplied open-title ledger was also checked. Receipts are retained in /tmp/agent-work.0TqT4J/android-manifest/manifest-entity-duplicate-searches.json.

Related reports were excluded:

  • #6819 concerns alternate Android namespace prefixes losing entire summary fields. This fixture uses the conventional prefix, retains the fields, and corrupts their values through entity spelling.
  • #6804 concerns Apktool 3's YAML dialect and producer escapes. This path uses JADX's XML manifest summary and never invokes the Apktool parser.
  • #3368 concerns ADB numeric version-code precision, not XML string decoding.

This research, fixtures, and draft were produced with AI assistance. No human review, actual Kotlin/JADX run, real APK reproduction, or known-good release is represented.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The executed REA adapter boundary returns entity spelling instead of the independently parsed version-name value, and pinned producer/decoder source supports the same chain. Full Kotlin/JADX and binary-XML execution remains unverified, so end-to-end reach and frequency are provisional.
  • Workaround status: available
  • Workaround basis: The executed boundary retains the complete XML, and REA's XML parser recovers the correct namespace-qualified version-name value for each demonstrated input, including literal entity text.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6863 ### 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-derived producer finding with an executed simulated-boundary reproduction: `inspect_android_package` publishes `version_name: "1 &amp; beta"` for a decoded manifest whose attribute value is `1 & beta`. A literal `1 &amp; beta` becomes `1 &amp;amp; beta`. The actual REA adapter forwards the pinned producer's captured XML substring without decoding entities. The producer was simulated using its verbatim published regex; Kotlin, JADX, and a binary-XML APK were not executed. The real REA adapter and XML parser ran locally. The retained XML remains complete and permits recovery of the correct attribute value. ### Reproduction From the source checkout with locked dependencies, save this actually executed reduced probe as `/tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts`: ```ts const root = process.env.REA_ROOT!; const { analyzeJadxRequest } = await import(`${root}/src/android/JadxAnalysis.ts`); const { parseXmlDocument } = await import(`${root}/src/domain/xmlDocument.ts`); const engine = {name:'jadx',version:'0.7.1',artifact_sha256:'a'.repeat(64),source_revision:null,worker_count:1,heap_limit_mib:null}; const loaded = {state:'LOADED',apk_path:'/inert.apk',class_count:0,resource_count:1,threads:1,resources:'full'}; for (const encoded of ['1.0', '1 &amp; beta', '1 &amp;amp; beta']) { const xml = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.example.fixture" android:versionName="${encoded}" android:versionCode="7"/>`; const parsed = parseXmlDocument(xml, {doctype:'reject', lineEndings:'preserve'}); if (!parsed.ok) throw parsed.error; const expected = parsed.value.documentElement.getAttributeNS('http://schemas.android.com/apk/res/android','versionName'); // Verbatim VERSION_NAME_RE from pinned ManifestParsing.kt, evaluated in JS. const app = {package:'org.example.fixture',version_name:/android:versionName="([^"]+)"/u.exec(xml)![1],version_code:'7',min_sdk:'',target_sdk:'',permissions:[]}; const result:any = await analyzeJadxRequest({json:async()=>app,text:async()=>xml,listClasses:async()=>{throw Error('unused')}}, {operation:'inspect_android_package',input:{path:'/inert.apk'}}, loaded, engine); console.log(JSON.stringify({expected,actual:result.version_name,manifest_status:result.manifest.status,xml_retained:result.manifest.text===xml})); } ``` Run with the pinned Node.js toolchain: ```sh REA_ROOT="$PWD" node_modules/.bin/tsx /tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts ``` The reduced probe ran once before this draft. An earlier five-case probe extracted the producer expressions directly from its pinned Kotlin source and also demonstrated preserved entity spelling for quotes and apostrophes. Neither probe executes that Kotlin implementation; this limitation is part of the reproduction, not an end-to-end claim. ### Expected behavior The structured `version_name` should report the decoded manifest attribute value: `1 & beta` for `android:versionName="1 &amp; beta"`. For `android:versionName="1 &amp;amp; beta"`, the value is the literal text `1 &amp; beta`, so decoding must preserve that distinction. The executed REA XML parser establishes these expected values independently of the simulated producer. [The package-inspection contract](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/docs/android-analysis.md#L173-L176) calls these summary declarations and retains the decoded XML for comparison. The source XML spelling should remain in the manifest, while the structured declaration should preserve its value. ### Impact and scope The boundary result contains incorrect structured version metadata for entity-bearing version names, while the plain control remains correct. The source-derived reach includes the pinned manifest summary parser; actual APK frequency and full binary-XML/JVM behavior remain unverified. No execution, signature, security, or authenticity conclusion is involved. A workaround is available at the reproduced boundary: parse the retained manifest XML and read the Android namespace's `versionName` attribute. The probe does this with REA's actual XML parser and retains the complete original XML for every case. ### REA version 6.7.0; immutable source revision `96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9`. The audited producer is jadx-headless-mcp 0.7.1 at `5844800a486d2248fa949b74c9896285f5b54de2`, declared by [JadxRelease.ts](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/android/JadxRelease.ts#L2-L6). ### Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; locked tsx 4.23.15. No Kotlin/JVM, Android SDK/device, native provider, or APK execution was performed. JADX versions below describe inspected source, not an executed engine. ### Target and artifact context Benign authored XML manifests with the conventional `android` namespace prefix and three `versionName` spellings. The provider response is simulated from the pinned upstream expression. There is no real APK in this reduced probe, and no private target data. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The reduced adapter-boundary probe exited 0 and printed: ```json {"expected":"1.0","actual":"1.0","manifest_status":"complete","xml_retained":true} {"expected":"1 & beta","actual":"1 &amp; beta","manifest_status":"complete","xml_retained":true} {"expected":"1 &amp; beta","actual":"1 &amp;amp; beta","manifest_status":"complete","xml_retained":true} ``` - Reduced probe/output: `/tmp/agent-work.0TqT4J/android/probe-manifest-entities.mts` and `probe-manifest-entities.out`. - Earlier source-extracted five-case probe/output: `/tmp/agent-work.0TqT4J/android-manifest/manifest-entities.mts` and `manifest-entities.stdout.jsonl`; its stderr was empty. - Evidence provenance: source-derived producer behavior; observed REA projection from a simulated producer plus an independently parsed XML attribute value. - Local verification: plain control and entity-bearing fixtures exercised the actual REA adapter and XML parser. - Reproduction completeness: complete for the demonstrated simulated boundary; actual Kotlin parser execution and a full real-APK/JADX workflow remain NOT_ENOUGH_INFO. The inspected source chain is: 1. The pinned producer [uses JADX 1.5.6](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/build.gradle.kts#L15). 2. Its decoder [escapes decoded attribute values](https://github.com/skylot/jadx/blob/4c0ac37699aa8c9803f1c73cfaacd9205acb044b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java#L431-L444), with [ampersand/quote/apostrophe entities](https://github.com/skylot/jadx/blob/4c0ac37699aa8c9803f1c73cfaacd9205acb044b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java#L215-L229). 3. [ManifestParsing.kt](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/src/main/kotlin/com/atxx/jhmcp/ManifestParsing.kt#L16-L24) captures the XML substring and publishes it directly as `version_name`. 4. REA [registers that upstream manifest tool](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/bridge/android/ReaJadxBridge.java#L433-L445) and [forwards `app.version_name`](https://github.com/morluto/rea/blob/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9/src/android/JadxAnalysis.ts#L117-L129). ### Typed outcome or recovery expected Preserve the semantic version-name attribute value alongside the exact decoded XML, or explicitly identify an uninterpreted declaration instead of publishing XML serialization as the value. The plain control and literal-entity distinction should remain intact. ### Analysis or suspected cause Source-derived: the pinned producer parses escaped XML with a regex and returns the captured substring, while REA accepts the summary unchanged. The adapter-boundary run confirms REA's forwarding, not the binary decoder or Kotlin runtime. The inspected decoder source makes the escaping chain plausible; a full binary-XML reproduction was not run. ### Additional context Explicit open and closed searches covered `JADX escape`, `manifest entity`, `ManifestParsing`, `"version_name" "amp"`, and version-name titles; the supplied open-title ledger was also checked. Receipts are retained in `/tmp/agent-work.0TqT4J/android-manifest/manifest-entity-duplicate-searches.json`. Related reports were excluded: - [#6819](https://github.com/morluto/rea/issues/6819) concerns alternate Android namespace prefixes losing entire summary fields. This fixture uses the conventional prefix, retains the fields, and corrupts their values through entity spelling. - [#6804](https://github.com/morluto/rea/issues/6804) concerns Apktool 3's YAML dialect and producer escapes. This path uses JADX's XML manifest summary and never invokes the Apktool parser. - [#3368](https://github.com/morluto/rea/issues/3368) concerns ADB numeric version-code precision, not XML string decoding. This research, fixtures, and draft were produced with AI assistance. No human review, actual Kotlin/JADX run, real APK reproduction, or known-good release is represented. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The executed REA adapter boundary returns entity spelling instead of the independently parsed version-name value, and pinned producer/decoder source supports the same chain. Full Kotlin/JADX and binary-XML execution remains unverified, so end-to-end reach and frequency are provisional. - Workaround status: available - Workaround basis: The executed boundary retains the complete XML, and REA's XML parser recovers the correct namespace-qualified version-name value for each demonstrated input, including literal entity text.
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#4201
No description provided.