Decode Record fields in snapshot value graphs

This commit is contained in:
pancake committed 2026-10-21 12:46:56 +02:00
1 parent 3bfdd5601a
commit 25a5b3f026
6 files changed
+173 -2

No files matched your search

+26
View File
@@ -2001,3 +2001,29 @@ On iOS AuthPass, `PP+0x68` holds ref 168520 in the Code cluster starting at
regression checks index zero, a discarded record, the following main record,
and an unresolved deferred record. These targets use the existing table
payload-address convention described above.
## Record inspection decodes the shape and field references
Record fills contain `ReadUnsigned (shape)` followed by one reference per
field. `RecordShape::NumFieldsBitField` in the VM is the low 16 bits; the upper
bits identify the registered array of named field names. Allocation records
separately store each Record's field count. Retain those counts and check each
fill against its own allocation instead of accepting only matching cluster
minimum, maximum, and total counts.
The value graph now decodes Record fields into the same bounded edges used by
other objects. JSON includes `shape`, `field_names_index`, `length`, and
`fields`; text output prints the shape, length, and child values. Labels remain
ordinal (`field_0`, etc.); the shape's named-field table index is preserved
rather than treated as a reference or used to invent names. Empty Records
report length zero. Cycles, depth limits, and the 128-child display limit use
the existing graph handling. An unreadable or inconsistent Record fill stays
`complete: false`.
In `t/libapp_1.so`, ref 146422 has shape 2 and fields referring to base object
996 and instance 164557; that instance exposes the string `DOUBLE_QUOTE`.
The public Android mafia fixture has the equivalent Record at ref 158051,
whose second field is an `XmlAttributeType` instance. Synthetic compressed and
uncompressed snapshots cover nested Records, named shapes, empty Records,
cycles, truncation, and a malformed field count. Existing iOS graph tests
continue to cover arrays, maps, and sets; those fixtures have no Record cluster.
+56 -2
View File
@@ -760,7 +760,7 @@ static bool modern_skip_alloc(ClusterStream *s, const ModernCidCache *cids, int
return false;
}
meta->count = count;
if (kind == MODERN_ALLOC_VARIABLE && count) {
if ((kind == MODERN_ALLOC_VARIABLE || kind == MODERN_ALLOC_RECORD) && count) {
meta->alloc_items = (ut64 *)calloc ((size_t)count, sizeof (ut64));
if (!meta->alloc_items) {
return false;
@@ -1303,6 +1303,9 @@ static bool modern_skip_fill_record(ClusterStream *s, const ModernClusterMeta *m
return false;
}
ut64 fields = shape & 0xffffULL;
if (meta->alloc_items && fields != meta->alloc_items[i]) {
return false;
}
if (meta->alloc_items_count == meta->count && fields > meta->alloc_items_max) {
return false;
}
@@ -2982,10 +2985,12 @@ typedef struct {
bool edges_truncated;
bool has_type_arguments;
bool has_scalar;
bool has_shape;
ut64 ref;
ut64 count;
ut64 type_arguments_ref;
ut64 scalar;
ut64 shape;
int cid;
const char *kind;
char *name;
@@ -3092,6 +3097,46 @@ static bool modern_value_read_array(ModernValueGraph *graph, ModernValueNode *no
return false;
}
static bool modern_value_read_record(ModernValueGraph *graph, ModernValueNode *node, const ModernClusterMeta *meta) {
ClusterStream s = {
.ctx = graph->ctx,
.cursor = meta->fill_offset,
.end = meta->fill_end,
};
const ut64 target = node->ref - meta->start_ref;
for (ut64 i = 0; i < meta->count; i++) {
ut64 shape = 0;
if (!cs_read_unsigned (&s, &shape)) {
return false;
}
const ut64 fields = shape & 0xffffULL;
if (!meta->alloc_items || fields != meta->alloc_items[i]) {
return false;
}
if (i == target) {
node->has_shape = true;
node->shape = shape;
if (!modern_value_edges_alloc (node, fields)) {
return false;
}
}
for (ut64 j = 0; j < fields; j++) {
ut64 ref = 0;
if (!cs_read_ref_id (&s, &ref)) {
return false;
}
if (i == target && j < node->edge_count) {
node->edges[j].index = j;
node->edges[j].ref = ref;
}
}
if (i == target) {
return true;
}
}
return false;
}
static bool modern_value_read_refs(ModernValueGraph *graph, ModernValueNode *node, const ModernClusterMeta *meta) {
const ModernFillSpec spec = modern_get_fill_spec (&graph->cids, meta->cid);
ClusterStream s = {
@@ -3255,6 +3300,8 @@ static ModernValueNode *modern_value_graph_node(ModernValueGraph *graph, ut64 re
}
if (!strcmp (node->kind, "array")) {
node->complete = modern_value_read_array (graph, node, meta);
} else if (meta->fill_kind == MODERN_FILL_RECORD) {
node->complete = modern_value_read_record (graph, node, meta);
} else if (meta->fill_kind == MODERN_FILL_REFS) {
node->complete = modern_value_read_refs (graph, node, meta);
} else if (!strcmp (node->kind, "instance")) {
@@ -3358,7 +3405,11 @@ static void modern_value_json_node(PJ *pj, ModernValueGraph *graph, ut64 ref, in
if (node->has_type_arguments) {
pj_kn (pj, "type_arguments_ref", node->type_arguments_ref);
}
if (node->count || node->edges) {
if (node->has_shape) {
pj_kn (pj, "shape", node->shape);
pj_kn (pj, "field_names_index", node->shape >> 16);
}
if (node->count || node->edges || node->has_shape) {
pj_kn (pj, "length", node->count);
pj_kb (pj, "values_truncated", node->edges_truncated);
}
@@ -3425,6 +3476,9 @@ static void modern_value_text_node(RStrBuf *sb, ModernValueGraph *graph, ut64 re
if (node->has_scalar) {
r_strbuf_appendf (sb, " value=%" PFMT64d, (st64)node->scalar);
}
if (node->has_shape) {
r_strbuf_appendf (sb, " shape=%" PRIu64 " length=%" PRIu64, (uint64_t)node->shape, (uint64_t)node->count);
}
r_strbuf_appendf (sb, " complete=%s\n", r_str_bool (node->complete));
if (modern_value_path_contains (path, path_count, ref) || depth >= MODERN_VALUE_MAX_DEPTH) {
return;
+23
View File
@@ -0,0 +1,23 @@
NAME=record-values-android
FILE=bins/android/mafia/libapp.so
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
r2flutter -q -j -O ref:158051~{snapshot.kind}
r2flutter -q -j -O ref:158051~{snapshot.complete}
r2flutter -q -j -O ref:158051~{snapshot.shape}
r2flutter -q -j -O ref:158051~{snapshot.length}
r2flutter -q -j -O ref:158051~{snapshot.fields[0].ref}
r2flutter -q -j -O ref:158051~{snapshot.fields[1].value.name}
r2flutter -q -j -O ref:158051~{snapshot.fields[1].value.fields[2].value.value}
EOF
EXPECT=<<EOF
record
true
2
2
1001
XmlAttributeType
DOUBLE_QUOTE
EOF
RUN
+14
View File
@@ -0,0 +1,14 @@
NAME=record-values-malformed
FILE=malloc://0xfd
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
wx 50414421f5f5dcdcf5000000000000000200000000000000333063303531636239313932663664363831326265336630623837613633353670726f647563742061726d363420636f6d707265737365642d706f696e746572730082898280800020cf82c1be0060d08580828281018180818384020084868381880181838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383
r2flutter -q -j -O ref:6~{snapshot.kind}
r2flutter -q -j -O ref:6~{snapshot.complete}
EOF
EXPECT=<<EOF
record
false
EOF
RUN
+34
View File
@@ -0,0 +1,34 @@
NAME=record-values-synthetic
FILE=malloc://0xfd
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
wx 50414421f5f5dcdcf5000000000000000200000000000000333063303531636239313932663664363831326265336630623837613633353670726f647563742061726d363420636f6d707265737365642d706f696e746572730082898280800020cf82c1be0060d08580828281018180828384020084868381880181838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383
r2flutter -q -j -O ref:5~{snapshot.complete}
r2flutter -q -j -O ref:5~{snapshot.length}
r2flutter -q -j -O ref:6~{snapshot.fields[0].value.value}
r2flutter -q -j -O ref:6~{snapshot.fields[1].value.value}
r2flutter -q -j -O ref:7~{snapshot.shape}
r2flutter -q -j -O ref:7~{snapshot.field_names_index}
r2flutter -q -j -O ref:7~{snapshot.fields[0].value.fields[1].value.value}
r2flutter -q -j -O ref:8~{snapshot.fields[0].value.cycle}
r2flutter -q -j -O ref:9~{snapshot.length}
r2flutter -q -j -O ref:9~{snapshot.values_truncated}
r2flutter -q -j -O ref:9~{snapshot.fields[127].value.value}
r2flutter -q -j -O ref:9~{=}~name = ~?
EOF
EXPECT=<<EOF
true
0
1
-2
65538
1
-2
true
129
true
1
128
EOF
RUN
+20
View File
@@ -0,0 +1,20 @@
NAME=record-values-uncompressed
FILE=malloc://0x100
CMDS=<<EOF
L ../src/r2/core_flutter.so
L ../src/r2/core_flutter.dylib
wx 50414421f5f5dcdcf8000000000000000200000000000000333063303531636239313932663664363831326265336630623837613633353670726f647563742061726d3634206e6f2d636f6d707265737365642d706f696e746572730082898280800020cf82c1be0060d08580828281018180828384020084868381880181838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383
r2flutter -q -j -O ref:6~{snapshot.kind}
r2flutter -q -j -O ref:6~{snapshot.complete}
r2flutter -q -j -O ref:6~{snapshot.length}
r2flutter -q -j -O ref:6~{snapshot.fields[1].value.value}
r2flutter -O ref:6~kind=record
EOF
EXPECT=<<EOF
record
true
2
-2
ref=6 kind=record cid=67 shape=2 length=2 complete=true
EOF
RUN