190 lines
11 KiB
Markdown
190 lines
11 KiB
Markdown
# Flutter build identification
|
|
|
|
REA identifies a Flutter app's build facts by **pure parsing** of its APK:
|
|
the Dart VM snapshot hash, per-ABI library digests, the engine's GNU
|
|
build-id, and any labeled version strings. No engine, no tool installation,
|
|
no execution, and no external lookups — this family answers "which Dart SDK
|
|
build is this, and what exactly is in the payload?" before any deeper
|
|
native analysis.
|
|
|
|
This is the triage step for Flutter reversing: the snapshot hash is the
|
|
artifact that maps to a Dart SDK release through an external dataset (such
|
|
as the one reFlutter maintains), and knowing the SDK version is what makes
|
|
SDK-layout-specific snapshot parsers applicable.
|
|
|
|
## Answer an analyst question
|
|
|
|
```sh
|
|
rea identify-flutter-build /targets/Example.apk
|
|
rea inspect-dart-aot /targets/Example.apk
|
|
rea inspect-dart-aot /targets/Example.apk --abi armeabi-v7a
|
|
```
|
|
|
|
The equivalent MCP methods are `identify_flutter_build` (with `path`) and
|
|
`inspect_dart_aot` (with `path` and an optional `abi`; the default prefers
|
|
arm64-v8a, then x86_64, then x86, then armeabi-v7a, then the remaining
|
|
ABIs alphabetically).
|
|
|
|
## Optional retained AOT delivery
|
|
|
|
MCP `inspect_dart_aot` keeps complete output when `detail` is omitted or
|
|
`"complete"`. Explicit `detail: "summary"` returns the selected target/ABI/library
|
|
identity, all snapshot headers, structural coverage, string-pool coverage and
|
|
original lower-bound counts without the string lists. It retains complete source
|
|
Evidence and returns a distinct linked `inspect_analysis_view` summary. Analysis
|
|
and the caller's ABI choice are unchanged; one-shot CLI output stays complete.
|
|
|
|
Use `normalized_result.parent_evidence_id` as the retained source for
|
|
`inspect_analysis_view`, with `view: {kind: "dart-aot", collection: "package_uris",
|
|
offset: 0, limit: 64}`. Collections are `package_uris`, `dart_uris` and
|
|
`class_like_tokens`. Pages preserve the original strings and order, selected ABI,
|
|
library identity and source coverage. `returned_counts` describe producer-returned
|
|
rows; counts ending in `_at_least` remain lower bounds. Exhausting a page sequence
|
|
does not make a partial pool complete or turn byte-pattern tokens into typed
|
|
classes/functions. Views do not reopen the APK. Original Evidence remains
|
|
exportable under its existing session lifetime. Summary requires a retaining
|
|
server; older servers keep the complete request after schema discovery.
|
|
|
|
## What the AOT inspection returns
|
|
|
|
`inspect_dart_aot` reads one ABI's `libapp.so` in full and reports:
|
|
|
|
- **Snapshot sections** — the four `_kDart*Snapshot*` symbols located
|
|
through the ELF **dynamic symbol table** (ELF32 and ELF64, little-endian,
|
|
every read bounds-checked), each with file offset, size, and — for data
|
|
sections, which carry the `f5f5dcdc` magic — the parsed header kind and
|
|
length (the stored length excludes the four magic bytes). Data headers
|
|
whose declared snapshot length does not fit their symbol retain their
|
|
observed fields and report partial coverage. Complete structural coverage
|
|
currently requires kind 3, the FullAOT value in the supported
|
|
[Dart 2.14 layout](https://github.com/dart-lang/sdk/blob/2.14.0/runtime/vm/snapshot.h).
|
|
Kind numbers vary across Dart releases; other values are preserved with
|
|
partial coverage, without guessing their interpretation.
|
|
Address resolution examines every overlapping `PT_LOAD` interpretation,
|
|
including partial overlaps. Equivalent mappings are coalesced; conflicting
|
|
mappings leave affected symbols unresolved and qualify coverage. Dynamic
|
|
hash, symbol and string tables use the same mapping checks.
|
|
A payload without exported snapshot symbols is refused rather
|
|
than guessed at.
|
|
- **The snapshot hash** from the header at the start of each exported data
|
|
symbol. Complete hash coverage requires a valid header from both the VM
|
|
and isolate; duplicate records inside one symbol cannot stand in for the
|
|
other's header. Aliased or overlapping data symbols report partial coverage.
|
|
- **A string-pool projection from the isolate data section**: distinct
|
|
`package:` source URIs (the app's own and its dependencies' Dart files —
|
|
a dependency inventory), `dart:` SDK URIs (which SDK libraries the app
|
|
uses), and identifier-shaped tokens, each bounded with counts and
|
|
`coverage: "partial"` when truncated. On the reference target this counts
|
|
756 package URIs and 201 dart URIs on arm64-v8a and x86_64, and 212 / 48
|
|
on armeabi-v7a, retaining up to 500 of each in the response.
|
|
`libapp.coverage` describes structural snapshot checks;
|
|
`string_pool.coverage` describes the bounded projection. Overall coverage
|
|
is partial when either facet is partial.
|
|
|
|
## Interpret the result
|
|
|
|
- **`flutter_detected`** is true when any `lib/<abi>/libapp.so` exists. A
|
|
negative answer reports complete coverage: absence is an answer.
|
|
- **`snapshot_hash`** is the Dart build hash observed 20 bytes after the
|
|
snapshot magic (`f5 f5 dc dc`) inside `libapp.so`. Identification scans
|
|
byte patterns; AOT inspection reads the exported VM and isolate headers.
|
|
`snapshot_hash_sources` counts the valid records observed by the chosen
|
|
operation, and when they **disagree** the distinct values are
|
|
kept in `snapshot_hash_candidates` with `snapshot_hash: null` and
|
|
`coverage: "partial"` rather than silently picking one.
|
|
- **`elf_header_valid`** checks the supported little-endian ELF32/ELF64
|
|
header, table entry widths, and full table extents against the library's
|
|
actual byte length. Truncated, malformed, or unsupported headers report
|
|
false and partial coverage while retaining byte-scanned hashes. This
|
|
header check does not establish valid table contents or exported symbols.
|
|
- **`libflutter` facts** are per-ABI observations: byte size, SHA-256
|
|
digest, GNU build IDs from ELF-declared `PT_NOTE` / `SHT_NOTE` ranges,
|
|
and any _labeled_ strings the build
|
|
carries. Release engine builds often strip the `Dart VM version` string —
|
|
absence is reported as `null`, not guessed. Android clang/LLVM toolchain
|
|
lines are reported verbatim as build provenance.
|
|
`build_id_candidates` preserves distinct IDs. `build_id` is populated only
|
|
when all declared note ranges were examined and one ID was found;
|
|
conflicting IDs or malformed/bounded note metadata make coverage partial.
|
|
Matching bytes outside declared note ranges cannot supply a build ID.
|
|
- Labeled strings must fit their complete grammar. `dart_version_capped`
|
|
and `toolchain_capped` independently report labels omitted by grammar or
|
|
projection bounds; either makes identification coverage partial. Values
|
|
split across read chunks are retained without publishing truncated prefixes.
|
|
- **`coverage: "partial"`** also appears when an ABI carries `libapp.so`
|
|
without `libflutter.so` (or vice versa).
|
|
- The APK itself is digested before parsing, so `target.sha256` binds the
|
|
observation to the exact input.
|
|
|
|
## Limits
|
|
|
|
- **Owned snapshots.** Both operations copy the admitted APK into a
|
|
provider-owned, read-only snapshot before digesting and parsing, so a
|
|
concurrently modified original cannot mix into one observation; failed
|
|
reader closure and snapshot cleanup are retained and retried on `close()`.
|
|
The snapshot is removed only after all owned readers and file descriptors
|
|
close successfully. Copying pins one admitted descriptor and compares its
|
|
identity, size, mode and timestamps after the copy. Source permission
|
|
failures remain `access_denied`, and source identity changes remain
|
|
`artifact_changed`. Provider-owned storage failures report a
|
|
`resource_constraint` with their native I/O cause. Cancellation is checked
|
|
during copying, metadata/symbol walks, string scanning, and after cleanup.
|
|
- **Bounded reads.** APK snapshots are bounded at 2 GiB, including while
|
|
copying. Identification permits 768 MiB of cumulative library extraction;
|
|
declared sizes are checked before extraction. Library reads are bounded at 256 MiB each and ZIP
|
|
metadata at 8 MiB; the AOT image buffer is allocated at exactly the
|
|
declared size before reading. Exceeded budgets report
|
|
`resource_constraint`.
|
|
- **Engine notes.** Engine libraries are spooled into the owned root. ELF
|
|
headers, note tables and note payloads share an 8 MiB metadata budget;
|
|
up to 64 distinct build IDs and 1024 distinct nonempty note ranges are retained.
|
|
The range cap bounds bookkeeping and separate positional reads even for tiny
|
|
overlapping declarations. Exact duplicates share one read and empty ranges
|
|
consume no inventory slots. Exceeding a bound preserves the candidates examined
|
|
from retained ranges and reports partial coverage with `build_id: null`.
|
|
- **Metadata and candidate capacity.** Both operations refuse duplicate
|
|
relevant library paths. Inventory is limited to 32 ABIs and 1024 bytes per
|
|
relevant library path, independently of compressed size. Identification
|
|
refuses more than 64 distinct snapshot hashes with `resource_constraint`
|
|
rather than silently dropping conflicting evidence.
|
|
- **Selected ABI.** AOT inspection applies the library budget to the selected
|
|
ABI after selection; an oversized unselected library does not block it.
|
|
Undefined dynamic-symbol imports are not treated as snapshot exports.
|
|
Section-stripped images require a bounded symbol count from `DT_HASH` or
|
|
`DT_GNU_HASH`; layouts without either are refused. Symbol names and their
|
|
terminators must fit their linked string table.
|
|
- REA performs **no hash-to-SDK lookup**: mapping the snapshot hash to a
|
|
Dart SDK release requires an external dataset and is deliberately out of
|
|
scope for this family.
|
|
- The AOT string pool is a **byte-pattern projection, not deserialized
|
|
cluster semantics**: typed class and function recovery requires an
|
|
SDK-specific parser (the snapshot layout changes per release — exactly
|
|
why the hash is the key), and `class_like_tokens` include incidental
|
|
matches. JEB's GUI Dart unit and native analysis of `libapp.so` cover
|
|
some ground today.
|
|
|
|
## Real verification
|
|
|
|
`npm run verify:flutter -- --apk PATH [--plain-apk PATH]` runs both tools
|
|
through compiled CLI and stdio MCP, compares Evidence, and checks every ABI.
|
|
Use `--expectations scripts/fixtures/flutter-gallery-expectations.json` with
|
|
the Gallery 2.9.2 release APK to check exact per-ABI engine labels,
|
|
URI counts and the snapshot hash against its pinned SHA-256. The verifier
|
|
requires complete structural snapshot coverage on every ABI and explicitly
|
|
reports the bounded pool projection as partial/unverified when truncated.
|
|
Other APKs need their own expectations file to verify exact engine labels
|
|
and URI counts; otherwise those facets are reported unverified.
|
|
The optional plain APK checks negative detection. Record: Flutter Gallery 2.9.2 (112 MB, three
|
|
ABIs) — every ABI reports the same agreeing hash
|
|
`65817c30a78bb44c3dc3771876b6010a` from two snapshot sections, per-ABI
|
|
engine build-ids, no labeled Dart version (as expected for a release
|
|
engine), and the Android clang toolchain line; a plain aapt2-built APK
|
|
reports not-detected with complete coverage. The suite uses self-contained
|
|
synthetic ELF images and scanner payloads; it does not read private local files.
|
|
|
|
The Gallery workflow and all 147 focused Flutter tests also pass on macOS Sonoma
|
|
14.8.9 x86-64 and Windows 11 x64 build 26300.9457 guests with Node 24.18.0,
|
|
including the conflicting-load and note-range-limit CLI/MCP regressions. Windows
|
|
verification runs under a non-administrator account. Verifiers use a shared
|
|
temporary parent so each CLI/MCP process can own and release its own snapshots.
|