Skip to content

fix(pool): resolve object pool references in the real index space, add r2flutter backend - #85

Open
caverav wants to merge 12 commits into
mainfrom
feat/pool-index-space-and-r2flutter-backend
Open

fix(pool): resolve object pool references in the real index space, add r2flutter backend#85
caverav wants to merge 12 commits into
mainfrom
feat/pool-index-space-and-r2flutter-backend

Conversation

@caverav

@caverav caverav commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

I went looking at r2flutter to see whether it was worth borrowing from, and found a bug in our pool handling instead. Both are in here.

The bug first, because it matters more: we were printing string literals into pseudocode that the binary never referenced.

ldr xN, [x27, #disp] was annotated pool[disp], i.e. the raw byte displacement used as if it were an entry index. It isn't. The entry is (disp - entries_offset) / word_size. That number then gets joined against object_pool[].index to look up a value, so the lookup landed on an unrelated slot and rendered whatever string lived there.

Concretely, from a Dart 3.9.2 libapp.so:

0x652bd4: ldr x1, [x27, #0xef8]
  we printed:  "_workoutWorkoutDeserialize" /* pool[3832] */
  actual slot: 477, holding a type_arguments object, not a string at all

The page form was wrong in a nastier way. add x0, x27, #0x23, lsl #12 + ldr x0, [x0, #0xa90] was reconstructed textually as displacement / 8, which is the right scale but skips entries_offset, so it lands exactly two slots early:

  we printed:  "...WebChromeClient.onShowFileChooser"   (slot 18258)
  actual:      "...WebChromeClient.onProgressChanged"   (slot 18256)

Off by two, still a real selector from the same class. Nothing in the output tells you which one you are looking at. For a tool whose pitch is "readable enough to make RE decisions from", that is the worst failure mode available, worse than printing nothing.

The test suite never caught it because every decompiler test does pool.insert(42, ...) against a literal pool[42]: synthetic on both sides, so the index space is never exercised.

While measuring this I also found the page form is 13903 of the 19604 pool loads in the sampled functions, and the disassembler was not annotating it at all. So we were getting about 29% of the pool, wrong.

What is in here

Pool index space (commits 1-4). pool_geometry is now part of the adapter contract, and emitting it is how an adapter asserts its indices are real ObjectPool indices. The disassembler converts displacements properly and tracks add xD, x27, #K, lsl #S page bases so both load forms resolve. Bases drop on control flow and on any write to the register, including both destinations of load-pair forms, so a stale base cannot invent a slot. Without geometry the annotation is poolOff[<displacement>], which is honest about what we know and is inert by construction: it cannot match a hint.

Then the important half: hints are only built when geometry is present. The internal adapter numbers its pool by string carve order, so it produces no pool literals rather than plausible wrong ones, and report.json.pool_metadata.hints_suppressed_reason says why.

More literals, all verified (commits 5-6). Suppressing wrong literals is only half the job; the other half is not losing right ones. Only call arguments were ever run through the pool hint, so slots we could resolve still printed bare:

t2.f19 = pool[40];
if ((obj4.f31) == pool[893]) {
return pool[5122];

Resolving when a register is read as a whole value covers assignments, comparisons and returns from one place. Rendered literals go from 15 to 24 across 80 functions, and known-value slots left unresolved drop from 12 to 3. Dereferences deliberately keep the slot, since pool[40].f7 reads a field of the pooled object and rendering the string there would claim a field access on a literal.

Doing that exposed a second bug worth its own commit. The expression cleanups scan raw text, so they treat string contents as code. A real engine string reads "... has been collected (nullptr). This is usually ...", and (nullptr). looks like a parenthesised member access, so the simplifier edited the parentheses out of a string that came from the binary. Both member-access simplifiers now skip string literals.

Artifact filenames (commit 7). decompile dies with File name too long (os error 36) the moment an adapter returns real names. Longest name in the sample is 305 bytes, NAME_MAX is 255. Unreachable today only because everything is sub_<addr>, which is also why nobody hit it.

Dart profiles (commits 8-9). We read the snapshot hash and then report dart_version: unknown, which is silly, because the hash is an MD5 over VM serializer sources and pins the SDK release exactly. The mapping cannot be computed, it has to be tabulated by building every SDK, so I vendored that table from r2flutter as data (61 hashes, 19 profiles, 11 KB, MIT). info now reports dart_version and dart_tag_style with no adapter installed at all.

Resolution is by floor, matching how the table is built: hashes are filed under exact versions (3.6.2), profiles under the release that last changed the layout (3.6.0). Unknown hashes stay unknown, since a guessed profile is indistinguishable from a real one downstream.

r2flutter backend (commits 10-11). --adapter-backend r2-flutter, at the existing adapter process boundary. No new Rust dependency, no radare2 at our build time. auto becomes r2flutter, blutter, internal, each falling through when its tooling is absent.

internal r2flutter
functions 7,458 37,258
exact names 0 37,258
classes 1 8,986
pool index space authoritative no yes
pool value hints 0 11,814

Before and after on the same function:

// internal
dynamic sub_652b98(...) { return tailCall_0xe136dc(); }

// r2flutter
dynamic method_SharedPreferences_getInstance(...) {
  final t3 = stub_Allocate_Future_5048458Stub(...);
  final t5 = method_SharedPreferences__getSharedPreferencesMap_967028285(objTmp1, t4, ...);
  final t8 = method__AsyncCompleter_5048458_complete(resultTmp1, resultTmp2, ...);

Worth saying plainly: the readability passes were always fine, they had just never been given real input.

Validation

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace, 286 passed, 0 failed
  • ./scripts/lint-shell.sh

Each of the 12 commits builds and passes the suite on its own.

Real-binary check on the Dart 3.9.2 libapp.so, all backend paths driven end to end (internal; auto falling back with r2flutter absent; auto selecting it when present; explicit r2-flutter and the r2flutter alias; a missing binary failing with a usable message instead of a traceback).

The check I actually care about, every pool literal the new pipeline emits cross-checked against an independent ObjectPool decoder:

matched: 24   mismatched: 0

That harness is how the string-corruption bug was caught: it was 23/24 until the literal-safety fix. The specific confabulation is gone too, pp+0xef8 now resolves to pool[477], a type_arguments object, so no string is printed. New regression tests use instruction encodings lifted from the real binary with indices verified the same way, so they would catch a reintroduction rather than agreeing with it.

Both adapter payloads now validate against schemas/adapter.schema.json, which was never true before.

CodeRabbit review

Seven findings. Five were real and are fixed:

  • subprocess timeout. Real. Six r2flutter invocations per model build, each loading the whole binary, none bounded. Now FLUTTERDEC_R2FLUTTER_TIMEOUT, default 900s.
  • ldp leaves a stale pool base. Real, and the same class of bug this PR exists to fix. invalidate_first_operand cleared one destination, so ldp x0, x1, [sp, #16] left x1 holding a page base it no longer had, and a later load would fabricate a slot from it. Fixed and given a regression test.
  • "pool_geometry": null fails the schema. Real, and bigger than reported. Validating the payload showed target_va, selector and owner_class have always been emitted as explicit nulls too, so the schema has never matched real adapter output on any branch. Nulls are now stripped on the way out.
  • dart_version missing from plain info. Real, only --json showed it. Now printed in both, and documented.
  • --adapter-backend r2flutter rejected. Real papercut: clap derives r2-flutter while the docs, env vars and report.json all say r2flutter. Both spellings accepted now.

Two were not:

  • libs[0] IndexError when no library URIs are carved. _collect_libraries falls back to ["package:app/main.dart"] before returning, so it cannot return empty and the index is always safe.
  • r2flutter lives at trufae/r2flutter, and ./configure will break. radareorg/r2flutter is not a fork and has no parent per the GitHub API, and the documented ./configure && make is exactly how I built the binary used throughout this PR.

Its nitpick about the dead second return value from _r2flutter_classes was fair and is applied. The remaining suggestion, pinning or regression-testing the r2flutter JSON contract, is a real gap; I have listed it as a follow-up rather than stubbing something shallow now.

Follow-ups, not in this PR

  • The README showcase assets need regenerating. docs/assets/readme/zedsecure-* shows ldr x3, [x27, #0x658] ; pool[1624] = "minWidth". Under the correct formula 0x658 is slot 201, so that mapping came from the old index space and is very likely wrong. I do not have the ZedSecure APK to re-render them. Flagged in context.md; we should not cite them as evidence until they are redone.
  • No regression coverage pinning the r2flutter JSON shapes the adapter parses. Captured fixtures plus a parser test would stop auto silently mis-parsing if upstream output changes.
  • r2flutter does not attribute classes to libraries, so app_package_counts_top is empty on that backend and --function-scope app cannot discriminate. Libraries come from -jzz (512 recovered) but are not joined to classes.
  • Its ObjectPool reconstruction only succeeds on modern compressed-pointer snapshots. On the 2.18.2 and iOS samples it returns PP not resolved, so geometry is omitted and hints stay off. It degrades correctly, but the backend is not uniformly better everywhere.
  • Pool slots holding code refs rather than strings are not ingested. -jxz only gives strings; slot 4941 for instance holds a method reference we could turn into a call target name.

Note on #74

No conflict, I checked before starting. flutterdec-dart-serwalker touches only its own crate plus one workspace-member line, zero file overlap with this.

They aim at the same place from different positions: serwalker is a future in-tree deserializer (currently 1 of about 55 clusters, nothing depends on it, no tests), this is an external backend that works today. Both meet the same ProgramModel contract, so --adapter-backend native can slot in later next to r2-flutter without disturbing anything here.

Two things in this PR help that work rather than compete with it. It now has a ground-truth oracle to diff against on the same binary. And data/dart-profiles.json covers a gap it has structurally: its ClassId is a #[repr(u32)] enum, which can only ever be correct for one profile, and it currently matches the 3.6/3.9-era table, so on any Dart 3.3 or older snapshot try_from returns the wrong variant or errors. Same for tag decoding, where DECODE_CID! is hardcoded to the OBJECT_HEADER form and there are three encodings in the wild. That constraint is written down in docs/research-decisions.md now.

Scope

  • Atomic commits (type(scope): description)
  • Docs updated (README.md, docs/*, context.md)
  • No unrelated refactors mixed in

One thing that looks like a drive-by but is not: backend_label was a second match arm listing every backend next to AdapterBackend::as_str. Adding a variant meant updating both, and forgetting one silently mislabels the backend in report.json. Folded into the existing one in the same commit that adds the variant.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds Dart snapshot hash profile resolution and exposes Dart version and tag-style metadata. It introduces the r2flutter adapter backend with CLI selection, subprocess integration, normalized model conversion, and fallback behavior. Program models now optionally carry pool geometry, enabling ARM64 pool-load resolution into authoritative indices or displacement-based annotations when unavailable. Core reports geometry and suppresses pool hints without it. Filename normalization is capped, schemas are updated, and documentation and tests cover the new behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: real ObjectPool index resolution plus the new r2flutter backend.
Description check ✅ Passed The description includes Summary, Validation, and Scope sections with the required key details and testing notes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
adapters/python/adapter_template.py (1)

823-843: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the unused second return value.

_r2flutter_classes always returns [] as the library table and the only caller discards it (Line 869). A plain List[dict] return is clearer than a Tuple whose second slot is dead by construction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapters/python/adapter_template.py` around lines 823 - 843, Update
_r2flutter_classes to return only the class list as List[dict], remove the
unused Tuple typing and empty library-table return, and adjust its caller to
consume the single result while preserving the existing class projection
behavior.
crates/flutterdec-core/src/lib.rs (1)

208-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New info metadata is only visible with --json.

handle_info in crates/flutterdec-cli/src/main.rs (Lines 262-314) prints each optional field individually in the human-readable branch; dart_version / dart_tag_style are not printed there, so plain flutterdec info silently drops the new metadata. Worth adding two if let Some(..) prints (and listing the fields in the info JSON field list in docs/cli-reference.md).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flutterdec-core/src/lib.rs` around lines 208 - 212, Update handle_info
in the human-readable output branch to print dart_version and dart_tag_style
when present, matching the existing optional metadata formatting; also add both
fields to the documented info JSON field list in docs/cli-reference.md.
crates/flutterdec-cli/src/main.rs (1)

168-168: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Accept r2flutter as a CLI alias for --adapter-backend.

AdapterBackendArg derives r2-flutter as the clap value name, while AdapterBackend::to_core("r2flutter") emits r2flutter, so typing the backend’s canonical token on the CLI currently fails until parsed and passed downstream.

♻️ Suggested alias
-    R2Flutter,
+    #[value(alias = "r2flutter")]
+    R2Flutter,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flutterdec-cli/src/main.rs` at line 168, Update the AdapterBackendArg
definition for R2Flutter to accept r2flutter as a clap value alias, while
preserving the existing r2-flutter value and downstream AdapterBackend::to_core
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@adapters/python/adapter_template.py`:
- Around line 878-883: Update the fallback class construction in the surrounding
adapter-generation flow so it does not index libs when _collect_libraries
returns an empty list. Preserve the Global class fallback, using the existing
library value when available and a safe empty/default value when no library URIs
were recovered.
- Around line 744-775: Validate and pin the r2flutter CLI/output contract used
by _build_r2flutter_model, including the -jH, -ji, -jxz, -jc, -jzz, and -jp
responses and their expected fields/types. Add regression coverage or document a
required r2flutter output version so changes to entries[].address, xref records,
object_pool.entry references, entries_offset, or word_size are detected and auto
mode cannot silently mis-parse them.
- Around line 714-737: Update the subprocess.run call in _r2flutter_json to
enforce a finite timeout for each r2flutter invocation, using the adapter’s
established timeout configuration or a clearly defined constant. Catch
subprocess.TimeoutExpired and raise a RuntimeError with the action flag and
timeout context, preserving the existing launch, nonzero-exit, and JSON parsing
error handling.

In `@crates/flutterdec-disasm-arm64/src/lib.rs`:
- Around line 167-175: Update PoolRefResolver::invalidate_first_operand to
invalidate every destination register written by multi-register instructions
such as ldp and ldpsw, not only the first captured operand; preserve harmless
behavior for stp-style forms and ensure subsequent uses cannot retain stale
entries in self.bases.

In `@docs/development.md`:
- Around line 14-21: Update the r2flutter setup instructions in the development
documentation to clone the repository from trufae/r2flutter and use its
supported make user-install build/install flow. Ensure FLUTTERDEC_R2FLUTTER_BIN
points to the resulting installed or built bin/r2flutter location, removing the
obsolete configure and incorrect repository path.

In `@docs/how-it-works.md`:
- Around line 378-395: The validation documentation still says Rust requires
schema version 2, contradicting the documented v2/v3 support and the schema
version 3 output. Update the “Validation in Rust enforces” bullet to state that
schema versions 2 and 3 are accepted, preserving the surrounding validation
requirements.

In `@schemas/adapter.schema.json`:
- Around line 93-101: The adapter payload must omit pool_geometry when geometry
probing fails or fallback entries are carved/object-pool values without real
ObjectPool indices; do not emit it as None. Update the pool_geometry
construction in adapters/python/adapter_template.py to conditionally exclude the
field, while preserving the schema requirements for valid geometry objects in
schemas/adapter.schema.json.

---

Nitpick comments:
In `@adapters/python/adapter_template.py`:
- Around line 823-843: Update _r2flutter_classes to return only the class list
as List[dict], remove the unused Tuple typing and empty library-table return,
and adjust its caller to consume the single result while preserving the existing
class projection behavior.

In `@crates/flutterdec-cli/src/main.rs`:
- Line 168: Update the AdapterBackendArg definition for R2Flutter to accept
r2flutter as a clap value alias, while preserving the existing r2-flutter value
and downstream AdapterBackend::to_core behavior.

In `@crates/flutterdec-core/src/lib.rs`:
- Around line 208-212: Update handle_info in the human-readable output branch to
print dart_version and dart_tag_style when present, matching the existing
optional metadata formatting; also add both fields to the documented info JSON
field list in docs/cli-reference.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b3bb4ce3-f654-429a-873e-c64790dd724f

📥 Commits

Reviewing files that changed from the base of the PR and between 7bf248c and ffd2b4d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • README.md
  • adapters/python/adapter_template.py
  • context.md
  • crates/flutterdec-adapter/src/lib.rs
  • crates/flutterdec-cli/src/main.rs
  • crates/flutterdec-core/src/lib.rs
  • crates/flutterdec-core/src/pipeline/apk_startup.rs
  • crates/flutterdec-core/src/pipeline/helpers.rs
  • crates/flutterdec-core/src/pipeline/runners.rs
  • crates/flutterdec-core/src/pipeline/runners/tests.rs
  • crates/flutterdec-decompiler/src/helpers/expr.rs
  • crates/flutterdec-decompiler/src/passes/expr_cleanup.rs
  • crates/flutterdec-decompiler/src/tests/emit_and_helpers/readability_and_naming.rs
  • crates/flutterdec-disasm-arm64/src/lib.rs
  • crates/flutterdec-ir/src/lib.rs
  • crates/flutterdec-loader/Cargo.toml
  • crates/flutterdec-loader/src/dart_profile.rs
  • crates/flutterdec-loader/src/lib.rs
  • data/dart-profiles.json
  • docs/cli-reference.md
  • docs/development.md
  • docs/how-it-works.md
  • docs/research-decisions.md
  • docs/user-guide.md
  • schemas/adapter.schema.json

Comment thread adapters/python/adapter_template.py
Comment thread adapters/python/adapter_template.py
Comment on lines +878 to +883
libs = _collect_libraries(
[s.get("value", "") for s in all_strings if isinstance(s.get("value"), str)]
)
libraries = [{"id": i, "uri": lib, "name_display": lib} for i, lib in enumerate(libs)]
if not classes:
classes = [{"id": 0, "name": "Global", "super": "Object", "lib": libs[0]}]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

libs[0] raises IndexError when no library URIs are carved.

_collect_libraries can legitimately return an empty list (obfuscated/URI-stripped snapshots, or the -jzz fallback to pool_strings). Line 883 then indexes libs[0] and the whole r2flutter backend dies — silently downgraded in auto, exit 1 under --adapter-backend r2-flutter, even though functions and pool were recovered fine.

🐛 Proposed fix
     if not classes:
-        classes = [{"id": 0, "name": "Global", "super": "Object", "lib": libs[0]}]
+        classes = [
+            {"id": 0, "name": "Global", "super": "Object", "lib": libs[0] if libs else ""}
+        ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
libs = _collect_libraries(
[s.get("value", "") for s in all_strings if isinstance(s.get("value"), str)]
)
libraries = [{"id": i, "uri": lib, "name_display": lib} for i, lib in enumerate(libs)]
if not classes:
classes = [{"id": 0, "name": "Global", "super": "Object", "lib": libs[0]}]
libs = _collect_libraries(
[s.get("value", "") for s in all_strings if isinstance(s.get("value"), str)]
)
libraries = [{"id": i, "uri": lib, "name_display": lib} for i, lib in enumerate(libs)]
if not classes:
classes = [{"id": 0, "name": "Global", "super": "Object", "lib": libs[0] if libs else ""}]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapters/python/adapter_template.py` around lines 878 - 883, Update the
fallback class construction in the surrounding adapter-generation flow so it
does not index libs when _collect_libraries returns an empty list. Preserve the
Global class fallback, using the existing library value when available and a
safe empty/default value when no library URIs were recovered.

Comment thread crates/flutterdec-disasm-arm64/src/lib.rs Outdated
Comment thread docs/development.md
Comment on lines +14 to +21
The r2flutter backend is not bundled. Build it once against radare2 and point
`FLUTTERDEC_R2FLUTTER_BIN` at the result:

```bash
git clone https://github.com/radareorg/r2flutter && cd r2flutter
./configure --prefix="$HOME/.local" && make
export FLUTTERDEC_R2FLUTTER_BIN="$PWD/bin/r2flutter"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

r2flutter radareorg install r2pm build instructions binary

💡 Result:

r2flutter is an external tool for analyzing Dart/Flutter AOT snapshots, maintained in its own repository [1]. It is not a standard plugin that is installed via the radare2 package manager (r2pm), but rather a standalone project that you build from source [1]. To install and build r2flutter, follow these steps: 1. Clone the repository: git clone https://github.com/trufae/r2flutter cd r2flutter 2. Build the project: You must have radare2 installed, as the build process requires radare2 development headers [1]. Use the following command: make 3. Install the plugin for use within radare2: To integrate r2flutter directly into your radare2 shell, run: make user-install Once installed via make user-install, the r2flutter command becomes available within your radare2 sessions [1]. Note that while r2pm is the standard package manager for radare2 plugins [2][3], r2flutter relies on its own Makefile system for compilation and installation [1]. Ensure that radare2 is correctly installed on your system—typically via its own repository source or r2env—before attempting to build r2flutter, as the latter depends on r2 development headers [1][4].

Citations:


🌐 Web query:

GitHub r2flutter Makefile configure bin r2flutter

💡 Result:

r2flutter is a specialized plugin and command-line tool designed for the reverse engineering of Flutter and Dart AOT (Ahead-of-Time) snapshots on Android and iOS applications [1][2]. It is maintained under the radare2 ecosystem [3][4]. Regarding your query about the components: 1. bin/r2flutter: This is the main executable for the tool [1]. When invoked from the command line, it allows you to analyze Dart snapshots, extract class information, recover code references, and print header information [1]. It processes inputs such as libapp.so (Android) or App.framework (iOS) [1]. 2. Makefile: The r2flutter repository uses a Makefile to manage its build and installation process [1]. Notably, the repository includes a make user-install command, which installs the core plugin functionality into your local radare2 environment as the r2flutter command [1]. 3. configure: While the radare2 framework itself utilizes a./configure script as part of its standard build process (typically followed by make) [5][6][4], r2flutter is a plugin that integrates with an existing radare2 installation. You generally do not need to run a configure script specifically for the plugin; rather, you install it as a plugin so that it can interface with the radare2 environment [1]. To use r2flutter, you should ensure you have a functional installation of radare2 [4]. You can then install the plugin (often via the provided Makefile commands) to enable its features within the radare2 framework [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "doc snippet:"
if [ -f docs/development.md ]; then
  sed -n '1,40p' docs/development.md
else
  echo "docs/development.md not found"
fi
echo
echo "Search for r2flutter mentions:"
rg -n "r2flutter|FLUTTERDEC_R2FLUTTER_BIN" . || true

Repository: caverav/flutterdec

Length of output: 10164


🌐 Web query:

"radareorg/r2flutter" "Makefile" "bin/r2flutter" "configure"

💡 Result:

The tool r2flutter, located in the radareorg/r2flutter repository, is a specialized utility for Dart and Flutter AOT snapshot analysis on Android and iOS platforms [1][2]. Regarding your specific inquiry: 1. Makefile and bin/r2flutter: The project includes a Makefile that facilitates installation. Running make user-install will install the core plugin command as r2flutter [1]. The binary bin/r2flutter acts as the main command-line interface for analyzing Dart snapshots (e.g., libapp.so or App framework files) [1]. 2. configure: There is no standard./configure script typically associated with the r2flutter repository itself for building. Unlike the main radare2 framework, which uses an ACR-based./configure script [3][4], r2flutter is a plugin/utility that does not follow the same complex build configuration process. If you are looking for build instructions, standard make commands are generally used as defined in the project's Makefile [1]. Please note that r2flutter is distinct from Cloudflare R2, which is an S3-compatible object storage service [5]. Confusion often arises due to the shared naming, but they are unrelated technologies [6][5]. Ensure you are referencing the radareorg/r2flutter repository if your goal is reverse engineering Flutter applications.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
python3 - <<'PY' "$tmp"
import urllib.request
tmp = __import__('sys').argv[1]
repo = "radareorg/r2flutter"
for path in ["Makefile", "configure", "README.md"]:
    url = f"https://raw.githubusercontent.com/{repo}/master/{path}"
    out = f"{tmp}/{path}"
    try:
        urllib.request.urlretrieve(url, out)
    except Exception as e:
        print(f"FETCH_FAIL {path}: {e}")
        continue
    print(f"--- {path} ---")
    with open(out) as f:
        for i, line in enumerate(f, 1):
            if i <= 160 and any(s in line.lower() for s in ["bin/r2flutter", "PREFIX", "prefix", "make install", "install", "r2pm"]):
                print(f"{i}: {line.rstrip()}")
PY

Repository: caverav/flutterdec

Length of output: 623


🌐 Web query:

github radareorg r2flutter source files Makefile configure bin r2flutter

💡 Result:

r2flutter is an open-source tool maintained in a dedicated GitHub repository [1] designed to provide Dart and Flutter AOT (Ahead-of-Time) snapshot analysis capabilities for the Radare2 reverse engineering framework [1][2]. Repository and Technical Details: - Source Code: The repository is located at github.com/trufae/r2flutter [1]. It is a self-contained C-based implementation that parses Dart AOT snapshots, data images, clusters, and ObjectPool records without requiring Dart VM headers or libraries [1]. - Build System: The project uses a Makefile [1] to handle compilation and installation. Typical targets include make user-install, which installs the core plugin command as r2flutter [1]. - Binaries: Upon successful installation, the r2flutter command becomes available, allowing users to perform analysis directly from the command line or within an active radare2 session to recover classes, methods, types, and xrefs [1]. - Configuration: As a plugin designed to integrate with radare2, it leverages radare2 for binary loading [1]. While some related radare2 plugins or tools utilize a./configure script to generate build configuration files [3], the primary interface for r2flutter is typically via make commands that interface with the radare2 environment [1]. In summary, r2flutter acts as a specialized extension for radare2 to support the unique structure of Flutter applications, providing features like snapshot header recovery and SBOM/component reporting [1]. It can be built and installed using the standard Makefile provided in its repository [1].

Citations:


🌐 Web query:

"radareorg/r2flutter" "make user-install"

💡 Result:

The make user-install command is used to install the r2flutter plugin for radare2 within your local user directory [1]. This command compiles and installs the plugin, allowing it to be accessed as a command (r2flutter) from your shell and integrated into the radare2 environment [1]. To use it, ensure you are in the r2flutter repository directory, then run: make user-install This effectively installs the plugin's core command into your ~/bin directory (assuming it exists or is created by the Makefile), which makes it accessible for use with radare2 [1]. Note that this is distinct from other Flutter reverse engineering tools, such as reFlutter, which are typically installed via Python's pip [2][3].

Citations:


Update the r2flutter build instructions.

r2flutter is maintained under trufae/r2flutter, not radareorg/r2flutter, and the repo’s build/install path is make user-install; as written, the documented clone URL, ./configure, and docs/development.md:20 path will break both package resolution and FLUTTERDEC_R2FLUTTER_BIN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/development.md` around lines 14 - 21, Update the r2flutter setup
instructions in the development documentation to clone the repository from
trufae/r2flutter and use its supported make user-install build/install flow.
Ensure FLUTTERDEC_R2FLUTTER_BIN points to the resulting installed or built
bin/r2flutter location, removing the obsolete configure and incorrect repository
path.

Comment thread docs/how-it-works.md
Comment thread schemas/adapter.schema.json
caverav added 12 commits July 25, 2026 11:51
`object_pool[].index` has always been ambiguous: nothing in the contract said
whether it was an ObjectPool entry index or some adapter-private numbering. Add an
optional `pool_geometry` record (`entries_offset`, `word_size`) that an adapter emits
to assert the former.

Presence of the record is the assertion. Adapters that only carve strings out of the
snapshot omit it, which lets core tell the two cases apart instead of assuming.
`ldr xN, [x27, #disp]` was annotated `pool[disp]`, using the raw byte
displacement as if it were an entry index. It is not: the entry is
`(disp - entries_offset) / word_size`. Downstream joins that number against
`object_pool[].index`, so pool-backed values could be attached to the wrong slot.

Also recognise the page form, `add xD, x27, #K, lsl #S` followed by
`ldr xN, [xD, #off]`, which Dart emits once the displacement outgrows the load
immediate. On a Dart 3.9.2 sample that form is 13903 of the 19604 pool loads across 60
functions, and none of them were annotated at all before.

Page bases are tracked per function and dropped on control flow and on any write to
the register, including both destinations of load-pair forms, so a stale base cannot
invent a slot. Without `pool_geometry` there is no index space to map into, so the
annotation is `poolOff[<displacement>]`, which states what is actually known and never
matches a value hint.

Regression tests use instruction encodings taken from a real libapp.so, with expected
indices cross-checked against an independent ObjectPool decoder.
Page-based pool loads the disassembler's register tracker cannot follow still
arrive here as `((pool + <page> /* lsl #N */)).f<off>` text. That text carries a byte
displacement; the old code divided it by 8 and emitted `pool[<n>]`, which is the right
scale but skips `entries_offset`, so it landed two slots early and picked up whichever
hint lived there.

This layer has no pool geometry, so it cannot convert at all. Report the displacement
as `poolOff[<n>]` instead: unresolvable references stay unresolved rather than
resolving to a neighbour.

The two expression tests encoded the old arithmetic; they now assert the displacement.
`resolves_shifted_pool_target_to_symbol_call_name` was asserting that this path
resolves a semantic hint, which is exactly the behaviour being removed, so it becomes
`residual_shifted_pool_syntax_reports_displacement_and_does_not_resolve`. Symbol
resolution through a pool target is still covered via the normal `pool[N]` path in
call_and_loops.rs.
Pool value and semantic hints were built from `object_pool[].index` regardless of
what that number meant. The internal adapter numbers its entries by string carve order,
which has nothing to do with the hardware index space, so every hint it produced was a
real string from the binary attached to a slot that never referenced it.

Build hints only when the adapter reported `pool_geometry`. A missing string literal is
recoverable; a plausible wrong one is not, because nothing in the output distinguishes
it from a correct one.

`report.json.pool_metadata` now carries `index_space_authoritative`, the geometry, and
`hints_suppressed_reason`, so a run with no pool literals explains itself.
The expression cleanups scan raw text, so they treat the contents of a string as
code. A real pool string from a Flutter engine build reads

    "... the native peer has been collected (nullptr). This is usually ..."

and `(nullptr).` is a parenthesised member access to a byte scanner, so the simplifier
edited the parentheses out of a string that came out of the binary.

Skip over string literals in both member-access simplifiers. Recovered strings are
program data; a readability pass has no business rewriting them, and a silently
altered string is indistinguishable from a correctly recovered one.
…guments

Only call arguments were run through the pool hint, so a slot whose value we knew
still printed as a bare `pool[N]` everywhere else:

    t2.f19 = pool[40];
    if ((obj4.f31) == pool[893]) {
    return pool[5122];

Resolve the hint when a register is read as a whole value, which covers assignments,
comparisons and returns from one place. On the Dart 3.9.2 sample this takes rendered
literals from 15 to 24 across 80 functions and leaves 3 of the 27 known-value slots
unresolved, down from 12.

Dereferences deliberately keep the slot: `pool[40].f7` reads a field of the pooled
object, so rendering the string there would claim a field access on a literal. Those
still get `pool[40 /* "onError" */].f7`.

`annotate_pool_refs` is now idempotent, since operands can reach it already resolved
and it would otherwise nest a second hint comment inside the first.
`decompile` aborts with `File name too long (os error 36)` as soon as an adapter
returns real Dart names. The longest name in a Dart 3.9.2 sample is 305 bytes, well past
the 255-byte NAME_MAX, and mangled mixin and generic names routinely get there.

Cap the sanitized stem at 160 bytes. Artifact names are already prefixed with the unique
function id, so truncation cannot collide.

Only unreachable today because every name the internal adapter produces is `sub_<addr>`.
We already read the snapshot hash and then report `dart_version: unknown`, which
is a waste: the hash is an MD5 over Dart VM serializer sources, so it pins the exact SDK
release and snapshot layout.

The hash-to-version mapping cannot be computed from a binary, it has to be tabulated by
building every SDK. Vendor that table as data from radareorg/r2flutter (MIT):
61 hashes over 19 layout profiles, 11 KB, embedded with `include_str!`.

Resolution is by floor, matching how the table is built. Hashes are filed under exact
versions (3.6.2) while profiles are keyed by the release that last changed the layout
(3.6.0), so an exact-match lookup would reject most real snapshots.

Identification only. Unknown hashes stay unknown rather than falling back to a guessed
profile, since a guess would be indistinguishable from a real answer downstream.
`info` now returns `dart_version` and `dart_tag_style` with no adapter installed
and no disassembly, in both JSON and plain output, and `report.json` gains a
`dart_profile` section with the profile bucket, compressed word size, header field
count and alignment.

`dart_tag_style` is the useful one for triage: `CID_INT32`, `CID_SHIFT1` and
`OBJECT_HEADER` are three incompatible object-header encodings, and it is the first
thing to check when a snapshot parser misbehaves on an older build.

Both fields are null for hashes outside the bundled table.
The internal adapter carves strings and scans prologues, so it cannot name a
single function and has no real ObjectPool. r2flutter (MIT, radareorg) deserializes the
AOT snapshot, so wire it up at the existing adapter process boundary rather than growing
a snapshot parser in core.

Maps `-ji` (instruction table) to functions with exact names, `-jc` to classes, `-jxz`
to pool entries keyed by the slot index r2flutter reports per string, `-jzz` to library
URIs for scope filtering, and `-jp` to pool geometry.

Pool entries come with their authoritative `entry=<N>`, so nothing here is positional.
When `-jp` cannot resolve the pool image, which happens on older and non-compressed
snapshots, geometry and pool entries are both dropped instead of being guessed, and the
key is omitted rather than set to null so the payload still validates.

Every invocation is bounded by FLUTTERDEC_R2FLUTTER_TIMEOUT (default 900s). Six run per
model build and each loads the whole binary, so an unbounded wait would hang the core
behind a wedged radare2.

`auto` order becomes r2flutter, blutter, internal, each falling through when its tooling
is absent. Resolved via FLUTTERDEC_R2FLUTTER_BIN, FLUTTERDEC_R2FLUTTER_CMD, or PATH.

On a Dart 3.9.2 sample: 37258 exactly named functions and 8986 classes, against 7458
`sub_*` placeholders and 1 synthetic class from the internal adapter.

Optional fields are typed concretely in the schema, so the explicit nulls the adapters
have always emitted (`target_va`, `selector`, `owner_class`, and now `pool_geometry`)
fail validation where an absent key passes. Strip nulls on the way out; both the
internal and r2flutter payloads validate against schemas/adapter.schema.json for the
first time.
Expose the r2flutter backend through the same selection path as blutter, so it can
be forced or excluded rather than only reached through `auto`. Both `r2-flutter` and
the tool's own spelling `r2flutter` are accepted, since the docs, `report.json` and the
environment variables all use the latter.

`backend_label` was a second match arm listing every variant next to
`AdapterBackend::as_str`; fold it into the one that already exists so a new backend
cannot be added to one and forgotten in the other.
…backend

Write down the pool index contract in context.md and how-it-works.md, since it is
the part that was implicit and therefore got it wrong: what `object_pool[].index` means,
that `pool_geometry` is how an adapter claims it, and why core refuses to resolve
references without it.

Cover the r2flutter backend and its environment variables across the README, user guide,
CLI reference and development guide, with a table of what each backend actually recovers
so the internal adapter's limits are stated up front rather than discovered.

Record the vendoring rationale for data/dart-profiles.json and credit r2flutter and
blutter in the README. Correct the stale claim that core only accepts schema version 2.

Also flags a follow-up: the README pipeline showcase assets were rendered before the pool
index fix, so their `pool[...]` mappings come from the old index space and need
regenerating before they can be cited as evidence.
@caverav
caverav force-pushed the feat/pool-index-space-and-r2flutter-backend branch from ffd2b4d to 2d8f301 Compare July 25, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant