Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions doc/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions include/r2flutter/dart_r2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions src/lib/dart_obf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 34 additions & 0 deletions src/lib/dart_pool_snapshot.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
5 changes: 5 additions & 0 deletions src/tool/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions test/db/extras/r2flutter-help
Original file line number Diff line number Diff line change
Expand Up @@ -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 <addr|pp+off> Decode Dart tagged/object pointer or ObjectPool PP slot
-p[jr*] Print reconstructed ObjectPool PP value
-R Print radare2 script for snapshot analysis
Expand All @@ -46,7 +47,6 @@ Usage: r2flutter [j*] <action>
| 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
Expand Down Expand Up @@ -74,7 +74,6 @@ Usage: r2flutter [j*] <action>
| 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
Expand Down
Loading