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.
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-classorresidual-signature-conflict, withcandidate_countand 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.unknownwith reasonprojection-budget: this claim did not fit the combined reply budget. The observed raw symbol remains present;coverage.unknowncounts 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.
Link native declarations to JNI candidates
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.xmlresource 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 explicitlynot_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
queryinventories all class-name rows; class names are decompiler representations. Repeated names remain repeated inmatchesso the inventory row count and producer evidence stay visible.duplicate_namesidentifies 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 asdex_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
mappingblock appears only when the caller selectedmapping_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_idis 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_nodemeanstotalcounts containing JADX nodes, not individual invoke instructions; two calls from one method can appear as one entry.coverage=completecovers this node inventory only. For a method reference whose using node has raw code,direct_invokeslists 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;nullmeans 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 owndex_offsetremain unknown. Method-reference results also carry the selecteddex_descriptor; class-reference results reportnull. 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.