Skip to content

v0.1.4

Choose a tag to compare

@github-actions github-actions released this 26 Sep 23:02
· 36 commits to main since this release
v0.1.4
46becc3

Real-device Android support

This is the release that makes dae work on the artifacts people actually reverse: compressed-pointer arm64 libapp.so from shipping Flutter apps. v0.1.3 was tagged before that work was committed, so the published binaries did not have it — this release does.

21 compressed-pointer SDK profile variants (2.13.4 → 3.14β) ship embedded and are selected automatically from the snapshot's own features string; no new flags, no guessing. Compressed builds differ structurally, not just in pointer width: they have no ROData clusters at all (strings, PcDescriptors, CodeSourceMap and CompressedStackMaps become filled clusters), instance field slots are counted in pointer-width units so the slot count is nfo − 2, the VM-side string cluster omits the canonical-set trailer that the isolate-side one writes, and the data image stays 64-aligned.

Verified against five real Android apps. The metric is the instructions-table entry count, which is the strongest available check when a binary carries no symbols — and it matches aotopsy exactly on every one:

App Dart Table entries dart analyze errors
Reqable 3.3.4 57,960 —
Lark (飞书) 3.6.1 79,327 0
ChatGLM 3.11.6 30,782 —
CHSI (学信网) 3.7.2 19,752 —
Weibo (微博) 2.19.6 22,623 0

All five at warnings 0. Lark and Weibo were also decompiled end to end: 3,517 and 19,053 function blocks, 95.9% and 91.1% structured, 1 unmapped line each, and the emitted Dart analyses with zero errors (the remaining diagnostics are unused_local_variable / dead_code warnings, see below).

Five parse bugs fixed, each against SDK source

Four of these came from the profile generator remapping the 3.3.4 baseline onto older versions by class id alone, which silently carried fields those versions never wrote:

  • ObjectPool resync discarded a correct value. The resync heuristic fires above 100,000 entries — a threshold chosen for drifted 2.10–2.14 samples. Lark's 25.6 MB build has a genuine 105,214-entry pool, so the heuristic threw it away and guessed 5,640. The alloc and fill passes each read that length independently, so when they agree the position is provably sound; resync now stands down in that case.
  • TypeParameter writes int32 + 3× uint8 up to 2.19, int32 + 2× uint16 + uint8 in 3.0, and 2× uint16 + uint8 from 3.2 on. (Write<uint8_t> is a genuine raw byte; Write<uint16_t>/Write<int32_t> go through Raw<2>/Raw<4> into a varint.) Reading a varint where a raw byte sits is fatal, because the signed-varint reader only stops at a byte with the high bit set.
  • Function: packed_fields_ moved inside if (kind != kFullAOT) in 2.18, so AOT snapshots carry one trailing varint from 2.18 on, not two.
  • SubtypeTestCache: num_inputs/num_occupied were added in 3.2.0.
  • FfiTrampolineData: ffi_function_kind_ was added in 3.2.0.

Gates that cannot pass without measuring

A gate that reports success when it never looked at anything is worse than no gate, and this repo had three:

  • dart analyze on a missing directory exits 64 with usage text containing no error - lines. Both analyze-based gates parsed that as "0 errors" and passed. They now cross-check their parse against the exit code (0/1/2 ⇒ none, 3 ⇒ at least one) and require dart's own summary line, and each carries a *_rejects_directory_it_never_analyzed test.
  • dart_valid asserted only that the error count was zero, so a regression that emitted nothing while exiting 0 scored files=0, errors=0 and passed. Every corpus must now produce files and functions.
  • Five of the six gate files consume gitignored corpora and skip themselves when those are absent, while cargo test swallows the notice — a fresh clone reported a green suite having measured almost nothing. DAE_REQUIRE_GATES=1 cargo test --release turns any such skip into a hard failure.

New gate: tests/source_truth.rs compiles tests/fixtures/truth.dart with the local dart, decompiles it, and checks the result against the source — the first gate whose input is source code rather than a self-consistency property or a .symtab diff. DAE_TRUTH_ANDROID=1 runs the same battery on a compressed-pointer arm64 build.

Also new: a collapse detector. A drifted parse used to announce itself only as libraries=1 / classes=1 with warnings=0; hello_2.18.1.aot sat at classes=2 (healthy is ~320) unnoticed. FUNC_FLOOR now fails any corpus sample recovering fewer than 400 functions, with KNOWN_COLLAPSED as an explicit registry.

Where a change could be adjudicated, it was adjudicated against evidence dae did not produce — .symtab, or aotopsy's counts — never against dae's own regression archives. Adding hello_2.19.6 to the ground-truth corpus shows why: 630 → 1,318 recovered functions, 558 → 1,081 name agreements, addresses 100% both ways.

Performance and memory

  • Dominator computation stored a full dominator set per block — O(n²) memory, ~2.6M set nodes for the 1608-block functions in this corpus — and rebuilt the predecessor list on every fixpoint iteration. It is now a standard Cooper–Harvey–Kennedy immediate-dominator array, O(n). That also fixed a real bug: intersecting with an unreachable predecessor's {self} set crushed a block's dominators and hid back edges, so loops were emitted as straight-line if/else. Detected loops on CHSI went 1173 → 1200 and gotoLabel fallbacks 4987 → 4961.
  • PoolEntry.typ was a String holding one of four values — one heap allocation per pool entry, 105,214 of them on Lark. It is a Copy enum now.
  • Measured with old and new binaries interleaved under contention, minimum of three: CHSI --decompile 160.4s → 157.9s and peak RSS 205 → 196 MB; Lark plain export 155 → 152 MB. Modest in percentage terms — the durable win is that a pathological function can no longer allocate quadratically.
  • 83 clippy warnings → 0.

Known limitations

  • Dart 2.18.1 is not usable. With the source-correct Function layout its parse collapses; the previous layout only scored better because an extra varint was compensating for a second, still-unlocated error. It is registered in KNOWN_COLLAPSED with that reason rather than left as a quietly low score. 2.15/2.16/2.17 and 2.19+ are fine.
  • WeChat 2.15.0, Tonghuashun 2.7.2 and DingTalk (custom engine) still report 0 table entries.
  • Call sites show no arguments, so the registers a caller sets up look like dead stores — that is the bulk of the unused_local_variable warnings (30,027 on Weibo). Rendering them is not honest yet: an argument register may have been written before an intervening call, and dae has no verified per-ABI clobber list to prove liveness. Attempted and reverted; the source-truth fixture caught it.
  • Everything is still dynamic. Field and return types are recoverable — the type ref is already read and discarded, and type_cids already maps a type ref to a class id — but that is not wired up yet.

Install

cargo install dae-rs          # crate name is dae-rs; the binary is `dae`
brew install ejfkdev/tap/dae
scoop bucket add ejfkdev https://github.com/ejfkdev/scoop-bucket; scoop install dae

Or grab a binary for Linux / macOS / Windows (x64 and arm64) below.