mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
* fix(flutter): reject ambiguous mappings and bound ELF notes Resolve PT_LOAD interpretations once for symbols and dynamic tables, including partial overlaps. Preserve equivalent mappings and leave conflicting or invalid alternate definitions unresolved. Bound distinct nonempty ELF note ranges before retention and reads, deduplicate shared ranges, preserve observed build-ID candidates and report partial coverage on exhaustion. Correct shared verifier workspace ownership and structured Windows path assertions. Addresses the two post-merge review findings on PR #1582. Validation: 147 focused cases across Linux, macOS Sonoma 14.8.9 and non-administrator Windows 11 build 26300.9457; real Gallery CLI/MCP checks on all three hosts; six compiler-produced ELF32/ELF64 hash-layout cases; static, docs, discovery and duplication checks. Full string-pool projections remain explicitly bounded. * test(flutter): parse complete facts through named result schemas --------- Co-authored-by: me <me@medz.org> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
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.
|