Add more documentation

This commit is contained in:
pancake committed 2026-06-01 18:02:10 +02:00
1 parent 9b9a112561
commit b69ebf35a4
3 files changed
+630 -24

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
## Version Identification in Dart/Flutter AOT Snapshots
### The Snapshot Hash (Checksum)
**What it is**: A **32-byte ASCII MD5 hash** (Snapshot_hash) generated at compile time using the `make_version.py` script in the Flutter Engine source . This hash is **not of the binary itself**, but rather a hash representing the **Dart VM version and snapshot format**.
**Where it's stored**:
- **Android**: In `libapp.so` (the AOT snapshot) and `libflutter.so` (the Flutter engine)
- **iOS**: In `App.framework/App` and `Flutter.framework/Flutter`
- **Purpose**: Runtime compatibility checking between the engine and the snapshot
### How Tools Associate Hash with Version
#### 1. **reFlutter's Approach: Static CSV Table**
reFlutter uses a manually maintained **`enginehash.csv`** file that maps:
- `Snapshot_hash` → `Engine_commit` → Flutter version → Dart version
Example from their table:
```csv
Flutter_Version, Engine_commit, Snapshot_hash
2.10.0, 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c, d56742caf7b3b3f4bd2df93a9bbb5503
```
**Update mechanism**: The table is **periodically updated manually** when new Flutter versions are released. If a hash isn't found, the tool reports "Engine not supported" .
#### 2. **unflutter's Approach: Constraint-Based Version Detection**
unflutter uses a **different philosophy** - it doesn't rely on a downloadable table. Instead:
- The **version hash in the snapshot header** selects a **constraint set** (CID table, tag encoding, pointer compression rules)
- Version-specific data is **statically defined in code** as version profiles:
- Dart 2.10.0: CID-Int32, Uncompressed pointers, 4 header fields
- Dart 2.18.0: CID-Shift1, Compressed pointers, Signed refs
- Dart 3.4.3+: ObjectHeader, Compressed, Record types
**Key insight**: unflutter proves you can **statically define all version constraints** without runtime downloads because the snapshot format changes are deterministic and versioned.
#### 3. **Blutter's Approach: SDK Compilation**
Blutter **compiles the matching Dart SDK** for each version:
- Detects version hash from snapshot
- Downloads/checkout the exact Dart SDK source for that version
- Builds the VM library to parse the snapshot
This is the most accurate but slowest approach.
---
## What Data is Version-Associated?
### Version-Specific Data Structures
| Component | Version-Dependent? | Description |
|-----------|-------------------|-------------|
| **Snapshot Header** | Yes | Magic (0xf5f5dcdc), version hash (32 bytes), feature flags |
| **CID Table** | Yes | Maps Class IDs to cluster handlers; changes per version |
| **Tag Encoding** | Yes | How object tags are packed (CID-Int32, CID-Shift1, ObjectHeader) |
| **Pointer Compression** | Yes | 32-bit vs 64-bit references (introduced in 2.18.0) |
| **Cluster Grammar** | Yes | How alloc/fill phases encode object fields |
| **THR (Thread) Fields** | Yes | Offset maps for thread-local storage access |
| **Instruction Layout** | Partial | Stubs vs code regions, entry point formats |
### Static vs Dynamic Definition
**You can statically define everything.** Here's why:
1. **The version hash is in the snapshot itself** - you read it directly from the binary at a known offset
2. **Format changes are versioned** - Dart uses semantic versioning, and snapshot format changes happen at specific version boundaries
3. **Constraint sets are finite** - unflutter demonstrates this by having version profiles for every major Dart version from 2.10.0 to 3.10.7
### Version Hash Location in Binary
```
Snapshot Header Structure:
[0x00-0x03] Magic: 0xf5f5dcdc
[0x04-0x23] Version Hash: 32-byte ASCII (e.g., "d56742caf7b3b3f4bd2df93a9bbb5503")
[0x24-...] Feature flags, pointer size, etc.
```
---
## Recommendation for Your Implementation
### Option 1: Static Constraint Tables (Recommended)
Like unflutter, define version constraints statically:
```go
// Example version profile structure
var VersionProfiles = map[string]VersionProfile{
"d56742caf7b3b3f4bd2df93a9bbb5503": {
DartVersion: "2.16.0",
TagStyle: "CID-Shift1",
PointerSize: 8, // Uncompressed
HeaderFields: 5,
CIDTable: Pre210CIDTable,
ClusterHandlers: Pre210ClusterHandlers,
},
"1441d6b13b8623fa7fbf61433abebd31": {
DartVersion: "2.17.0",
TagStyle: "CID-Shift1",
PointerSize: 4, // Compressed pointers
HeaderFields: 5,
CIDTable: Post218CIDTable,
ClusterHandlers: Post218ClusterHandlers,
},
}
```
### Option 2: Hybrid Approach
- **Static**: Core constraint tables for known versions
- **Dynamic**: Download new version profiles when encountering unknown hashes (rare, only for bleeding-edge Flutter versions)
### Where to Get Version Data
1. **hadysata/flutter-versions** repo: Comprehensive table of Flutter version ↔ Dart version ↔ Snapshot hash ↔ Engine commit
2. **mildsunrise/darter/info/versions.md**: Historical snapshot hash reference
3. **Google Storage**: `storage.googleapis.com/flutter_infra_release/flutter/<engine_commit>/` for engine artifacts
### Critical Files in Dart SDK Source
If you need to extract version-specific logic from the Dart SDK source :
- `runtime/vm/clustered_snapshot.cc`: Serialization/deserialization logic
- `runtime/vm/raw_object.h`: Object tag definitions per version
- `runtime/vm/object.cc`: Class ID assignments
---
## Summary
**You do NOT need to download anything at runtime.** The version hash in the snapshot header (32-byte ASCII at offset 0x04) maps to a finite set of constraint profiles that can be statically defined. unflutter proves this works for all Dart versions from 2.10.0 to 3.10.7 without any runtime SDK compilation or network downloads .
The only time you'd need dynamic updates is when Flutter releases a new version with breaking snapshot format changes - which happens infrequently and follows a predictable release cycle (stable, beta, dev channels) .
---
## r2flutter Implementation
r2flutter implements Option 1 (Static Constraint Tables) from this document. The implementation in `src/lib/dart_pool_parse.c` includes:
### Hash-to-Version Mapping
- 40+ known snapshot hashes from Flutter 1.22.x (Dart 2.10.0) to Flutter 3.38.x (Dart 3.10.7)
- Sources: unflutter version.go, reFlutter enginehash.csv, blutter precompiled SDKs
### Version Profiles
Each supported Dart version has a profile with:
- Tag encoding style (CID-Int32, CID-Shift1, ObjectHeader)
- Header field count (4-6 depending on version)
- CID table mapping (Class, Function, Code, String, Array, etc.)
- Compressed pointer configuration
- Number of predefined CIDs
### Supported Versions
| Dart Version | Tag Style | Notes |
|--------------|-----------|-------|
| 2.10.0 | Int32 CID | Pre-FunctionType, single cluster loop |
| 2.13.0 | Int32 CID | Split canonical/non-canonical clusters |
| 2.14.0-2.17.6 | CID Shift1 | `(cid << 1) \| canonical` |
| 2.18.0-3.3.0 | CID Shift1 | Various CID changes |
| 3.4.3-3.10.7 | ObjectHeader | CID at bits 12-31 |
Unknown hashes fall back to v3.9.2 profile with ObjectHeader encoding.
+79
View File
@@ -0,0 +1,79 @@
## What is a Cluster in Dart AOT?
A **cluster** is the fundamental serialization unit in Dart's AOT snapshot format. It's a group of objects of the **same class/type** that are serialized together using a shared encoding scheme .
### The Two-Phase Cluster Serialization
Dart AOT snapshots use a two-phase process where clusters are the core organizational structure:
```
Phase 1: ALLOC → Phase 2: FILL
```
**Phase 1: Allocation (Alloc)**
- Walks through clusters in **Class ID (CID) order**
- Each cluster declares: *"I contain N objects of class X"*
- Assigns sequential **reference IDs** to every object
- **No actual data is read yet** - only counts and reservations
**Phase 2: Fill**
- Walks through the same clusters again
- Reads actual field values: string bytes, reference IDs, integers
- Encoding varies by object type and Dart version
### Cluster Structure
```
Cluster Header:
- CID (Class ID): Identifies the class type (e.g., String, List, Code)
- Count: Number of objects in this cluster
- Flags: Encoding options
Cluster Body (Fill Phase):
- Object 1 field data
- Object 2 field data
- ... N times
```
### Why Clusters Exist
1. **Deduplication**: Objects of the same type share encoding logic
2. **Sequential Access**: Better cache locality during deserialization
3. **Compression of Metadata**: Class information stored once per cluster, not per object
4. **Version Flexibility**: Different Dart versions can change cluster handlers independently
### Cluster Types (CID Examples)
| CID | Class | Description |
|-----|-------|-------------|
| 1-5 | Object, Class, Function, etc. | VM internal classes |
| 50+ | String variants | One-byte, two-byte strings |
| 60+ | Array types | TypedData, GrowableObjectArray |
| 100+ | Code objects | Compiled function machine code |
### Real-World Example from unflutter
When unflutter parses a snapshot:
```
1. Reads snapshot header → finds version hash
2. Loads CID table → maps CID → cluster handler
3. For each cluster:
- ALLOC: Reads count, assigns reference IDs
- FILL: Parses field values based on CID-specific grammar
4. Result: Complete object graph with all strings, classes, functions
```
### Cluster Grammar Constraints
The unflutter tool treats clusters as **constraint-based parsing**:
- **ELF invariants** eliminate invalid binaries
- **Snapshot magic** (`0xf5f5dcdc`) confirms Dart format
- **Version hash** selects exact CID table and encoding
- **Cluster alloc counts** fix object population
- **Fill parsing** recovers field values
### Key Insight for Reverse Engineering
**Clusters are why plain text extraction works**: Because clusters group objects by type, all strings of a certain type are stored in sequence with predictable offsets. This allows tools like Blutter and unflutter to walk the cluster and extract every string without executing the code.
The cluster format is **deterministic and version-specific**, which is why reverse engineering tools need to handle format changes across Dart versions explicitly .
+391 -24
View File
@@ -1,29 +1,396 @@
# Support Matrix
r2flutter currently carries layout profiles for Dart AOT snapshots from **Dart 2.10.0 to 3.10.7** (roughly Flutter **1.22.x to 3.38.x**).
This document summarizes what the current first-party source supports. It is a
parser/layout support note, not a promise that every Flutter engine patch in a
range has regression coverage.
This matrix describes parser/layout support, not exhaustive regression coverage for every engine patch release. Platform notes summarize the binary/container differences the current parser accounts for when applying the same Dart snapshot profile.
r2flutter targets Dart AOT snapshots embedded in Flutter applications. It uses
radare2 for binary loading and then reads Dart snapshot metadata, data images,
instruction tables, and strings directly.
| Dart Version | Flutter Version | Tag Style | Notes | Platform Details |
|--------------|-----------------|-----------|-------|------------------|
| 2.10.0 | 1.22.x | Int32 CID | Pre-FunctionType split | Android: legacy `libapp.so` ELF snapshots in the pre-compressed-pointer era. iOS: same Int32 profile in Mach-O-style app binaries, typically with full-width pointers. |
| 2.13.0 | 2.2.x - 2.3.x | Int32 CID | Split canonical clusters | Android: ELF parser still sees older non-ObjectHeader snapshots. iOS: same snapshot layout family, with platform differences mostly limited to Mach-O container handling. |
| 2.14.0 | 2.4.x - 2.5.x | CID Shift1 | TypeParameters added | Android: first Shift1-era `libapp.so` layouts. iOS: matching Shift1 snapshot profile, usually without Android-style compressed-pointer behavior. |
| 2.15.0 | 2.6.x - 2.7.x | CID Shift1 | NativePointer inserted | Android: ELF snapshots with updated CID table. iOS: same Dart layout profile applied through Mach-O snapshot discovery. |
| 2.16.0 | 2.8.x - 2.16.x | CID Shift1 | ConstMap/ConstSet added | Android: broader Flutter 2.x coverage on `libapp.so`. iOS: same Shift1 decoding, with iOS-specific work mostly in binary loading rather than object layout. |
| 2.17.6 | 2.17.x | CID Shift1 | WeakReference added | Android: final pre-2.18 range before compressed pointers become common. iOS: same snapshot family, generally still treated as full-width-pointer binaries. |
| 2.18.0 | 3.3.x | CID Shift1 | SuspendState added | Android: compressed-pointer-era builds become common and cluster counts can grow significantly. iOS: still uses the same Shift1 layout profile, but current samples remain easier to walk due to smaller metadata. |
| 2.19.0 | 3.7.x | CID Shift1 | RecordType/Record added | Android: ELF snapshots continue in the compressed-pointer era. iOS: same profile on Mach-O, with platform deltas still centered on container parsing and address mapping. |
| 3.0.5 | 3.10.x - 3.12.x | CID Shift1 | WeakArray added | Android: Shift1 + compressed pointers remain the main path. iOS: Mach-O snapshots still use the matching Shift1 layout without moving to ObjectHeader yet. |
| 3.1.0 | 3.13.x | CID Shift1 | TypeRef removed | Android: updated CID table in ELF snapshots. iOS: same layout transition applied in Mach-O binaries. |
| 3.2.5 | 3.16.x | CID Shift1 | PoolType swapped | Android: late Shift1 profile before ObjectHeader. iOS: same late-Shift1 profile, still generally observed with full-width pointers. |
| 3.3.0 | 3.19.x | CID Shift1 | Last Shift1 format | Android: last Shift1 generation before the ObjectHeader change. iOS: same final Shift1 profile on Mach-O binaries. |
| 3.4.3 | 3.22.x | ObjectHeader | New tag encoding | Android: ObjectHeader + compressed pointers are the main modern `libapp.so` target. iOS: ObjectHeader too, but current samples keep full-width pointers and smaller cluster metadata. |
| 3.5.0 | 3.24.x | ObjectHeader | ObjectHeader family stabilized | Android: modern ELF path with ObjectHeader parsing. iOS: same tag format with Mach-O container differences only. |
| 3.6.2 | 3.27.x | ObjectHeader | Incremental CID/profile updates | Android: ObjectHeader remains the expected format. iOS: same ObjectHeader profile, typically with simpler address translation in current samples. |
| 3.7.0 | 3.29.x | ObjectHeader | Ongoing ObjectHeader era | Android: modern compressed-pointer snapshots. iOS: same ObjectHeader layout profile in app binaries. |
| 3.8.1 | 3.32.x | ObjectHeader | Ongoing ObjectHeader era | Android: ELF support continues with the same high-level parser flow. iOS: Mach-O support continues with the same snapshot profile selection. |
| 3.9.2 | 3.35.x | ObjectHeader | Fallback baseline for unknown hashes | Android: default fallback profile for unknown modern ELF hashes. iOS: default fallback profile for unknown modern Mach-O hashes. |
| 3.10.7 | 3.38.x | ObjectHeader | Current latest | Android: newest known `libapp.so` profile in-tree. iOS: newest known app-binary profile in-tree. |
## Main Targets
Unknown snapshot hashes currently fall back to the **v3.9.2** profile with ObjectHeader encoding.
| Area | Supported in source |
|------|---------------------|
| App containers | Android directories containing `libapp.so`; iOS `.app` directories containing `Frameworks/App.framework/App`; direct binary paths. |
| Binary loaders | Whatever radare2 `RBin` opens, with explicit code paths for ELF-style symbols/sections and Mach-O app binaries. |
| Mach-O details | Thin Mach-O64 and fat Mach-O/fat64; arm64 slice is preferred. LC_NOTE owner `__dart_app_snap` is detected and the embedded Dart Mach-O payload is extracted to a temporary file. |
| Architecture | ARM64/AArch64 is the real target. Instruction-table entrypoints are based on ARM64 code offsets, PP tracking uses `x27`, and the radare2 analysis pass understands AArch64 registers/op metadata. |
| Snapshot kind | Clustered Dart AOT snapshots with magic `0xdcdcf5f5`, 32-byte snapshot hashes, feature strings, and unsigned varint header fields. |
| Dart layouts | In-tree profiles cover Dart `2.10.0` through `3.10.7`, roughly Flutter `1.22.x` through `3.38.x`. |
| Unknown hashes | First tries `r2flutter/offsets.json` or `offsets.json`; otherwise uses v3.9.2-shaped ObjectHeader defaults while reporting the Dart version as `unknown`. |
| Obfuscation maps | Flutter/Dart `--save-obfuscation-map` JSON arrays are supported with `-o <file>` or `e r2flutter.mapfile=<file>` in the plugin. |
## Layout Profiles
`CWS` is the profile default compressed word size. The actual value can be
overridden by snapshot feature flags: `compressed-pointer` sets CWS to 4 unless
`no-compressed-pointer` or `no-compressed` is present; otherwise CWS is 8.
Compressed ObjectHeader snapshots, and compressed Shift1 snapshots from Dart
2.19 onward, can raise object alignment to 64 bytes.
The CID column is compacted as:
`class/function/code/string/one_byte_string/two_byte_string/array/mint/object_pool`
| Dart | Flutter range | Tag style | CWS | Header fields | Predefined CIDs | CID set | Source note |
|------|---------------|-----------|-----|---------------|-----------------|---------|-------------|
| 2.10.0 | 1.22.x | `CID_INT32` | 8 | 4 | 156 | `4/6/16/80/81/82/78/53/20` | Pre-FunctionType split. |
| 2.13.0 | 2.2.x - 2.3.x | `CID_INT32` | 8 | 5 | 148 | `4/6/15/77/78/79/75/51/18` | Split canonical clusters. |
| 2.14.0 | 2.4.x - 2.5.x | `CID_SHIFT1` | 8 | 5 | 152 | `4/6/16/81/82/83/79/54/20` | TypeParameters era. |
| 2.15.0 | 2.6.x - 2.7.x | `CID_SHIFT1` | 8 | 5 | 153 | `5/7/17/82/83/84/79/55/21` | NativePointer inserted. |
| 2.16.0 | 2.8.x - 2.16.x | `CID_SHIFT1` | 8 | 6 | 154 | `5/7/17/84/85/86/81/55/21` | ConstMap/ConstSet added. |
| 2.17.6 | 2.17.x | `CID_SHIFT1` | 4 | 6 | 158 | `5/7/17/89/90/91/86/59/21` | WeakReference added. |
| 2.18.0 | 3.3.x | `CID_SHIFT1` | 4 | 5 | 159 | `5/7/17/90/91/92/87/59/21` | SuspendState added. |
| 2.19.0 | 3.7.x | `CID_SHIFT1` | 4 | 5 | 176 | `5/7/17/92/93/94/89/60/21` | RecordType/Record added. |
| 3.0.5 | 3.10.x - 3.12.x | `CID_SHIFT1` | 4 | 5 | 177 | `5/7/18/93/94/95/90/61/22` | WeakArray added. |
| 3.1.0 | 3.13.x | `CID_SHIFT1` | 4 | 5 | 176 | `5/7/18/92/93/94/89/60/22` | TypeRef removed. |
| 3.2.5 | 3.16.x | `CID_SHIFT1` | 4 | 5 | 176 | `5/7/18/92/93/94/89/60/22` | PoolType swapped. |
| 3.3.0 | 3.19.x | `CID_SHIFT1` | 4 | 5 | 176 | `5/7/18/92/93/94/89/60/22` | Last Shift1 profile in-tree. |
| 3.4.3 | 3.22.x | `OBJECT_HEADER` | 4 | 5 | 174 | `5/7/18/92/93/94/89/60/22` | ObjectHeader tag encoding begins. |
| 3.5.0 | 3.24.x | `OBJECT_HEADER` | 4 | 5 | 174 | `5/7/18/92/93/94/89/60/22` | ObjectHeader family. |
| 3.6.2 | 3.27.x | `OBJECT_HEADER` | 4 | 5 | 175 | `5/7/18/93/94/95/90/61/23` | CID table update. |
| 3.7.0 | 3.29.x | `OBJECT_HEADER` | 4 | 5 | 175 | `5/7/18/93/94/95/90/61/23` | Same modern profile family. |
| 3.8.1 | 3.32.x | `OBJECT_HEADER` | 4 | 5 | 175 | `5/7/18/93/94/95/90/61/23` | Same modern profile family. |
| 3.9.2 | 3.35.x | `OBJECT_HEADER` | 4 | 5 | 175 | `5/7/18/93/94/95/90/61/23` | Unknown-hash fallback baseline. |
| 3.10.7 | 3.38.x | `OBJECT_HEADER` | 4 | 5 | 175 | `5/7/18/93/94/95/90/61/23` | Newest in-tree profile. |
All profiles use heap object tag `1`, default alignment `16`, and instruction
table capacity `20000` before feature-flag or `offsets.json` adjustments.
## Known Snapshot Hashes
These hashes are mapped directly by `src/lib/dart_version.c`.
| Dart version | Hashes |
|--------------|--------|
| 2.10.0 | `8ee4ef7a67df9845fba331734198a953` |
| 2.13.0 | `e4a09dbf2bb120fe4674e0576617a0dc`, `34f6eec64e9371856eaaa278ccf56538`, `7a5b240780941844bae88eca5dbaa7b8` |
| 2.14.0 | `9cf77f4405212c45daf608e1cd646852`, `659a72e41e3276e882709901c27de33d` |
| 2.15.0 | `f10776149bf76be288def3c2ca73bdc1`, `24d9d411c2f90c8fbe8907f99e89d4b0` |
| 2.16.0 | `d56742caf7b3b3f4bd2df93a9bbb5503`, `3318fe66091c0ffbb64faec39976cb7d`, `adf563436d12ba0d50ea5beb7f3be1bb` |
| 2.17.6 | `1441d6b13b8623fa7fbf61433abebd31`, `a0cb0c928b23bc17a26e062b351dc44d`, `ded6ef11c73fdc638d6ff6d3ad22a67b` |
| 2.18.0 | `b0e899ec5a90e4661501f0b69e9dd70f`, `b6d0a1f034d158b0d37b51d559379697`, `8e50e448b241be23b9e990094f4dca39`, `6a9b5a03a7e784a4558b10c769f188d9` |
| 2.19.0 | `adb4292f3ec25074ca70abcd2d5c7251`, `501ef5cbd64ca70b6b42672346af6a8a` |
| 3.0.5 | `90b56a561f70cd55e972cb49b79b3d8b`, `aa64af18e7d086041ac127cc4bc50c5e`, `36b0375d284ee2af0d0fffc6e6e48fde`, `16ad76edd19b537bf6ea64fdd31977a7` |
| 3.1.0 | `7dbbeeb8ef7b91338640dca3927636de` |
| 3.2.5 | `f71c76320d35b65f1164dbaa6d95fe09` |
| 3.3.0 | `ee1eb666c76a5cb7746faf39d0b97547` |
| 3.4.3 | `d20a1be77c3d3c41b2a5accaee1ce549` |
| 3.5.0 | `80a49c7111088100a233b2ae788e1f48`, `cda356e9bae476c70de33809fd92e009` |
| 3.6.2 | `2858c2c0920495f00b9bce9edf6a8cd9`, `f956f595844a2f845a55707faaaa51e4` |
| 3.7.0 | `d91c0e6f35f0eb2e44124e8f42aa44a7` |
| 3.8.1 | `830f4f59e7969c70b595182826435c19` |
| 3.9.2 | `97ff04a728735e6b6b098bdf983faaba` |
| 3.10.7 | `1ce86630892e2dca9a8543fdb8ed8e22` |
`offsets.json` can enrich or override a hash with `compressed_word_size`,
`heap_object_tag`, `max_alignment`, `it_cap`, `code_entry_point_offsets`,
`code_owner_offsets`, and `function_name_offsets`. The code looks for it first
under `r2flutter/offsets.json`, then in the current directory as `offsets.json`.
## Snapshot Discovery
Discovery first looks for these exact symbol spellings:
- `_kDartVmSnapshotData`
- `DartVmSnapshotData`
- `_kDartVmSnapshotInstructions`
- `DartVmSnapshotInstructions`
- `_kDartIsolateSnapshotData`
- `DartIsolateSnapshotData`
- `_kDartIsolateSnapshotInstructions`
- `DartIsolateSnapshotInstructions`
If symbols are missing, readable sections are scanned in 4 KB windows for
snapshot magic. Candidates that parse as data snapshots are split from
instruction snapshots; the smaller data snapshot is treated as VM data and the
larger as isolate data. The same size heuristic is used for instruction
snapshots.
Header parsing reads:
- magic, total length, and snapshot kind
- 32-byte hash
- null-terminated feature string, capped at 2048 scanned bytes
- base object count, object count, cluster count, instruction-table length,
instruction-table data offset
- cluster stream start
Cluster values are rejected or downgraded when they are obviously too large:
cluster count must stay below 1,000,000 and object count below 10,000,000 for
full deserialization.
## Extraction Support
### Functions and Names
Function discovery merges several sources:
- instruction-table entries
- class method metadata recovered from clusters or data image scans
- legacy clustered Function objects
- loader symbols only when stubs are explicitly included internally
The default CLI and plugin set `no_stubs=true`, so loader-provided ELF/Mach-O
stub symbols are skipped. That keeps output focused on Dart-derived names.
Recovered method names are normalized into radare2-safe names. Class-method
names use the `method.<library>.<owner>.<leaf>` shape. Library names strip
`package:` and trailing `.dart`; non-name characters collapse to underscores.
Constructors, getters, setters, anonymous closures, and Dart operators are
renamed into stable leaf names such as `ctor`, `get_name`, `set_name`,
`_anon_closure`, `op_eq`, `op_add`, and `op_ushr`.
The optional `-n` / `r2flutter.namepool` fallback consumes `package:` and
`dart:` strings from the data image as sequential names. It is intentionally
opt-in because one wrong stub/function distinction shifts every later name.
### Instruction Table
`-i` exposes the instruction table. JSON output includes:
- table `length`
- `first_entry_with_code`
- `canonical_stack_map_entries_offset`
- per-entry `index`, `code_index`, `address`, `pc_offset`,
`stack_map_offset`, `kind`, and optional `name`
The decoder supports:
- linear fallback when `itdata == 0`: `iso_instr + index * 4`
- fixed table data with 8-byte entries (`pc_offset`, `stack_map_offset`)
- header probing near `data_image_base + itdata`
- scanning OneByteString-like payloads for embedded table data
- bounded fallback scans in the first data-image region
- varint-encoded table fallback around the expected table address
Entry names are resolved in this order: modern cluster code-index map, loader
symbol at the entrypoint, data-image code-name scan, nearby stack-map string,
opt-in name pool, owner-kind stub names, then `method.fn_<index>`.
Modern compressed ObjectHeader snapshots also classify code owners so allocation
stubs, type-test stubs, VM stubs, and regular functions do not consume the same
name-pool stream.
### Modern ObjectHeader Naming
The modern cluster scanner is enabled only for snapshots with:
- `OBJECT_HEADER` tag style
- CWS 4 compressed pointers
It walks allocation/fill records for strings, classes, libraries, patch
classes, closure data, functions, code objects, arrays, typed data,
object pools, contexts, records, and common runtime objects. Its primary goal
is name recovery, not full object reconstruction.
It maps:
- Function owners to `method.<owner>.<name>`
- closure data to `_anon_closure`
- class-owned Code objects to `stub.Allocate<ClassName>Stub`
- type-owned Code objects to `stub.TypeTest_<TypeName>`
- ownerless Code objects to VM stubs
Full-width ObjectHeader snapshots still use the layout profile, data-image
scans, instruction-table decoding, and string scans, but they do not take this
compressed modern cluster naming path.
### Classes, Types, Fields, and Methods
`-c` dumps class metadata. `-T` prints a type-oriented view and runs enum
recovery. `-C` in the radare2 plugin emits type definitions.
Class output can include:
- reference id and class name
- library ref/name
- superclass ref/name
- instance size, type-parameter count, type-argument offset
- flags: abstract, enum, mixin, top-level
- enum values
- fields with name, type, offset, and static/final/const/late flags
- methods with entrypoint, owner, kind tag, and decoded method kind
Cluster extraction reads String, Class, Field, and Library records where the
cluster layout is understood. If that does not produce classes, a string-based
type-name fallback scans the VM-to-isolate data window.
Data-image scans add fields and methods back onto recovered classes. Field
scans recognize Field objects and read owner/name strings and offsets. Method
scans recognize Function objects, entrypoints, kind tags, owner class names,
and method names.
Recognized method kind names are:
- `RegularFunction`
- `ClosureFunction`
- `ImplicitClosureFunction`
- `GetterFunction`
- `SetterFunction`
- `Constructor`
- `ImplicitGetter`
- `ImplicitSetter`
- `ImplicitStaticGetter`
- `FieldInitializer`
- `MethodExtractor`
- `NoSuchMethodDispatcher`
- `InvokeFieldDispatcher`
- `IrregexpFunction`
- `DynamicInvocationForwarder`
- `FfiTrampoline`
- `RecordFieldGetter`
Enum recovery is heuristic. It looks for type-name strings, trailing
`TypeName.`, and `TypeName.value` pairs. Candidate names must look like Dart
types, values must look like enum values, and suffixes such as `Action`,
`Behavior`, `Direction`, `Kind`, `Mode`, `State`, `Status`, and `Style` boost
confidence.
### Strings
`-s` dumps strings. The string scanner supports:
- ASCII strings
- UTF-16LE strings converted to UTF-8
- packed strings inside compressed snapshots
- snapshot cluster/data windows
- readable sections ending in `.__const`, `.__cstring`, `.rodata`, or
`.data.rel.ro`
Strings are classified as:
- `rnt` for Dart runtime-looking strings (`dart:`, `dartvm`, `dart/`)
- `lib` for package/library-looking strings (`package:`, `.dart`, slash paths)
- `app` for human text with spaces or punctuation
- `unknown` otherwise
String scans are bounded: 64 MB per snapshot region, 8 MB per data-image window,
32 MB per section, and 50,000 total strings.
### Xrefs
`-x` combines metadata, data-image scans, strings, classes, and instruction
table entries into cross-reference records.
Supported xref kinds include:
- `class.name`
- `class.library`
- `class.super`
- `field.owner`
- `field.name`
- `field.type`
- `method.owner`
- `method.name`
- `method.entry`
- `it.code`
- `it.stub`
Origins are reported as `metadata`, `data-image`, or `scan`.
### Obfuscation Maps
The obfuscation map loader expects a JSON array of alternating original and
obfuscated strings. It builds an inverse map from obfuscated name to original
name, then applies it to:
- function names
- instruction-table names
- class names
- field names
- method owner/name strings
- dotted names component by component
## CLI Support
`bin/r2flutter` supports one action per invocation.
| Flag | Support |
|------|---------|
| `-h` | Help. |
| `-V` | Version string. |
| `-j` | JSON output where the selected action supports it. |
| `-r` | radare2 command/script output where the selected action supports it. |
| `-q` | Suppress non-essential stdout/log noise. |
| `-v`, `-vv` | Increase stderr diagnostics; `-vv` also prints extra snapshot/header bytes. |
| `-c` | Dump classes. |
| `-f` | Dump recovered functions as `addr name`; with `-j`, emits objects with `addr`, `name`, and optional `size`. |
| `-H` | Dump snapshot header/layout/container information. |
| `-i` | Dump instruction-table entries. |
| `-R` | Dump a radare2 script for applying method flags/comments and PP helpers. |
| `-s` | Dump strings. |
| `-T` | Dump type-oriented class output and run enum recovery. |
| `-x` | Dump metadata/data-image xrefs. |
| `-l <N>` | Limit function or instruction-table/xref output depending on the action. |
| `-n` | Enable heuristic name-pool fallback for otherwise unnamed functions. |
| `-o <file>` | Load a Flutter obfuscation map JSON file. |
Directory inputs are resolved as Android first (`libapp.so`), then iOS
(`Frameworks/App.framework/App`).
## radare2 Plugin Support
The core plugin command is `r2flutter`. Running it with no flags analyzes the
current file and applies flags/comments.
Config keys:
- `r2flutter.mapfile`: Flutter obfuscation map JSON path
- `r2flutter.namepool`: enable heuristic name-pool fallback
Plugin commands:
| Command | Support |
|---------|---------|
| `r2flutter` | Analyze Dart snapshot, apply method flags/comments, and set `e emu.str=true`. |
| `r2flutter -a` | Run Dart-aware code analysis and recover refs/comments. |
| `r2flutter -H` | Print snapshot header info. |
| `r2flutter -j` | Analyze quietly and print one JSON snapshot header line. |
| `r2flutter -i` | Print instruction-table entries. |
| `r2flutter -r` | Print radare2 script output. |
| `r2flutter -f [n]` | List first `n` discovered functions, default 20. |
| `r2flutter -q` | Analyze quietly. |
| `r2flutter -n` | Analyze with name-pool fallback enabled. |
| `r2flutter -c` | Dump classes as JSON. |
| `r2flutter -C` | Dump classes as radare2 type definitions. |
| `r2flutter -F` | Analyze with field extraction enabled. |
| `r2flutter -s` | Dump strings as JSON. |
| `r2flutter -t` | Dump strings as radare2 commands. |
| `r2flutter -x` | Dump xrefs as text. |
The Dart-aware analysis pass creates or reuses functions at recovered Dart
entrypoints, tracks AArch64 register values, follows PP-relative loads through
`x27`, propagates simple stack values, annotates direct string/class/type/field
refs, adds call xrefs, and comments indirect calls through PP slots.
## radare2 Script Output
The `-R` path emits:
- `e emu.str=true`
- `app.base` and `app.heap_base` flags
- `method.*` flags for recovered functions
- comments at method entrypoints
- `PP=x27` and PP offset helper flags found by disassembling functions and
looking for loads from `[x27, imm]`
## Practical Limits
- The implementation is ARM64-oriented. Header and string extraction may work
on other containers if radare2 maps the snapshots, but instruction-table
naming, PP analysis, and code comments assume AArch64 conventions.
- Modern cluster name reconstruction currently requires compressed
ObjectHeader snapshots. Full-width ObjectHeader snapshots rely more heavily
on data-image and string heuristics.
- Legacy cluster parsing uses conservative decoders and skips unknown clusters;
when class/function records cannot be trusted, the code falls back to bounded
scans.
- `-n` can produce plausible but wrong names. It is a last-resort fallback for
samples where metadata cannot associate names with instruction-table slots.
- This is not a Dart decompiler and does not parse Dart bytecode, kernel
`.dill`, Flutter web JavaScript, or the Flutter engine itself.
- Snapshot discovery prefers symbol names, then section scanning. Stripped or
unusual packers can still require manual triage with `-v`, `-vv`, `-H`, and
`-i`.