r2 flag names are unique, so flagging a second function with a repeated
Dart name (e.g. Native._anon_closure) moved the flag away from the first
one, and current r2's af then named it after the Mach-O sym.func.* flag.
Suffix repeats with their address, use the same names for afs, and rename
analyzed functions after the af pass.
Also skip var recovery, call-following and jump tables in the signature
af pass (AuthPass -AAA 47s -> 29s) and raise R2R_TIMEOUT to 60s.
cs_read_tagged64 biased the final byte by -64 instead of -192, adding
128 << shift to every Mint/Smi value, which inflated field offsets and
decoded constants.
Map each Function's code index (a raw InstructionTable slot, including
discarded-Code slots) to its entry address. Class-derived names only
replace placeholder names when merged into the function list, so
InstructionTable names stay authoritative for deduplicated code.
modern_can_extract_classes only accepted OBJECT_HEADER (3.4+) snapshots,
so CID_SHIFT1 apps such as Dart 2.19 fell back to the string heuristic
after the legacy parser failed. Gate on modern_supported instead.
x22 caches Object::null() program-wide; the VM places null/true/false as
adjacent heap objects, so the compiler materialises booleans with add off
NULL_REG (true = null+0x20, false = null+0x30 on arm64, constant across Dart
versions and pointer compression). The scan now comments add x22,#0x20/#0x30
as true/false, mov from x22 as null, and compares against x22 as null checks -
the primitive the certificate-pinning bypass keys on. Verified on iOS
(cws=8) and Android (cws=4).
x27/PP is the only Dart reserved register with a static address (the pool is
in the snapshot). r2flutter -A now seeds anal.gp with the pool base,
anal.gpseed=x27, anal.roregs=x27,gp,zero and an 'f PP' flag, so plain-r2
analysis/ESIL resolves [x27, off] pool loads - matching what -p -r already
set up. Resolution runs on a shallow DartCtx copy so the per-command read
cache (ctx->rmem) is not shared into the analysis DartApp and double-freed.
The tracker marked the destination of a pool load as a PP pointer, so a later
[dst, field] deref was read as another slot: ldr x30,[x27,0x18] (a Code
object) then ldur x30,[x30,7] (entry-point) produced a bogus PP slot +0x1f,
and the blr reported 'indirect call via PP+0x1f'. Now:
- pp_load tags the dest with has_pp_slot/pp_slot (the slot it came from),
never has_pp_off;
- flutter_process_pp_slot_deref carries that taint across the entry-point
deref so the blr names the real slot and can xref it;
- a hard guard rejects any pp_off & 7, since valid slots are always
word-aligned ((index+2)*8), killing every misaligned +0x..7 bogus comment.
After -AAA, non-string PP slots were left as bare 'dart: PP slot +0x..'
comments even though -O resolves them by hand. Resolve every slot up front
with modern_collect_direct_pool_refs and, in the analysis:
- inline the resolved name for function/class slots,
- add a navigable xref to the entrypoint for code/function slots (CALL for
blr, DATA otherwise), mapping code_index through the instruction table,
- label every remaining slot by its kind, so none stays a bare offset.
On a 37,863-function Dart 3.13 iOS app `r2flutter -AAA` took 19 minutes
(1,136s). Three costs grew with snapshot size:
- read_mem cached a single 1 MB window. modern_load_rodata_strings
alternates between a cluster alloc stream and the RO data image
megabytes away, so each string refilled the window twice (a 1 MB read
plus the 0xff fill r_io_bank_read_at does first). Keep four windows
with LRU eviction. Move the cache behind a pointer so DartCtx copies
(flutter_model_load, dart_recovery_model_load) share buffers together
with their addresses instead of leaving the original with stale ones,
and drop a window whose refill failed since the failed read already
overwrote it. `-f` 12.4s -> 0.5s; -c/-i/-T/-S ~6s -> <0.25s.
- afs stores prototypes through r_anal_save_parsed_type, which calls
r_anal_remove_parsed_type for each declared type. For an existing name
that sorts and scans the whole type database although r_type_del has
already removed every key of a function type. Names repeat often:
anonymous closures, and names the C parser cuts at ':' (dyn:call is
stored as `call`). Parse the prototype with r_anal_cparse and
r_type_del those names first so afs takes its early return. The
signature pass went from 1,127s to 27s.
- With anal.trycatch (the radare2 6.2.2 default) every af walks all flags
twice for try.<addr>.catch|filter. After -AAA creates its flags that is
O(flags) per function. Disable it around r2flutter's af loops when no
try.* flag exists, which cannot change the result.
-AAA now takes 36s on that app (35s -> 6s on a smaller 3.13 build). A
dump of afl, afs @@F, f, CC and ax after -AAA is byte-identical to the
previous build on both apps (796,194 and 138,032 lines), and so is stderr.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KmxPHTVwSkr2HTHJHQsord
- Support Dart 3.13 combined VM snapshots
- Add CID kinds and layouts for LocalVarDescriptors, ApiError, and UnwindError
- Fix stream desync causing "PP not resolved" in `r2flutter -p`
- Skip zero-length ObjectPools when resolving PP slots
- Replaces the hardcoded 'v3.9.2 defaults' fallback
- Fix load_layout_from_json to use that profile on the -f/-A decode path for
unknown hashes, instead of a degenerate zeroed layout (cws=0/alignment=0);
- Adds a version_source field to -H, guessed layout is never exact match now
* Make -p (and cluster-stream walkers) fast: cache reads, skip RBin strings
r2flutter -p burned ~100% CPU for several seconds on large libapp.so/App
images. Two bottlenecks were responsible:
1. read_mem() went through the full radare2 IO stack (banks, caches,
interval trees, mmap seek/lseek) once per byte, because the cluster
stream decoders read one byte at a time while walking the whole
modern_parse_cluster_meta stream twice. read_mem() now serves reads from
a forward-sliding 1 MiB window cached in DartCtx, collapsing millions of
IO calls into a handful. The cache is scoped to a single command and
freed in dart_obf_fini. A request is only served when fully contained in
the window; a failed windowed refill falls back to an exact read, so
semantics are unchanged.
2. The standalone tool let r_core_bin_load run RBin's whole-file string
scan (the single largest profile entry) even though r2flutter never uses
RBin's string list. The CLI now sets bin.strings=false before loading.
The core plugin path is untouched.
On test/bins/android/mafia/libapp.so (14 MB), -p drops from ~2.6s to
~0.05s (~50x) with byte-identical output across all actions; the shared
read cache also speeds up the heavier -x/-z walkers. Full custom
testsuite (35/35) still passes.
- Consolidate all version metadata into offsets.json as the single source
of truth (hashes, version profiles, offset arrays)
- Remove CID fields (cid_class, cid_function, cid_code, cid_string, ...)
from DartVerLayout struct — all CID lookups now go through dart_cid.c's
cid_tables[] via dart_cid_get()
- Remove CID fast-path switch from dart_cid_get(); always use cid_tables[]
- Replace l->cid_* references in dart_pool_parse.c and dart_pool_names.c
with dart_cid_get(l, DART_CID_*) calls
- Remove CID fallback assignments from dart_pick_layout_by_hash()
- Delete obsolete scripts/derive_offsets.py and scripts/gen_offsets.sh
- Add `make gen` target to regenerate C files from offsets.json
- Update doc/learn.md, doc/support.md, INFO.md, and test expectations
- Update snapshot hash mapping for Dart 3.11.5, 3.12.0, and 3.12.1.
- Add CID and layout profiles for the new versions.
- Document that snapshot hashes are shared across patch-release groups.
- Update documentation and test expectations.