Files
rea/docs/android-analysis.md
T
gensokyo-dev 570abe95f2 feat(android): link native declarations to static JNI candidates (#7472)
Link original DEX native declarations to per-ABI function exports and relocated static JNI table candidates through shared CLI/MCP workflows. Preserve global attribution ambiguity, exact provenance, partial coverage and retained Evidence; default MCP delivery to bounded summaries with filtered/batch pages.

Use one immutable ELF relocation decoder with streaming IL2CPP materialization, cooperative bounded export/version parsing and bounded JNI state. Reuse validated session inventories without loading target libraries or executing JNI code.

Validation: relevant type/lint/build and parser/domain/public CLI/MCP regressions; genuine four-ABI NDK fixtures and independent library/slot oracles; actual JDK/JADX Conscrypt workflows with retained-source removal. Full measurements and limitations are recorded in docs/verification/android-jni-bindings-2026-10-27.md.

Closes #6403.
2026-10-27 18:53:22 +08:00

42 KiB

Static Android APK analysis

REA wraps an existing headless JADX engine through src/android/. Its source is kept as an unmodified, commit-pinned Git submodule; see upstream provenance. CLI and MCP use the same application workflow and return artifact-bound Evidence inline.

This family is present on repository main and in npm 4.1.0. Check the released package boundary and the connected server's tool list before selecting it.

On Linux, JADX launches an REA-owned process and requires a procps-compatible ps on REA's PATH; see the shared Linux process prerequisite.

Optional package delivery

The MCP inspect_android_package request keeps complete output when detail is omitted or "complete". Select detail: "summary" explicitly to receive all package, SDK and permission declarations, engine identity and class/resource counts without decoded manifest text or duplicate raw provider replies. A retaining session keeps the complete original Evidence and returns a distinct linked inspect_analysis_view summary. This changes delivery, not JADX analysis.

Use the returned normalized_result.parent_evidence_id with inspect_analysis_view, source kind retained-evidence, and view: {kind: "android", facet: "manifest", offset: 0, limit: 4096} to read the exact decoded manifest without reopening the APK. Offsets and limits count UTF-16 code units; follow coverage.next_offset. The page preserves source_status and reported_total_bytes: exhausting retained text does not make a partial decoded manifest complete. Complete source Evidence remains exportable under the existing session lifetime. Servers without retention reject summary before starting JADX. One-shot CLI output remains complete by default. Select --detail summary with --evidence_output /local/capture.json to retain complete Evidence before returning a linked view. Older servers require the existing complete request after schema discovery. APK authenticity and runtime behavior remain unverified.

Manifest component projection

inspect_android_package also returns a static inventory of declared activities, aliases, services, receivers and providers from the decoded manifest. declared_name preserves the source attribute; name resolves a relative name only when the manifest package is known and the resolved copy fits the UTF-8 byte budget. An unresolved package or exhausted name budget retains the declared name and marks the inventory partial. Empty component names and names with forbidden DEX characters retain their raw spelling and are not treated as identities. Valid DEX spaces, including boundary spaces, are preserved when resolving component names and matching alias targets. Whitespace in the manifest package is rejected by its separate package grammar. Component permissions inherit the application permission when no component permission is declared, except an activity alias with no permission: Android documents that case as requiring no permission through the alias. permission_source distinguishes a local declaration, inherited value, no requirement (including an explicit empty override), and a value omitted because its resource/manifest-placeholder token is unresolved or the total permission-byte budget was reached. Resource and placeholder references are not expanded at this parser boundary. Repeated inherited values share one inventory-wide byte budget. Provider android:readPermission and android:writePermission overrides are not projected as separate fields; their presence marks the inventory partial and is called out in limitations. Relative component names are not resolved against a manifest package with surrounding whitespace. Class-name segments follow DEX SimpleName characters, including a leading digit or hyphen, rather than Java source identifier rules. Relative-name expansion does not prove class existence, DEX-version compatibility or runtime loadability. SDK defaults accept JADX's decimal and hexadecimal integer spellings; the original SDK declaration strings remain unchanged.

exported is the declared literal true or false, otherwise null. exported_default is the documented default only where it can be established; it remains null for unknown provider target SDKs, nonliteral declarations, or components with filters targeting API 31 or later that omit the now-required explicit declaration. A limitation records the latter validation error. Provider authorities and all inventory dimensions are bounded. status: "partial" and limitations identify omitted declarations or values; a capacity limitation appears only when an actual collection limit was reached. The projection does not merge manifests, expand resource references, or observe runtime enabled/export state.

enabled and grant_uri_permissions report only literal component-level android:enabled and provider-level android:grantUriPermissions declarations. They are null when absent or not a literal boolean; missing attributes are not filled from application defaults. A nonliteral declaration gets a limitation. process preserves the exact component-level android:process spelling (including empty or symbolic values), or null when absent or omitted under the inventory-wide 1 MiB copy budget. Neither field resolves resources, merges manifest values or establishes the effective runtime process or enabled state. The exact boolean attributes are retained in enabled_declaration and grant_uri_permissions_declaration, including resource references.

intent_filters preserves each filter's source ordinal, exact priority spelling, actions, categories and individual data declarations (scheme, host, port, path, path prefix/pattern/advanced pattern/suffix and MIME type). These are declarations, not computed Android matching rules. meta_data lists names; values remain in the complete decoded manifest. Component, filter, data and metadata location values use one-based lines/columns and UTF-16 offsets into that manifest. A component's declaration_index preserves its original declaration ordinal, including gaps.

declaration_coverage distinguishes complete, partial and unknown observations. Historical Evidence omits newly introduced fields and defaults coverage to unknown; omission never establishes absence. Current records contain the fields, with null for absent or unresolved scalar declarations. The inventory retains at most 32 filters and 64 nested action/category/data/metadata records per component, and shares a 1 MiB structured-value copy budget across components. Omitted values or unsupported filter constructs mark coverage partial and include a limitation; complete retained manifest text remains accessible.

For a small answer, select detail: "summary" with an explicit view:

{
  "kind": "android",
  "facet": "components",
  "actions": ["android.provider.Telephony.SMS_RECEIVED"],
  "fields": ["name", "permission", "enabled", "process", "intent_filters"],
  "offset": 0,
  "limit": 16
}

The same JSON works with CLI --view or retained inspect_analysis_view. kinds, names and actions use exact matching; names accept either the recorded declared or resolved name, actions match any requested action, and different filters combine with AND. exported matches only explicit declarations, keeping exported_default separate. Empty selectors match nothing. Every selected row retains its parent-array source_index and declaration_coverage; source coverage, parent identity and limitations remain alongside the page. Follow coverage.next_offset. The 1 MiB projection budget can shorten a page; a single oversized row requires fewer selected fields. Exhausting a partial source does not establish complete manifest coverage. facet: "component-summary" applies the same filters and counts kinds, declaration coverage and explicit/default exported values separately. Both adapters use the same retained projection; repeated pages do not reload JADX.

The service foreground_service_type field preserves the declared attribute, including an empty value or surrounding spaces; null means the attribute is absent or the component is not a service. This field does not validate service type tokens or establish runtime eligibility.

Supply tools

Use a full JDK 17 or newer (including its compiler module) and jadx-headless-mcp 0.7.1. Verify the downloaded JAR's SHA-256 against REA's pinned release metadata before using it. The following download and verification commands work for npm package consumers as well as repository checkouts; the repository-only npm run fixtures:android helper is not included in the npm package.

On macOS or Linux, choose a destination and verify the pinned digest:

mkdir -p "$HOME/.local/share/rea/android" &&
cd "$HOME/.local/share/rea/android" &&
curl -fL -o jadx-headless-mcp-0.7.1-all.jar https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar &&
if command -v sha256sum >/dev/null; then
  echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75  jadx-headless-mcp-0.7.1-all.jar' | sha256sum -c -
else
  echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75  jadx-headless-mcp-0.7.1-all.jar' | shasum -a 256 -c -
fi &&
export REA_JADX_MCP_JAR="$PWD/jadx-headless-mcp-0.7.1-all.jar"

On Windows, run the equivalent in PowerShell:

$jar = Join-Path $HOME '.local\share\rea\android\jadx-headless-mcp-0.7.1-all.jar'
New-Item -ItemType Directory -Force (Split-Path $jar) | Out-Null
Invoke-WebRequest -Uri 'https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar' -OutFile $jar
if ((Get-FileHash -Algorithm SHA256 $jar).Hash.ToLowerInvariant() -ne '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75') { throw 'JADX JAR SHA-256 does not match REA pinned metadata' }
$env:REA_JADX_MCP_JAR = $jar

REA runs its packaged Java metadata bridge in source-file mode against the unmodified engine JAR. REA does not install Java, an Android SDK, JADX, a device service or an emulator. Set the path in the shell or MCP server environment:

export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
# Optional: select an existing JDK rather than java on PATH.
export JAVA_HOME=/absolute/path/existing-jdk

On Windows x64, use a REA package with its matching bundled Windows native controls including owned protocol stdin support. This Windows boundary is new on repository main; an older released package can still reject this host. PowerShell accepts paths containing spaces without additional escaping inside these environment values:

$env:REA_JADX_MCP_JAR = 'D:\tools\jadx-headless-mcp-0.7.1-all.jar'
$env:JAVA_HOME = 'D:\tools\jdk-21'
rea inspect-android-package 'D:\targets\Example.apk'

REA selects JAVA_HOME\bin\java.exe, or resolves java.exe on the selected Windows PATH. A missing or mismatched native artifact reports the package prerequisite rather than attempting an unowned Java launch. Windows arm64 is outside this provider's current native boundary.

Readiness checks the selected JAR's ZIP directory for the classes consumed by REA's metadata bridge, as well as Java's compiler and version modules. It does not load an APK or execute engine code. Invalid archives and missing bridge classes report JAR-specific recovery; classpath inventory alone does not prove that every class or engine operation will load successfully. Discovery limits each ZIP metadata read to 8 MiB before allocation; a larger directory reports the exhausted read budget and the selected JAR path.

The filtered class-page bridge (protocol 2) is verified on Linux x86-64 with Temurin 21, public Appium ApiDemos 6.0.18 and NewPipe 0.29.1 APKs. Earlier metadata-bridge records covered macOS arm64/OpenJDK 21 and Windows x64/JDK 21; this protocol revision has not been rerun on those hosts or Linux/JDK 25. Windows x64 uses an owned Job Object with bidirectional stdio, atomic job assignment and kill-on-owner-close. Real Windows verification uses JDK 21 and the pinned JADX 0.7.1 JAR; see the real verification lane below.

Answer an analyst question

rea inspect-android-package /path/Example.apk
rea search-android-classes /path/Example.apk MainActivity
rea inspect-android-class /path/Example.apk example.MainActivity
rea inspect-android-method /path/Example.apk example.MainActivity onCreate
rea inspect-android-method /path/Example.apk example.MainActivity select --overload-index 1
rea trace-android-references /path/Example.apk example.MainActivity
rea trace-android-references /path/Example.apk example.MainActivity --method-name onCreate
rea trace-android-references /path/Example.apk example.MainActivity --method-name select --overload-index 1

Equivalent MCP methods are inspect_android_package, search_android_classes, inspect_android_class, inspect_android_method and trace_android_references. Every request includes path. Class/method operations use class_name, method_name and optional overload_index as applicable. For example:

{
  "name": "inspect_android_method",
  "arguments": {
    "path": "/path/Example.apk",
    "class_name": "example.MainActivity",
    "method_name": "select",
    "overload_index": 1
  }
}

Use inspect_android_class to choose an overload. REA rejects an ambiguous method request rather than silently selecting the first match. Method reference queries accept the same overload_index; the bridge resolves the selected method node and the result reports its index and displayed signature. These indices are tied to the current artifact and engine, not stable across builds.

inspect_android_class also reports dex_static_value for each static final field whose class definition encodes a value in its DEX static values, so compile-time constants are readable even when the class has no <clinit>. Each value is typed: boolean, byte, short, char (UTF-16 code unit) and int values are JSON booleans or numbers, while long, float and double values are strings holding Java's text for the value, so 64-bit values stay exact. The field reports null when no value is encoded, the encoded value is null or of another kind, or the field is not static final. The value is read from parsed metadata without generating source, and a static initializer can still assign the field; use inspect_android_method on <clinit> for that evidence. Only inspect_android_class asks the bridge for these values. inspect_android_method and trace_android_references read the member-only class summary, so large encoded strings cannot push their member lookup past the 8 MiB protocol frame limit.

Smali fallbacks select one complete block using the original method name and DEX descriptor, before applying the source byte limit. This preserves the selected overload even when Java code generation renames a method. If the selected Java body was omitted, including a method merged into a synthetic bridge, REA returns its original smali with the java_body_unavailable marker. The marker describes missing Java; it does not infer why the method was omitted or name a replacement. Unresolved or duplicate disassembly identities fail instead of attributing another method's instructions to the selection.

Native and abstract methods are identified from access flags captured before code generation. They report body_status: "not_available", an empty source, and the native_method or abstract_method marker; their declarations are not passed through the engine's source slicer. Other missing decompiled bodies do not establish either access flag.

Static initializers (<clinit>) return smali with the static_initializer_disassembly marker. Java decompilation can move their instructions into field declarations, leaving an empty or incomplete method body. The selected method's disassembly preserves those instructions and retains the same source byte limit and explicit truncation reporting.

Apply an R8/ProGuard mapping

Release builds shrunk by R8 or ProGuard rename classes and members. Select the build's mapping.txt with mapping_path on inspect_android_class or inspect_android_method, or --mapping-path on the matching CLI command:

rea inspect-android-class /path/Example.apk a.b --mapping-path /path/mapping.txt \
  --detail summary --evidence_output /tmp/class-evidence.json

For agent workflows, select detail: "summary" on class and method inspection. It preserves the complete normalized member inventory or selected method body, including mapping annotations, while retaining duplicate raw provider calls in the complete parent Evidence. The reply links that parent's identity and digest; export it with export_evidence_bundle or read its summary again through inspect_analysis_view. CLI summaries require --evidence_output and return the portable bundle path. Omitted or complete detail preserves complete inline Evidence. Search and reference operations retain their existing contracts. Symbol summary views admit at most 1 MiB, including view context. An admitted method's source can already consume that amount; a larger combined summary fails explicitly instead of silently truncating facts. MCP refusal includes the retained parent ID. Select complete detail, or inspect one method rather than a whole class, when the requested summary does not fit.

The mapping path must be absolute. Selectors still take the observed names, such as a.b, and observed rows are unchanged. The result adds one mapping block with the file's path, SHA-256 and size, the header values the file reports (compiler, compiler_version, min_api, map_version, pg_map_id, pg_map_hash), valid record counts and up to 16 malformed line samples. Its symbols list has one entry per observed class, inner class, method and field; inspect_android_method lists the class and the selected method. coverage counts each resolution status. With summary delivery, these unchanged facts are under the view's summary field; the view separately reports source coverage.

Lookups use the bridge's raw DEX names, not JADX display aliases such as a renamed reserved word. Each symbol resolves as:

  • mapped: exactly one record matches. A class needs one record for its raw name (verified_by: "class"). A method must also match its DEX prototype ("descriptor") and a field its DEX type ("type"). R8 residual signature metadata is used when present; otherwise original types are translated through the file's class records.
  • ambiguous: multiple-records, descriptor-mismatch, type-mismatch, ambiguous-class or residual-signature-conflict, with candidate_count and up to 8 candidates. Conflicting residual metadata never selects a winner. A name match whose prototype or type does not verify is never reported as mapped.
  • unmapped: no valid record matched the raw identity.
  • unknown with reason projection-budget: this claim did not fit the combined reply budget. The observed raw symbol remains present; coverage.unknown counts these omissions. Inspect that specific class or method separately, or consult the selected local mapping file for complete source records.

A line continues an inline frame stack when it repeats the previous line's minified range and obfuscated name and gives a single call-site line, such as :7. Only the outermost original of a stack becomes a candidate. Original line numbers and source files are not projected. Malformed records are counted and skipped, so an unmapped symbol might be described by one of them. A file that is missing, not a regular file, not UTF-8, larger than the byte limit or without any class record, such as usage.txt or seeds.txt, is refused before JADX is used. Requests read their mappings one at a time, in the same queue as the JADX work and under the same operation deadline. A mapping failure therefore never starts or retires the JADX session, and a different mapping reuses the loaded APK.

When advertised, inspect_android_jni_bindings links original ACC_NATIVE declarations from standard root classes*.dex members to bundled ELF libraries. The supplied JAR's DEX reader preserves duplicate declarations before decompiler deduplication. This operation does not load the APK into the decompiler. MCP defaults to retained summary delivery when detail is omitted; select complete explicitly to receive the full report inline.

{
  "path": "/absolute/Example.apk",
  "class_name": "org.conscrypt.NativeCrypto",
  "method_names": ["EVP_PKEY_new_RSA"],
  "abis": ["arm64-v8a"],
  "detail": "summary"
}

Class/method names and optional descriptors select raw DEX identities. library_paths selects exact logical APK paths such as lib/arm64-v8a/libconscrypt_jni.so; each library's source_path preserves its original ZIP spelling. Omitted filters include all scoped facts; [] matches none. Class selection never narrows the global inventory used for table-class attribution.

Independent declarations, libraries, candidates and bindings collections have stable IDs and source ordinals. Candidates retain APK/library digests, ABI, function addresses and file offsets. Table candidates also retain section, table and entry identity plus all three name/signature/function pointer slots and their relocation sources. JNI exports follow the UTF-16 escaping and short-before-long rules; failed escaping does not acquire an invented export match. Tables require NUL-terminated Modified UTF-8 strings, valid descriptors and executable load mappings. REL, RELA and RELR retain separate provenance; unsupported packed relocations and unresolved pointer spans stay explicit unknowns.

A static JNINativeMethod entry names no class. Attribute it uniquely only when exactly one declaration in the complete recorded native inventory has that (name, descriptor) pair. Multiple classes remain ambiguous after a class filter; a partial inventory cannot establish unique attribution.

Each selected declaration has a row per selected/observed ABI. matched means candidate presence was observed; ambiguous means multiple observed targets or class associations remain. Binding coverage is separate: a matched partial row does not establish complete uniqueness, and an empty partial scan is unknown, never unmatched. ELF/ABI label mismatches preserve facts without asserting a binding. No result establishes library loading, runtime registration or method invocation.

Summary returns the first 64 selected binding rows, their declaration/candidate closure and every selected library's identity, coverage and failure reason; counts and page coverage describe the full selected binding set. Selected declaration IDs have separate preview coverage, so native declarations remain visible even when the APK has no bundled ABI libraries. Complete Evidence stays retained. Read exact fields or further pages without reopening files or rerunning analysis:

{
  "source": { "kind": "retained-evidence", "evidence_id": "ev_<digest>" },
  "view": {
    "kind": "android-jni",
    "collection": "bindings",
    "offset": 64,
    "limit": 64,
    "fields": ["declaration_id", "abi", "status", "coverage", "candidate_ids"]
  }
}

Collections also include declarations, candidates and libraries, with exact filters and batch lookup by declaration/candidate/library IDs. row_index is the original parent collection index; DEX and ZIP source ordinals stay separate. Complete detail returns the full report. CLI uses the same selection and delivery contracts. Its common one-shot default remains complete; summary delivery requires an explicit Evidence path:

rea inspect-android-jni-bindings /absolute/Example.apk \
  --query '{"class_name":"org.conscrypt.NativeCrypto","method_names":["EVP_PKEY_new_RSA"],"abis":["arm64-v8a"]}' \
  --detail summary --evidence_output /absolute/fresh-jni-evidence.json

Optional --view accepts the same JSON page and requires summary delivery. Evidence bundles never overwrite files. Complete declarations and one library selection are cached within the immutable APK session. Changing ABI/library selection replaces the scan; class/method selection reuses it. Fresh analysis still validates current caller APK bytes. Idle/close releases the caches; retained Evidence remains historical and independently readable.

Interpret the result

  • Package inspection retains decoded manifest XML, summary declarations, permissions and class/resource counts. REA's bridge selects the exact root AndroidManifest.xml resource by original archive name and manifest type, independently of asset order. One namespace-aware JDK XML parse supplies summary attributes and direct permission/SDK declarations; namespace prefixes and XML entity spelling do not change their values. Raw decoded XML is retained exactly and only its displayed text is subject to the existing source byte cap. Summaries use the complete selected XML before that cap. Document types are unsupported; parsing does not load external DTDs, schemas, or XInclude files. Signature verification is explicitly not_performed.
  • Class search filters the engine's cached top-level names before transferring pages, then follows contiguous inventory positions without rescanning earlier rows. It checks selection, scope, examined ranges and counts before reporting complete coverage. Complete producer replies stay in Evidence; an empty query inventories all class-name rows; class names are decompiler representations. Repeated names remain repeated in matches so the inventory row count and producer evidence stay visible. duplicate_names identifies matching names with multiple rows and their occurrence counts; a name alone does not distinguish which provider class node a later lookup selects.
  • Class inventories read parsed metadata without generating source and return full reported display types, fields and inner class names. Synthetic members that code generation hides can appear in the inventory; it stays consistent after method decompilation within the retained session. Class and method inspection and reference tracing accept either the exact reported class name or its original binary name, including $ in nested and anonymous classes. The bridge keeps both identities from JADX; it does not replace separators or guess suffix matches. Ambiguous selectors are refused. Follow-up calls preserve the selected name, while results report the frozen JADX display name. Quote $ names in the shell, for example 'com.example.Manager$WorkerThread'. Exact DEX prototypes are reported from frozen JADX method metadata as dex_descriptor, for example (I)V. Display names can be JADX aliases; the descriptor alone is not a complete raw DEX method ID. Overload indices are tied to this artifact and engine, not stable identities across builds.
  • A mapping block appears only when the caller selected mapping_path. Its names are claims from that file, not observations of the APK. REA does not check that the mapping belongs to this build; pg_map_id is reported, not compared. Evidence keeps the selected path in its parameters and the file's SHA-256 in the result.
  • Method inspection reports Java or smali, fallback markers and complete/partial returned text. Decompiled source is a derived representation. A complete text result does not establish complete semantics or a byte-identical reconstruction.
  • Reference results are incoming static relationships between provider nodes. reference_unit=using_node means total counts containing JADX nodes, not individual invoke instructions; two calls from one method can appear as one entry. coverage=complete covers this node inventory only. For a method reference whose using node has raw code, direct_invokes lists matching DEX invoke instructions with their DEX entry, raw source method ID, code-unit offset, method index and opcode. An empty array means no direct invoke was matched in that node; null means raw code was unavailable or the using node was not a method. This list does not establish complete call-site coverage outside JADX's using-node inventory or through reflection and dynamic loading. A virtual or interface invoke also does not establish its runtime implementation. Source lines and the containing node's own dex_offset remain unknown. Method-reference results also carry the selected dex_descriptor; class-reference results report null. These relationships are not observed runtime calls.
  • Original requests, APK SHA-256, actual engine JAR SHA-256 and raw producer responses remain in Evidence. Source revision is populated only for bytes matching the audited release JAR.

If a later step fails, the error's details.partial_observation retains the selected and resolved APK paths, digest, observed provider identity and versions, and each upstream request and reply collected before the failure. Failed replies remain available even when their payload cannot be interpreted. Cancellation also stops target hashing before provider admission.

These operations do not execute the APK or provide split APK/AAB handling, signature validation, full resource-table semantics, native-library analysis or Android runtime capture. An .aab suffix is a ZIP archive, not an APK, so Android tools and the application graph do not treat it as a standalone APK. Existing artifact inventory/extraction tools can supply archive evidence. Inventory records an APK root only for ZIP bytes whose name ends in .apk. The projection adds a limitation when no root AndroidManifest.xml was observed, because such a ZIP may not be an APK. Nested members take a ZIP or ASAR family from their bytes; an unread member keeps an unknown format. Inventory keeps ZIP names that differ only in case and records that a case-insensitive destination cannot store both spellings. Extraction fails only when that destination directory cannot keep both names. Project that Evidence with the execution-free inventory tool:

rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
{
  "name": "project_android_application_graph",
  "arguments": {
    "inventory_evidence": ["<inventory_artifact Evidence>"]
  }
}

The projection reports exact component paths and hashes, runtime-family hints, and path-based bridge hypotheses. File and slice occurrences supply component endpoints; directory trees and symlinks retain their identities in the source inventory without being classified as code files. It does not decode DEX or claim observed runtime calls. DEX/JVM runtime hints summarize the classified bytecode component lists. Framework hints match complete native library names, such as libreactnativejni.so, libreactnative.so, libhermes.so, libhermes_executor.so, libflutter.so, and libunity.so; unrelated names such as libcommunity.so and libreactivity.so do not identify those families. The signing list retains the META-INF/MANIFEST.MF, .SF, .RSA, .DSA, and .EC paths selected by Android's v1 verifier, including nested paths. These are inventory metadata candidates, not proof that a v1 signature verifies or that the APK is signed; use inspect-apk-signature for signature analysis. The bridge basis is inferred from the native path and repeated for each managed component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library Cartesian product. The projection computes the product and exact serialized candidate-array size before creating candidate objects, then retains a deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted, bridge_candidate_coverage reports total, emitted, and omitted counts; overall coverage is partial while component arrays still include every component from the supplied inventory pages. A path pair does not establish that a managed declaration names, loads, or calls the native library.

Resource and lifecycle limits

REA retains one serialized session with private, immutable APK, engine JAR and metadata-bridge copies. Matching requests reuse the loaded engine. APK/engine digests and JVM configuration are checked before reuse; changed inputs retire the previous session. Sessions are cleaned after 60 seconds of inactivity, on target/configuration changes, failure, timeout, cancellation, MCP disconnect or server shutdown. One-shot CLI commands always join cleanup before returning. REA never writes to the original APK or launches target code.

JNI scans reuse the analyzer process without loading the APK into the decompiler. They admit at most 64 MiB per DEX member and 256 MiB of DEX bytes, with a 4 MiB native-declaration JSON allowance before MCP string escaping. Native libraries are processed sequentially: one 64 MiB buffer, 256 MiB total selected read work, and a 16 MiB parsed-library-facts allowance. ZIP inventory and library counts also have explicit resource bounds. Unexamined occurrences remain unknown. A complete relationship report has a separate 16 MiB serialized allowance; summaries/pages allow 1 MiB including their provenance envelope. These are resource admissions, not claims about maximum valid Android formats. Select fewer ABI/library occurrences or smaller retained pages when needed. Completed parsed library facts survive failed reader finalization in typed partial observations; the exact reader owner remains available for cleanup retry.

On Windows, stdin writes preserve stream backpressure and run outside the Node event loop. Cancellation terminates the retained job, including descendants, before workspace cleanup. On Windows, a detached REA helper owns the private workspace's native directory handle. An abrupt CLI/server owner termination closes the Java job and the helper's input pipe; the helper then removes the workspace by handle. The real Windows lane checks both effects. If the helper also dies or the host loses power before it finishes, private files can remain.

If Java cannot start the bridge, REA reports the selected executable and retained startup diagnostics. Select a full JDK through JAVA_HOME or PATH, and use java --list-modules to confirm jdk.compiler is present. A runnable JRE alone cannot compile the bridge. Missing or ambiguous class and method selections are input failures; discover names with search_android_classes and inspect_android_class before correcting the selected argument.

If process cleanup cannot be confirmed, REA retains the workspace and reports its location. That provider instance blocks subsequent engine launches until the caller resolves the reported resources and starts a fresh instance.

The provider uses one JADX analysis worker independently of JVM processor and garbage-collector settings. REA supplies no default heap or visible-CPU override; Java uses its normal ergonomics and inherits caller JVM settings. Optional provider settings add explicit JVM arguments:

export REA_JADX_HEAP_MIB=8192
# Optional, independent of the single JADX worker:
export REA_JADX_ACTIVE_PROCESSOR_COUNT=4
# Or select settings through Java's standard environment options:
export JAVA_TOOL_OPTIONS=-Xmx8g

Both REA settings must be positive safe integers; processor counts must also fit the JVM's signed 32-bit range. REA preserves JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS and _JAVA_OPTIONS rather than replacing them. Java's standard precedence applies: explicit command-line heap settings override JAVA_TOOL_OPTIONS, while _JAVA_OPTIONS can override command-line settings. Evidence reports the worker's observed Runtime.maxMemory() rounded down to MiB and retains exact bytes, Java version and visible processors in its raw runtime response. The observed allocatable heap can differ from configured -Xmx, especially with Serial GC. Heap is not a total RSS cap: the JVM also uses native memory. A typed JVM memory allocation failure during APK loading reports resource_constraint with the observed maximum heap in bytes. Known heap failures name REA_JADX_HEAP_MIB in remediation; other memory regions remain unknown. A healthy doctor configuration check does not establish that any particular APK fits. Failures before the bridge can report a typed result and upstream errors from other operations retain their existing diagnostics; error text alone does not establish memory exhaustion.

Independent REA instances have independent queues. Whole operations have a 120-second execution deadline by default, starting after they reach the queue's front. For a known slow cold load, such as a large multidex APK, select another finite deadline:

export REA_JADX_OPERATION_TIMEOUT_MS=600000

The value must be a decimal millisecond count from 1 to 2147483647; anything else is refused before Java starts. REA reads it from the environment captured at process start. Changing that environment requires a new REA process. On a running MCP server, set jadx.operation_timeout_ms through update_analysis_settings to change the default for newly accepted operations; queued or running operations retain their accepted budget. The same deadline bounds each upstream call. Timeout recovery reports the effective milliseconds and range, with details.limit identifying the runtime setting. An invalid provider-local launch value can also be repaired by a runtime override; reset exposes that invalid launch value again. See the settings design. A passing JADX configuration check in rea doctor reports its launch-derived operation_timeout_ms; it does not inspect another running server's overrides. Explicit cancellation still ends the operation immediately, and partial observations and owned-process cleanup are unchanged. Raising the deadline does not extend the bridge's 30-second startup handshake or the upstream 90-second decompilation limit. MCP clients enforce their own tool-call timeouts, and setup's derived client tool timeout does not include this setting, so also raise the client timeout when selecting a longer deadline.

Decoded manifest and method text have a 1 MiB upstream byte budget and report truncation. Class-name pages use indexed slices of up to 4,096 names without changing the complete, case-sensitive search contract. A protocol frame above 8 MiB, or combined stdout/stderr above 32 MiB per operation, fails with a diagnostic instead of returning an incomplete inventory as complete. These are memory-safety boundaries, not undocumented result caps.

A selected R8/ProGuard mapping is re-read and SHA-256 checked on every call, up to 64 MiB. The serialized provider keeps one successful parsed index, keyed by byte length and digest with fixed parser options. Identical bytes reuse that index; changed bytes are reparsed, missing files still fail, and each response preserves the currently selected path. The index is released on provider idle retirement or close. Omitting mapping_path never applies the cached mapping. During replacement, the old index can coexist with the new parse until it succeeds; these transient allocations count toward process memory.

Before retaining records, the parser admits an estimated 128 MiB index representation and 8 MiB of expanded mapped claims per residual class, including shared header facts. The index allowance accounts for strings and containers; it is not a measured V8 heap cap. Parameter and metadata expansion is admitted before splitting or JSON decoding. These fixed resource bounds have no runtime override; refusal reports resource_constraint, the source line and mapping SHA-256. Ambiguous lookups count all candidates while keeping only the first eight in stable file order. A separate 8 MiB combined claim budget applies to the actual query set, including outer and inner classes, before cloning the report. Claims that do not fit become explicit unknown entries; other admitted claims and all observed identities remain available.

Real verification

Download the explicit public fixtures once; this installs no tools:

npm run fixtures:android
export REA_JADX_MCP_JAR="$PWD/_reference/apk-integration/jadx-headless-mcp-0.7.1-all.jar"
export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
npm run verify:android

The main lane and Windows Java-selection checks validate the selected JDK and engine archive through validateVerifierJadxConfiguration. This shared verifier boundary supplies the production validator's resource owner and waits for retained cleanup before proceeding. Failed closes retain their exact owners for retry; prerequisite failures must not be mistaken for completed bridge tests. Recovery checks match the typed Java or archive failure and its applicable remediation.

The lane also uses the selected full JDK's jlink to create a disposable JRE without the compiler module. It checks real Java startup failures, missing selectors, recovery after failures, and CLI cancellation with owned cleanup. The original APK and host Java configuration remain unchanged.

The same lane runs on Windows x64 with the bundled native controls and a full JDK. It checks CLI/MCP results, real MCP cancellation and disconnect, and Java exit and workspace removal after abrupt CLI owner termination. Windows forced termination is checked separately from the POSIX CLI SIGTERM handler. A synthetic two-owner test confirms that one owner's exit does not remove the other owner's workspace. The lane does not claim a structured cancellation result or power-loss recovery.

The download script verifies fixed SHA-256 values, reuses matching files and refuses to overwrite a different existing file. APK/JAR files live under ignored _reference/ and are excluded from package contents. The lane exercises actual REA CLI and MCP results with the real engine, separately from synthetic protocol regressions. It needs no Gradle build, emulator, Android SDK or Ghidra.

Direct invoke sites retain exact DEX code-unit offsets separately from each containing node's unknown dex_offset. direct_invokes_exhaustive: false means instruction rows were bounded by the local bridge frame budget; null means the producer did not establish instruction coverage. Using-node totals and coverage do not imply complete instruction evidence.