From c71eab3402130f32115445d437df52ea00fdf73e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 16:52:26 +0000 Subject: [PATCH 1/2] 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. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RzwE1yCFnmG27vED2CpkR3 --- doc/learn.md | 27 +++++++++++++++++++++++++++ include/r2flutter/dart_r2.h | 7 +++++++ src/lib/dart_obf.c | 6 ++++++ src/lib/dart_pool_snapshot.c | 34 ++++++++++++++++++++++++++++++++++ src/tool/main.c | 5 +++++ 5 files changed, 79 insertions(+) diff --git a/doc/learn.md b/doc/learn.md index e299129..d342850 100644 --- a/doc/learn.md +++ b/doc/learn.md @@ -1166,3 +1166,30 @@ ObjectPool/code xref scan found code-to-string xrefs for nearby serialized strings, but no code-reachable ObjectPool path to ref `6433`. Treat that as "present in the snapshot string cluster" rather than a proven code reference until another metadata container or instruction pattern links to it. + +## Performance: `-p` was CPU-bound on per-byte IO and RBin string scanning + +`bin/r2flutter -p` (and every cluster-stream walker) used to burn ~100% CPU for +several seconds on large `libapp.so` / `App` images. Two bottlenecks: + +1. `read_mem()` issued one `r_io_read_at()` through the full radare2 IO stack + (banks, caches, interval trees, mmap seek/lseek) for *every byte*, because + the cluster stream decoders (`cs_read_u8`, `cs_read_ref_id`, + `cs_read_tagged32/64`, `cs_read_unsigned`) read one byte at a time while + walking the whole `modern_parse_cluster_meta` stream twice. `read_mem()` now + keeps a forward-sliding 1 MiB window cache in `DartCtx`, collapsing millions + of IO calls into a handful. It is scoped to a single command invocation and + released in `dart_obf_fini()`. The window is only served when the whole + request is contained in it; a windowed refill that fails (e.g. near the end + of a mapped region) falls back to an exact read, so semantics are identical. + +2. `r_core_bin_load()` in the standalone tool ran RBin's `string_scan_range` + over the entire binary at load time (the single largest profile entry). + r2flutter never consumes RBin's string list — it has its own Dart-aware + string extraction — so the CLI now sets `bin.strings=false` before loading. + The r2 core plugin path is left untouched (it runs inside the user session). + +Net effect on `test/bins/android/mafia/libapp.so` (14 MB): `-p` dropped from +~2.6s to ~0.05s (~50x), with byte-identical output across all actions and the +whole custom testsuite still green. The shared read cache also speeds up the +heavier `-x`/`-z` walkers. diff --git a/include/r2flutter/dart_r2.h b/include/r2flutter/dart_r2.h index 031643d..515a4ac 100644 --- a/include/r2flutter/dart_r2.h +++ b/include/r2flutter/dart_r2.h @@ -79,6 +79,13 @@ typedef struct { const char *obf_map_path; HtPP *obf_by_obfuscated; bool obf_map_tried; + // Windowed read cache for read_mem, to avoid one r_io_read_at syscall per + // byte while decoding cluster streams (which read one byte at a time). + // Scoped to a single command invocation and released by dart_obf_fini. + ut8 *rmem_cache; + ut64 rmem_cache_addr; + int rmem_cache_len; + int rmem_cache_cap; } DartCtx; #endif diff --git a/src/lib/dart_obf.c b/src/lib/dart_obf.c index 656d224..01b0078 100644 --- a/src/lib/dart_obf.c +++ b/src/lib/dart_obf.c @@ -101,6 +101,12 @@ void dart_obf_fini(DartCtx *ctx) { ht_pp_free (ctx->obf_by_obfuscated); ctx->obf_by_obfuscated = NULL; ctx->obf_map_tried = false; + // Release the read_mem window cache (scoped to one command invocation). + free (ctx->rmem_cache); + ctx->rmem_cache = NULL; + ctx->rmem_cache_addr = 0; + ctx->rmem_cache_len = 0; + ctx->rmem_cache_cap = 0; } char *dart_obf_resolve(DartCtx *ctx, const char *name) { diff --git a/src/lib/dart_pool_snapshot.c b/src/lib/dart_pool_snapshot.c index 67c1e7e..23cc1a8 100644 --- a/src/lib/dart_pool_snapshot.c +++ b/src/lib/dart_pool_snapshot.c @@ -2,10 +2,44 @@ #include "dart_pool_parse_priv.h" +// Window size for the read cache. Cluster streams are decoded one byte at a +// time, so serving those reads from a prefetched window turns millions of +// r_io_read_at calls into a handful. +#define RMEM_WINDOW (1024 * 1024) + bool read_mem(DartCtx *ctx, ut64 addr, void *buf, int len) { if (!ctx || !ctx->core || !buf || len <= 0) { return false; } + // Fast path: request fully contained in the cached window. + if (ctx->rmem_cache && addr >= ctx->rmem_cache_addr) { + ut64 off = addr - ctx->rmem_cache_addr; + if (off + (ut64)len <= (ut64)ctx->rmem_cache_len) { + memcpy (buf, ctx->rmem_cache + off, (size_t)len); + return true; + } + } + // Reads larger than the window bypass the cache. + if (len > RMEM_WINDOW) { + return r_io_read_at (ctx->core->io, addr, (ut8 *)buf, len); + } + if (!ctx->rmem_cache) { + ctx->rmem_cache = malloc (RMEM_WINDOW); + if (!ctx->rmem_cache) { + return r_io_read_at (ctx->core->io, addr, (ut8 *)buf, len); + } + ctx->rmem_cache_cap = RMEM_WINDOW; + } + // Refill the window at addr. r_io_read_at only succeeds when the whole + // range is readable, so a successful window read means every byte in it is + // valid and safe to serve. On failure (e.g. near the end of a mapped + // region) fall back to an exact read and keep any previous window intact. + if (r_io_read_at (ctx->core->io, addr, ctx->rmem_cache, ctx->rmem_cache_cap)) { + ctx->rmem_cache_addr = addr; + ctx->rmem_cache_len = ctx->rmem_cache_cap; + memcpy (buf, ctx->rmem_cache, (size_t)len); + return true; + } return r_io_read_at (ctx->core->io, addr, (ut8 *)buf, len); } diff --git a/src/tool/main.c b/src/tool/main.c index a8bde47..dbe9d53 100644 --- a/src/tool/main.c +++ b/src/tool/main.c @@ -253,6 +253,11 @@ int main(int argc, char **argv) { free_resolved_path (libapp_path, extracted_inner); return 1; } + // r2flutter does its own Dart-aware string/symbol extraction and never + // consumes RBin's string list, so skip the costly RBin string scan that + // r_core_bin_load runs over the whole binary (dominant load-time cost on + // large libapp.so / App images). + r_config_set_b (core->config, "bin.strings", false); r_core_bin_load (core, NULL, 0); dctx.core = core; From 9b635ccccc6f9de7b430a239ba4f5405e2a76341 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 17:23:22 +0000 Subject: [PATCH 2/2] Fix db/extras/r2flutter-help expected output The help test drifted from the actual program output and failed CI: - Standalone usage (-h) gained the "(-ii same as -i)" note on -i and a new "-ie[jr*], -E[jr*] Print Dart code entrypoint" line that the EXPECT block never picked up. - The core plugin help lists -h once (grouped with -v/-V), but the EXPECT had a duplicate "-h show this help" line in the middle of the alphabetical list, in both plugin-help blocks. Regenerated the expected output to match the current binaries; all three db/extras tests pass under r2r. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RzwE1yCFnmG27vED2CpkR3 --- test/db/extras/r2flutter-help | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/db/extras/r2flutter-help b/test/db/extras/r2flutter-help index 8a843dc..ed8cfd3 100644 --- a/test/db/extras/r2flutter-help +++ b/test/db/extras/r2flutter-help @@ -25,7 +25,8 @@ Actions: -c[jr*] Print extracted class information -f[jr*] Print all extracted functions (addr name) -H[HH] Print Dart AOT snapshot header info - -i[jr*] Print instruction table entries + -i[jr*] Print instruction table entries (-ii same as -i) + -ie[jr*], -E[jr*] Print Dart code entrypoint -O Decode Dart tagged/object pointer or ObjectPool PP slot -p[jr*] Print reconstructed ObjectPool PP value -R Print radare2 script for snapshot analysis @@ -46,7 +47,6 @@ Usage: r2flutter [j*] | r2flutter -D prof override Dart snapshot profile by hash or version | r2flutter -f[j*] dump recovered functions | r2flutter -H[HH] dump Dart AOT snapshot header info -| r2flutter -h show this help | r2flutter -i[j*] dump instruction table entries | r2flutter -ie[j*] dump Dart code entrypoint; with -* mark instruction snapshots as dword arrays | r2flutter -l N limit function/instruction-table/xref output @@ -74,7 +74,6 @@ Usage: r2flutter [j*] | r2flutter -D prof override Dart snapshot profile by hash or version | r2flutter -f[j*] dump recovered functions | r2flutter -H[HH] dump Dart AOT snapshot header info -| r2flutter -h show this help | r2flutter -i[j*] dump instruction table entries | r2flutter -ie[j*] dump Dart code entrypoint; with -* mark instruction snapshots as dword arrays | r2flutter -l N limit function/instruction-table/xref output