fix(pool): resolve object pool references in the real index space, add r2flutter backend - #85
fix(pool): resolve object pool references in the real index space, add r2flutter backend#85caverav wants to merge 12 commits into
Conversation
WalkthroughThe 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)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
adapters/python/adapter_template.py (1)
823-843: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the unused second return value.
_r2flutter_classesalways returns[]as the library table and the only caller discards it (Line 869). A plainList[dict]return is clearer than aTuplewhose 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 valueNew
infometadata is only visible with--json.
handle_infoincrates/flutterdec-cli/src/main.rs(Lines 262-314) prints each optional field individually in the human-readable branch;dart_version/dart_tag_styleare not printed there, so plainflutterdec infosilently drops the new metadata. Worth adding twoif let Some(..)prints (and listing the fields in theinfoJSON field list indocs/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 valueAccept
r2flutteras a CLI alias for--adapter-backend.
AdapterBackendArgderivesr2-flutteras the clap value name, whileAdapterBackend::to_core("r2flutter")emitsr2flutter, 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (25)
README.mdadapters/python/adapter_template.pycontext.mdcrates/flutterdec-adapter/src/lib.rscrates/flutterdec-cli/src/main.rscrates/flutterdec-core/src/lib.rscrates/flutterdec-core/src/pipeline/apk_startup.rscrates/flutterdec-core/src/pipeline/helpers.rscrates/flutterdec-core/src/pipeline/runners.rscrates/flutterdec-core/src/pipeline/runners/tests.rscrates/flutterdec-decompiler/src/helpers/expr.rscrates/flutterdec-decompiler/src/passes/expr_cleanup.rscrates/flutterdec-decompiler/src/tests/emit_and_helpers/readability_and_naming.rscrates/flutterdec-disasm-arm64/src/lib.rscrates/flutterdec-ir/src/lib.rscrates/flutterdec-loader/Cargo.tomlcrates/flutterdec-loader/src/dart_profile.rscrates/flutterdec-loader/src/lib.rsdata/dart-profiles.jsondocs/cli-reference.mddocs/development.mddocs/how-it-works.mddocs/research-decisions.mddocs/user-guide.mdschemas/adapter.schema.json
| 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]}] |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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" | ||
| ``` |
There was a problem hiding this comment.
📐 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:
- 1: https://github.com/trufae/r2flutter
- 2: https://book.rada.re/tools/r2pm/intro.html
- 3: https://man.archlinux.org/man/extra/radare2/r2pm.1.en
- 4: https://github.com/radareorg/radare2/blob/cdda780e9364aeb39d29460b069389b169f0f8d3/INSTALL.md
🌐 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:
- 1: https://github.com/trufae/r2flutter
- 2: https://github.laiyagushi.com/radareorg/radare2
- 3: https://www.github.com/radare/radare2
- 4: https://github.com/radareorg/radare2/blob/master/README.md
- 5: https://github.com/radareorg/radare2/blob/master/configure
- 6: https://github.com/radareorg/radare2/blob/cdda780e9364aeb39d29460b069389b169f0f8d3/INSTALL.md
🏁 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" . || trueRepository: 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:
- 1: https://github.com/trufae/r2flutter
- 2: https://github.com/radareorg/radare2
- 3: https://github.com/radareorg/radare2/blob/master/configure
- 4: https://book.rada.re/install/android.html
- 5: https://stackoverflow.com/questions/78099780/cloudflare-r2-with-flutter
- 6: https://pub.dev/packages/cloudflare_r2
🏁 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()}")
PYRepository: 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:
- 1: https://github.com/trufae/r2flutter
- 2: https://github.com/radareorg/radare2/tree/refs/heads/master
- 3: https://github.com/nowsecure/r2frida/blob/master/Makefile
🌐 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:
- 1: https://github.com/trufae/r2flutter
- 2: https://github.com/ptswarm/reFlutter
- 3: https://pypi.org/project/reflutter/0.4.8/
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.
`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.
ffd2b4d to
2d8f301
Compare
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 annotatedpool[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 againstobject_pool[].indexto 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:The page form was wrong in a nastier way.
add x0, x27, #0x23, lsl #12+ldr x0, [x0, #0xa90]was reconstructed textually asdisplacement / 8, which is the right scale but skipsentries_offset, so it lands exactly two slots early: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 literalpool[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_geometryis 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 tracksadd xD, x27, #K, lsl #Spage 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 ispoolOff[<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_reasonsays 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:
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].f7reads 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).
decompiledies withFile 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 issub_<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).infonow reportsdart_versionanddart_tag_stylewith 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.autobecomes r2flutter, blutter, internal, each falling through when its tooling is absent.Before and after on the same function:
Worth saying plainly: the readability passes were always fine, they had just never been given real input.
Validation
cargo fmt --all --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace, 286 passed, 0 failed./scripts/lint-shell.shEach 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;autofalling back with r2flutter absent;autoselecting it when present; explicitr2-flutterand ther2flutteralias; 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:
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+0xef8now resolves topool[477], atype_argumentsobject, 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:
FLUTTERDEC_R2FLUTTER_TIMEOUT, default 900s.ldpleaves a stale pool base. Real, and the same class of bug this PR exists to fix.invalidate_first_operandcleared one destination, soldp x0, x1, [sp, #16]leftx1holding 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": nullfails the schema. Real, and bigger than reported. Validating the payload showedtarget_va,selectorandowner_classhave 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_versionmissing from plaininfo. Real, only--jsonshowed it. Now printed in both, and documented.--adapter-backend r2flutterrejected. Real papercut: clap derivesr2-flutterwhile the docs, env vars andreport.jsonall sayr2flutter. Both spellings accepted now.Two were not:
libs[0]IndexError when no library URIs are carved._collect_librariesfalls back to["package:app/main.dart"]before returning, so it cannot return empty and the index is always safe.trufae/r2flutter, and./configurewill break.radareorg/r2flutteris not a fork and has no parent per the GitHub API, and the documented./configure && makeis exactly how I built the binary used throughout this PR.Its nitpick about the dead second return value from
_r2flutter_classeswas 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
docs/assets/readme/zedsecure-*showsldr x3, [x27, #0x658] ; pool[1624] = "minWidth". Under the correct formula0x658is 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 incontext.md; we should not cite them as evidence until they are redone.autosilently mis-parsing if upstream output changes.app_package_counts_topis empty on that backend and--function-scope appcannot discriminate. Libraries come from-jzz(512 recovered) but are not joined to classes.PP not resolved, so geometry is omitted and hints stay off. It degrades correctly, but the backend is not uniformly better everywhere.-jxzonly 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-serwalkertouches 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
ProgramModelcontract, so--adapter-backend nativecan slot in later next tor2-flutterwithout 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.jsoncovers a gap it has structurally: itsClassIdis 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 snapshottry_fromreturns the wrong variant or errors. Same for tag decoding, whereDECODE_CID!is hardcoded to theOBJECT_HEADERform and there are three encodings in the wild. That constraint is written down indocs/research-decisions.mdnow.Scope
type(scope): description)README.md,docs/*,context.md)One thing that looks like a drive-by but is not:
backend_labelwas a second match arm listing every backend next toAdapterBackend::as_str. Adding a variant meant updating both, and forgetting one silently mislabels the backend inreport.json. Folded into the existing one in the same commit that adds the variant.