Fix ObjectPool string xrefs through instance fields

This commit is contained in:
pancake committed 2026-10-21 09:32:21 +02:00
1 parent 7be0c178fb
commit bd6f596279
10 files changed
+272 -41

No files matched your search

+32
View File
@@ -1883,3 +1883,35 @@ placeholder (`func.*`, `method.fn_*`) names.
came out as 128 + words, e.g. `_Closure._context` at `+0x428` instead of
`+0x28` on 64-bit, and the `SemanticsAction` const map key `tap` decoded as
129 instead of 1. `cs_read_tagged32` was already correct.
## ObjectPool instances can carry strings through their fields
`t/libapp_1.so` loads `PP+0x17200` at `0xfb4ae8`. The slot references
snapshot object 165628, a `Ubb` instance derived from `_Enum`; its string field
references object 33665 (`privilegedAccess`, payload `0xbcedf`). Blutter's
`pp.txt` confirms enum index 9 and the name at instance offset `0x10`.
A direct-String-only PP lookup consequently misses this use.
Analysis now collects direct strings and immediate string fields of pool
instances. `-AA` links the synthetic pool slot (`0x30a1b0` in this sample) to
the string, and `-AAA` links the loading instruction and adds a `contains
string` comment while retaining the `instance` label. Each slot can contain
multiple strings; they must not collapse to its first string field. This
analysis path stops at immediate fields rather than traversing metadata or
arbitrary object graphs.
Instance field counts include an eight-byte header expressed in compressed
words: two words on compressed Android snapshots, one on uncompressed iOS.
Using a fixed two-word header skips the enum name on iOS. The collector also
loads ROData strings for uncompressed snapshots and preserves VM base-object
strings. Regression tests use `Brightness.dark` in Android w0rdle and iOS
Runner to cover both layouts, pool xrefs, and instruction comments/xrefs.
PP resolution can initialize `ctx->layout` from a local stack buffer. Callers
that need to continue parsing after it returns must keep their own live
layout across resolution and free it afterward; otherwise the field-string
collector can silently miss all slots on either platform.
Instance fill offsets are indexed once per cluster while collecting strings.
Rescanning a cluster prefix for each PP slot makes large const collections
quadratic, even when only immediate fields are read.
+74 -23
View File
@@ -3501,6 +3501,8 @@ typedef struct {
ut64 pool_ref;
ut64 pool_index;
ut64 entry_index;
bool instance_fields_only;
ut64 *instance_offsets;
} ModernPoolStringRefCollector;
static void modern_emit_pool_string_ref(ModernPoolStringRefCollector *collector, ut64 target_ref, const char *target_kind, ut64 field_index, ut64 string_ref) {
@@ -3654,36 +3656,49 @@ static bool modern_collect_instance_strings(ModernPoolStringRefCollector *collec
if (!cs_read_unsigned (&s, &bitmap)) {
return false;
}
int header_words = 2;
int num_fields = meta->next_field_offset_words - header_words;
if (num_fields < 0) {
num_fields = 0;
}
ut64 target_index = target_ref - meta->start_ref;
for (ut64 i = 0; i < meta->count; i++) {
for (int j = 0; j < num_fields; j++) {
int field_word_idx = header_words + j;
bool is_unboxed = ((bitmap >> field_word_idx) & 1ULL) != 0;
if (is_unboxed) {
const int word_size = resolver->ctx->compressed_word_size? resolver->ctx->compressed_word_size: 4;
const int header_words = 8 / word_size;
const int num_fields = R_MAX (meta->next_field_offset_words - header_words, 0);
// Index the cluster once. Rescanning its prefix for every pool slot is
// quadratic for large collections of const instances such as icons.
if (!collector->instance_offsets[meta->start_ref]) {
for (ut64 i = 0; i < meta->count; i++) {
collector->instance_offsets[meta->start_ref + i] = s.cursor;
for (int j = 0; j < num_fields; j++) {
const int field_word = header_words + j;
const bool unboxed = field_word < 64 && ((bitmap >> field_word) & 1ULL);
ut32 tmp32 = 0;
if (!cs_read_tagged32 (&s, &tmp32) || !cs_read_tagged32 (&s, &tmp32)) {
return false;
}
} else {
ut64 ref = 0;
if (!cs_read_ref_id (&s, &ref)) {
if (unboxed) {
if (!cs_read_tagged32 (&s, &tmp32) || !cs_read_tagged32 (&s, &tmp32)) {
collector->instance_offsets[meta->start_ref] = 0;
return false;
}
} else if (!cs_read_ref_id (&s, &ref)) {
collector->instance_offsets[meta->start_ref] = 0;
return false;
}
if (i == target_index) {
(void)modern_collect_ref_strings (collector, ref, "instance", (ut64)j, depth + 1);
}
}
}
if (i == target_index) {
return true;
}
s.cursor = collector->instance_offsets[target_ref];
for (int j = 0; j < num_fields; j++) {
const int field_word = header_words + j;
const bool unboxed = field_word < 64 && ((bitmap >> field_word) & 1ULL);
if (unboxed) {
ut32 tmp32 = 0;
if (!cs_read_tagged32 (&s, &tmp32) || !cs_read_tagged32 (&s, &tmp32)) {
return false;
}
} else {
ut64 ref = 0;
if (!cs_read_ref_id (&s, &ref)) {
return false;
}
(void)modern_collect_ref_strings (collector, ref, "instance", (ut64)j, depth + 1);
}
}
return false;
return true;
}
static bool modern_collect_ref_strings(ModernPoolStringRefCollector *collector, ut64 target_ref, const char *target_kind, ut64 field_index, int depth) {
@@ -3698,6 +3713,11 @@ static bool modern_collect_ref_strings(ModernPoolStringRefCollector *collector,
if (collector->seen) {
ht_up_insert (collector->seen, seen_key, (void *)1);
}
// Isolate base objects can include VM strings without a local cluster.
if (R_STR_ISNOTEMPTY (resolver->strings_by_ref[target_ref])) {
modern_emit_pool_string_ref (collector, target_ref, target_kind, field_index, target_ref);
return true;
}
const ModernClusterMeta *meta = modern_resolver_cluster_for_ref (resolver, target_ref);
if (!meta) {
return false;
@@ -3709,6 +3729,9 @@ static bool modern_collect_ref_strings(ModernPoolStringRefCollector *collector,
if (!meta->fill_parsed || !meta->fill_ok || meta->fill_offset >= meta->fill_end || !meta->count) {
return false;
}
if (collector->instance_fields_only && (depth > 0 || meta->fill_kind != MODERN_FILL_INSTANCE)) {
return false;
}
if (modern_is_array_cid (collector->cids, meta->cid)) {
return modern_collect_array_strings (collector, meta, target_ref, false, depth);
}
@@ -3729,7 +3752,7 @@ static bool modern_collect_ref_strings(ModernPoolStringRefCollector *collector,
return false;
}
bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernPoolStrRefCb cb, void *user) {
static bool modern_collect_pool_strrefs_internal(const ModernReq *req, HtUP *pp_offsets, ModernPoolStrRefCb cb, void *user, bool instance_fields_only) {
DartCtx *ctx = req? req->ctx: NULL;
if (!ctx || !cb || !modern_supported (ctx)) {
return false;
@@ -3744,6 +3767,21 @@ bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernP
modern_cluster_meta_free (meta, req->num_clusters);
return false;
}
ut64 *instance_offsets = (ut64 *)calloc ((size_t)resolver.refs_count, sizeof (ut64));
if (!instance_offsets) {
modern_ref_resolver_fini (&resolver);
modern_cluster_meta_free (meta, req->num_clusters);
return false;
}
const ut64 snapshot_bases[] = { ctx->iso_data, ctx->vm_data };
for (size_t i = 0; i < R_ARRAY_SIZE (snapshot_bases); i++) {
DartSnapshotHeader sh = { 0 };
const ut64 base = snapshot_bases[i];
if (base && dart_snapshot_header_read (ctx, base, &sh) && sh.ok && sh.cluster_start == req->cluster_start) {
modern_load_rodata_strings (ctx, meta, req->num_clusters, base, sh.total_len, resolver.strings_by_ref, resolver.string_addr_by_ref, resolver.refs_count);
break;
}
}
const ModernCidCache cids = modern_cid_cache_init (ctx->layout);
for (ut64 i = 0; i < req->num_clusters; i++) {
ModernClusterMeta *m = &meta[i];
@@ -3785,6 +3823,8 @@ bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernP
.pool_ref = m->start_ref + pool_index,
.pool_index = pool_index,
.entry_index = entry_index,
.instance_fields_only = instance_fields_only,
.instance_offsets = instance_offsets,
};
(void)modern_collect_ref_strings (&collector, entry.ref, "object_pool.entry", entry_index, 0);
ht_up_free (seen);
@@ -3797,11 +3837,22 @@ bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernP
break;
}
}
free (instance_offsets);
modern_ref_resolver_fini (&resolver);
modern_cluster_meta_free (meta, req->num_clusters);
return ok;
}
bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernPoolStrRefCb cb, void *user) {
return modern_collect_pool_strrefs_internal (req, pp_offsets, cb, user, false);
}
// Include immediate string fields of instances (notably _Enum._name) without
// walking unrelated metadata graphs or treating the instance as a String.
bool modern_collect_pool_value_strrefs(const ModernReq *req, HtUP *pp_offsets, ModernPoolStrRefCb cb, void *user) {
return modern_collect_pool_strrefs_internal (req, pp_offsets, cb, user, true);
}
static ut64 modern_find_sb(const ModernReq *req) {
DartCtx *ctx = req? req->ctx: NULL;
if (!ctx) {
+1
View File
@@ -291,6 +291,7 @@ const char *modern_value_graph_root_kind(ModernValueGraph *graph);
bool modern_collect_direct_pool_strrefs(const ModernReq *req, HtUP *pp_offs, ModernPoolStrRefCb cb, void *user);
bool modern_collect_direct_pool_refs(const ModernReq *req, ModernPoolRefCb cb, void *user);
bool modern_collect_pool_strrefs(const ModernReq *req, HtUP *pp_offs, ModernPoolStrRefCb cb, void *user);
bool modern_collect_pool_value_strrefs(const ModernReq *req, HtUP *pp_offs, ModernPoolStrRefCb cb, void *user);
bool modern_extract_pool_strings(const ModernReq *req, RList *strings, HtUP *seen, ut64 *next_ref);
bool modern_scan_names(DartCtx *ctx, ut64 start, ut64 end, ut64 nclusters, ut64 itlen);
bool modern_extract_classes(const ModernReq *req, RList *classes);
+42 -1
View File
@@ -1016,6 +1016,47 @@ char *dart_pool_dump_xrefs(DartCtx *ctx, int fmt) {
return out;
}
typedef struct {
RCore *core;
ut64 pp_base;
int applied;
} DartPoolValueXrefCtx;
static void xref_pool_value_string_cb(const ModernPoolStrRef *info, void *user) {
DartPoolValueXrefCtx *ux = user;
if (!info->string_addr || !strcmp (info->target_kind, "object_pool.entry")) {
return;
}
r_anal_xrefs_set (ux->core->anal, ux->pp_base + info->pp_offset, info->string_addr, R_ANAL_REF_TYPE_STRN | R_ANAL_REF_TYPE_READ);
ux->applied++;
}
static int apply_pool_value_xrefs(DartCtx *ctx) {
if (find_snapshots (ctx) != 0) {
return 0;
}
// Keep a live layout across PP resolution: its fallback layout can otherwise
// point at dart_resolve_pp_info's temporary stack buffer on return.
DartVerLayout layout_tmp;
bool layout_ours = !ctx->layout;
DartVerLayout *layout_owned = layout_ours? dart_ctx_init_layout (ctx, &layout_tmp): NULL;
DartPpInfo pp = { 0 };
DartPoolValueXrefCtx ux = { .core = ctx->core };
if (dart_resolve_pp_info (ctx, &pp)) {
ux.pp_base = pp.base;
DartSnapshotHeader sh = { 0 };
if (dart_snapshot_header_read (ctx, pp.snapshot_base, &sh) && sh.ok) {
const ModernReq req = { ctx, sh.cluster_start, pp.snapshot_base + sh.total_len, sh.nc, sh.nb };
modern_collect_pool_value_strrefs (&req, NULL, xref_pool_value_string_cb, &ux);
}
}
dart_pp_info_fini (&pp);
if (layout_ours) {
dart_ctx_fini_layout (ctx, layout_owned);
}
return ux.applied;
}
// Apply the recovered xref graph to the RCore as real r2 xrefs. Unlike the
// analysis disassembly scan, which only sees loads inside functions, this also
// surfaces references that live in the data image (e.g. a const object in
@@ -1052,5 +1093,5 @@ int dart_pool_apply_xrefs(DartCtx *ctx) {
}
}
dart_string_list_free (strings);
return applied;
return applied + apply_pool_value_xrefs (ctx);
}
+36 -17
View File
@@ -60,6 +60,7 @@ typedef struct {
typedef struct {
ut64 string_addr;
char *string_value;
bool indirect;
} FlutterPpStringRef;
typedef struct {
@@ -93,15 +94,19 @@ static void flutter_stack_slot_free(void *p) {
free (p);
}
static void flutter_pp_string_ref_free(HtUPKv *kv) {
FlutterPpStringRef *ref = kv->value;
static void flutter_pp_string_ref_free(void *p) {
FlutterPpStringRef *ref = p;
free (ref->string_value);
free (ref);
}
static void flutter_pp_string_bucket_free(HtUPKv *kv) {
r_list_free (kv->value);
}
static void flutter_pp_string_map_init(FlutterPpStringMap *map) {
memset (map, 0, sizeof (*map));
map->by_pp_off = ht_up_new (NULL, flutter_pp_string_ref_free, NULL);
map->by_pp_off = ht_up_new (NULL, flutter_pp_string_bucket_free, NULL);
}
static void flutter_pp_string_map_fini(FlutterPpStringMap *map) {
@@ -567,13 +572,23 @@ static void flutter_pp_string_map_add(FlutterPpStringMap *map, const ModernPoolS
if (!map || !map->by_pp_off || !info || R_STR_ISEMPTY (info->string_value) || !info->string_addr) {
return;
}
if (ht_up_find (map->by_pp_off, info->pp_offset, NULL)) {
return;
RList *refs = ht_up_find (map->by_pp_off, info->pp_offset, NULL);
if (!refs) {
refs = r_list_newf (flutter_pp_string_ref_free);
ht_up_insert (map->by_pp_off, info->pp_offset, refs);
}
RListIter *it;
FlutterPpStringRef *existing;
r_list_foreach (refs, it, existing) {
if (existing->string_addr == info->string_addr) {
return;
}
}
FlutterPpStringRef *ref = R_NEW0 (FlutterPpStringRef);
ref->string_addr = info->string_addr;
ref->string_value = strdup (info->string_value);
ht_up_insert (map->by_pp_off, info->pp_offset, ref);
ref->indirect = strcmp (info->target_kind, "object_pool.entry") != 0;
r_list_append (refs, ref);
}
static void flutter_pp_string_ref_cb(const ModernPoolStrRef *info, void *user) {
@@ -595,7 +610,7 @@ static bool flutter_collect_pp_strings_from_snapshot(DartCtx *ctx, ut64 snapshot
.num_clusters = sh.nc,
.num_base_objects = sh.nb,
};
return modern_collect_direct_pool_strrefs (&req, wanted, flutter_pp_string_ref_cb, map);
return modern_collect_pool_value_strrefs (&req, wanted, flutter_pp_string_ref_cb, map);
}
static void flutter_pp_string_map_load(DartCtx *ctx, HtUP *wanted, FlutterPpStringMap *map) {
@@ -616,7 +631,7 @@ static void flutter_pp_string_map_load(DartCtx *ctx, HtUP *wanted, FlutterPpStri
}
}
static const FlutterPpStringRef *flutter_pp_string_map_get(FlutterPpStringMap *map, ut64 pp_off) {
static RList *flutter_pp_string_map_get(FlutterPpStringMap *map, ut64 pp_off) {
return map && map->by_pp_off? ht_up_find (map->by_pp_off, pp_off, NULL): NULL;
}
@@ -1215,22 +1230,26 @@ static bool flutter_process_pp_load(RCore *core, FlutterAnalState *state, Flutte
if (pp_refs && pp_refs->pp_base) {
flutter_add_ref (core, at, pp_refs->pp_base + slot_off, R_ANAL_REF_TYPE_DATA | R_ANAL_REF_TYPE_READ);
}
const FlutterPpStringRef *string_ref = flutter_pp_string_map_get (pp_strings, slot_off);
if (string_ref) {
RList *strings = flutter_pp_string_map_get (pp_strings, slot_off);
FlutterPpStringRef *first = strings? r_list_first (strings): NULL;
if (!first || first->indirect) {
if (!flutter_annotate_pp_ref (core, pp_refs, at, slot_off, false, stats)) {
char *msg = r_str_newf ("dart: PP slot +0x%" PFMT64x, slot_off);
flutter_append_comment (core, at, msg, stats);
free (msg);
}
}
RListIter *it;
FlutterPpStringRef *string_ref;
r_list_foreach (strings, it, string_ref) {
flutter_set_flag (core, "dart.str", string_ref->string_value, string_ref->string_addr, (ut32)strlen (string_ref->string_value), true);
flutter_add_ref (core, at, string_ref->string_addr, R_ANAL_REF_TYPE_STRN | R_ANAL_REF_TYPE_READ);
stats->string_refs++;
char *escaped = r_str_escape_utf8 (string_ref->string_value, false, true);
char *msg = r_str_newf ("dart: PP slot +0x%" PFMT64x " string \"%s\"", pp_off, escaped);
char *msg = r_str_newf ("dart: PP slot +0x%" PFMT64x " %sstring \"%s\"", pp_off, string_ref->indirect? "contains ": "", escaped);
flutter_append_comment (core, at, msg, stats);
free (msg);
free (escaped);
} else if (!flutter_annotate_pp_ref (core, pp_refs, at, slot_off, false, stats)) {
char *msg = r_str_newf ("dart: "
"PP slot +0x%" PFMT64x,
slot_off);
flutter_append_comment (core, at, msg, stats);
free (msg);
}
stats->pp_refs++;
return true;
+21
View File
@@ -0,0 +1,21 @@
NAME=r2-pp-analysis-discarded-code-android
FILE=bins/android/first/libapp.so
ARGS=-NN -e scr.color=0 -e bin.relocs.apply=true -e io.cache=true
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
# Simulate fewer retained Code objects than instruction entries, with all
# entries before first_entry_with_code. Patches stay in the IO cache.
wx 1e8a @ 0x8f7f
wv4 1311 @ 0x1f558
r2flutter -q -i~?
r2flutter -q -l 2 -i~?
r2flutter -q -AAA
axtq @ 0x2710
EOF
EXPECT=<<EOF
1311
2
0x8b914
EOF
RUN
@@ -0,0 +1,18 @@
NAME=r2-pp-instance-string-xref-android
FILE=bins/android/w0rdle/libapp.so
ARGS=-NN -e scr.color=0 -e bin.relocs.apply=true
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
r2flutter -q -AA
axtq @ 0x3d29e~0xdb87f
r2flutter -q -AAA
CC. @ 0x19d630
axtq @ 0x3d29e~0x19d630
EOF
EXPECT=<<EOF
0xdb87f
dart: PP slot +0xcd8 instance | dart: PP slot +0xcd8 contains string "dark"
0x19d630
EOF
RUN
+18
View File
@@ -0,0 +1,18 @@
NAME=r2-pp-instance-string-xref-ios
FILE=bins/ios/Runner.app/Frameworks/App.framework/App
ARGS=-NN -e scr.color=0 -e bin.relocs.apply=true
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
r2flutter -q -AA
axtq @ 0x2a3300~0x22dd19
r2flutter -q -AAA
CC. @ 0x330ac
axtq @ 0x2a3300~0x330ac
EOF
EXPECT=<<EOF
0x22dd19
dart: PP slot +0xa48 instance | dart: PP slot +0xa48 contains string "dark"
0x330ac
EOF
RUN
@@ -0,0 +1,15 @@
NAME=r2-pp-shifted-string-xref-android
FILE=bins/android/w0rdle/libapp.so
ARGS=-NN -e scr.color=0 -e bin.relocs.apply=true
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
r2flutter -q -AAA
CC. @ 0x178c70
axtq @ 0x51746~0x178c70
EOF
EXPECT=<<EOF
dart: PP slot +0xb5f8 string "Unexpected state: "
0x178c70
EOF
RUN
+15
View File
@@ -0,0 +1,15 @@
NAME=r2-pp-shifted-string-xref-ios
FILE=bins/ios/Runner.app/Frameworks/App.framework/App
ARGS=-NN -e scr.color=0 -e bin.relocs.apply=true
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
r2flutter -q -AAA
CC. @ 0x44604
axtq @ 0x27af40~0x44604
EOF
EXPECT=<<EOF
dart: PP slot +0xa590 string "callOnHover"
0x44604
EOF
RUN