Harden unknown-hash handling and report version provenance

- 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
This commit is contained in:
pancake committed 2026-08-11 11:29:32 +02:00
1 parent 19d8b96fc5
commit d387c0b15b
10 files changed
+108 -29

No files matched your search

+2 -2
View File
@@ -17,8 +17,8 @@ instruction tables, and strings directly.
| Mach-O details | Thin Mach-O64 and fat Mach-O/fat64; arm64 slice is preferred. LC_NOTE owner `__dart_app_snap` is detected and the embedded Dart Mach-O payload is extracted to a temporary file. |
| Architecture | ARM64/AArch64 is the real target. Instruction-table entrypoints are based on ARM64 code offsets, PP tracking uses `x27`, and the radare2 analysis pass understands AArch64 registers/op metadata. |
| Snapshot kind | Clustered Dart AOT snapshots with magic `0xdcdcf5f5`, 32-byte snapshot hashes, feature strings, and unsigned varint header fields. |
| Dart layouts | In-tree profiles cover Dart `2.10.0` through `3.12.1`, roughly Flutter `1.22.x` through `3.41.x`. |
| Unknown hashes | Falls back to hardcoded default offsets (8/24/16/32 for entry points, 56 for owner, 24/8 for names) + v3.9.2-shaped ObjectHeader defaults while reporting the Dart version as `unknown`. |
| Dart layouts | In-tree profiles cover Dart `2.10.0` through `3.12.1`, roughly Flutter `1.22.x` through `3.41.x`. Profiles are keyed at layout-change boundaries and resolved by floor: e.g. `3.7`/`3.8` share the `3.6.0` CID layout and `3.10`–`3.12` share the `3.9.0` layout. Verified byte-for-byte against every release's `runtime/vm/class_id.h` (2.10.0–3.12.1). |
| Unknown hashes | Git/dev builds carry an unlisted snapshot hash. r2flutter **fingerprints** them: it applies the newest known layout profile (snapshot formats only change at release boundaries, so a fresh git build matches the latest release) and refines the compressed word size from the feature flags. The Dart version is reported as `~<version>+ (fingerprint)` and `version_source` is set to `fingerprint` (vs `exact-hash` / `override`) so the result is never silently presented as an exact match. |
| Obfuscation maps | Flutter/Dart `--save-obfuscation-map` JSON arrays are supported with `-m <file>` or `e r2flutter.mapfile=<file>` in the plugin. |
## Layout Profiles
+1
View File
@@ -47,6 +47,7 @@ typedef struct {
char dart_version_override[32];
const DartVerLayout *layout;
int compressed_word_size;
int version_source; // DartVersionSource: how the layout/version was chosen
HtUP *name_by_ep;
char **name_by_code_index;
ut64 name_by_code_index_count;
+17 -1
View File
@@ -19,6 +19,15 @@ typedef enum {
DART_TAG_STYLE_OBJECT_HEADER = 2 // v3.4.3+ and known 2.18.2 exception
} DartTagStyle;
// How the Dart version/layout for a snapshot was determined. Reported so users
// can tell whether decoding rests on an exact match or a best-effort guess.
typedef enum {
DART_VERSION_SOURCE_EXACT = 0, // snapshot hash matched a known SDK release
DART_VERSION_SOURCE_OVERRIDE = 1, // user forced a profile via -D
DART_VERSION_SOURCE_FINGERPRINT = 2, // unknown hash, layout inferred from evidence
DART_VERSION_SOURCE_UNKNOWN = 3 // no snapshot available
} DartVersionSource;
// Version layout information for Dart snapshots
// CID values are stored in dart_cid.c's cid_tables[] and accessed via dart_cid_get()
typedef struct {
@@ -42,9 +51,16 @@ const DartVerLayout *dart_profile_from_version(const char *version);
// Pick a layout structure based on hash, with fallback to defaults
// Returns a newly allocated DartVerLayout (caller must free)
// If hash is unknown, uses v3.9.2 defaults
// If hash is unknown, fingerprints against the newest known layout profile
DartVerLayout *dart_pick_layout_by_hash(const char *hash);
// Newest known layout profile (used as the fingerprint baseline for unknown
// git/dev builds, whose snapshot format tracks the latest release).
const DartVerLayout *dart_newest_profile(void);
// Human-readable name for a DartVersionSource value.
const char *dart_version_source_str(int source);
// Free a DartVerLayout allocated by dart_pick_layout_by_hash
void dart_ver_layout_free(DartVerLayout *layout);
+52 -12
View File
@@ -69,6 +69,37 @@ const char *dart_ctx_effective_version(DartCtx *ctx) {
return dart_version_from_hash (dart_ctx_effective_hash (ctx));
}
// Classify how the layout was determined, for honest reporting: exact hash
// match, user override, best-effort fingerprint (unknown hash), or no snapshot.
static void dart_ctx_set_version_source(DartCtx *ctx) {
if (!ctx) {
return;
}
if (R_STR_ISNOTEMPTY (ctx->dart_version_override)) {
ctx->version_source = DART_VERSION_SOURCE_OVERRIDE;
} else if (dart_version_from_hash (dart_ctx_effective_hash (ctx))) {
ctx->version_source = DART_VERSION_SOURCE_EXACT;
} else if (R_STR_ISNOTEMPTY (dart_ctx_effective_hash (ctx))) {
ctx->version_source = DART_VERSION_SOURCE_FINGERPRINT;
} else {
ctx->version_source = DART_VERSION_SOURCE_UNKNOWN;
}
}
// Effective version label including a marker when the layout was fingerprinted
// from an unknown (git/dev build) hash rather than matched exactly.
static const char *dart_ctx_version_label(DartCtx *ctx, char *buf, size_t bufsz) {
const char *v = dart_ctx_effective_version (ctx);
if (v) {
return v;
}
if (ctx && ctx->version_source == DART_VERSION_SOURCE_FINGERPRINT && ctx->layout && ctx->layout->dart_version) {
snprintf (buf, bufsz, "~%s+ (fingerprint)", ctx->layout->dart_version);
return buf;
}
return "unknown";
}
static const char *dart_tag_style_names[] = {
"CID_INT32", // DART_TAG_STYLE_CID_INT32 = 0
"CID_SHIFT1", // DART_TAG_STYLE_CID_SHIFT1 = 1
@@ -103,12 +134,13 @@ static const DartVerLayout *load_layout_from_json(const char *hash, DartVerLayou
}
const char *version = dart_version_from_hash (hash);
const DartVerLayout *base_profile = version? dart_profile_from_version (version): NULL;
if (base_profile) {
memcpy (out, base_profile, sizeof (*out));
} else {
memset (out, 0, sizeof (*out));
out->tag_style = DART_TAG_STYLE_OBJECT_HEADER;
if (!base_profile) {
// Unknown hash: fingerprint against the newest known profile so the
// decode path (-f/-A) uses the same real layout the -H report shows,
// not a degenerate zeroed struct. cws is still refined from the flags.
base_profile = dart_newest_profile ();
}
memcpy (out, base_profile, sizeof (*out));
r_str_ncpy (out->hash, hash, sizeof (out->hash));
for (int i = 0; i < DART_HASH_ENTRIES_COUNT; i++) {
if (!strcmp (dart_hash_entries[i].hash, hash)) {
@@ -206,6 +238,7 @@ DartVerLayout *dart_ctx_init_layout(DartCtx *ctx, DartVerLayout *tmp) {
ctx->layout = owned;
}
derive_layout_from_flags (ctx);
dart_ctx_set_version_source (ctx);
return owned;
}
@@ -583,6 +616,7 @@ static int prepare_header_data(DartCtx *ctx) {
extract_snapshot_hash_flags (ctx, ctx->vm_data);
ctx->layout = dart_pick_layout_owned_for_ctx (ctx);
derive_layout_from_flags (ctx);
dart_ctx_set_version_source (ctx);
return 0;
}
@@ -594,7 +628,9 @@ char *dart_pool_dump_header(DartCtx *ctx, int fmt) {
return fmt == 'r'? strdup ("# Error: Dart snapshots not found\n"): strdup ("Error: Dart snapshots not found\n");
}
const bool quiet = ctx && ctx->quiet;
const char *version = dart_ctx_effective_version (ctx);
char vbuf[48];
const char *version = dart_ctx_version_label (ctx, vbuf, sizeof (vbuf));
const char *version_source = dart_version_source_str (ctx? ctx->version_source: DART_VERSION_SOURCE_UNKNOWN);
if (fmt == 'j') {
ut64 header_addr = ctx->iso_data? ctx->iso_data: ctx->vm_data;
DartSnapshotHeader sh = { 0 };
@@ -630,7 +666,8 @@ char *dart_pool_dump_header(DartCtx *ctx, int fmt) {
pj_kn (pj, "total", sh.total_len);
pj_end (pj);
pj_ki (pj, "cws", ctx->compressed_word_size);
pj_ks (pj, "dart_version", version? version: "unknown");
pj_ks (pj, "dart_version", version);
pj_ks (pj, "version_source", version_source);
if (ctx->layout) {
const DartVerLayout *l = ctx->layout;
pj_ks (pj, "tag_style", dart_tag_style_to_string (l->tag_style));
@@ -669,7 +706,7 @@ char *dart_pool_dump_header(DartCtx *ctx, int fmt) {
if (!quiet) {
// Add comments with metadata
r_strbuf_appendf (sb, "'@0x%" PFMT64x "'CC Dart snapshot hash: %s\n", (ut64)ctx->vm_data, ctx->snapshot_hash[0]? ctx->snapshot_hash: "(unknown)");
r_strbuf_appendf (sb, "'@0x%" PFMT64x "'CC Dart version: %s\n", (ut64)ctx->vm_data, version? version: "unknown");
r_strbuf_appendf (sb, "'@0x%" PFMT64x "'CC Dart version: %s (%s)\n", (ut64)ctx->vm_data, version, version_source);
if (ctx->layout) {
const DartVerLayout *l = ctx->layout;
r_strbuf_appendf (sb, "'@0x%" PFMT64x "'CC Tag style: %s\n", (ut64)ctx->vm_data, dart_tag_style_to_string (l->tag_style));
@@ -681,7 +718,7 @@ char *dart_pool_dump_header(DartCtx *ctx, int fmt) {
RStrBuf *sb = r_strbuf_new ("");
if (quiet) {
r_strbuf_appendf (sb, "snapshot_hash=%s dart_version=%s vm_data=0x%" PFMT64x " vm_instr=0x%" PFMT64x " iso_data=0x%" PFMT64x " iso_instr=0x%" PFMT64x " cws=%d", ctx->snapshot_hash[0]? ctx->snapshot_hash: "(unknown)", version? version: "unknown", (ut64)ctx->vm_data, (ut64)ctx->vm_instr, (ut64)ctx->iso_data, (ut64)ctx->iso_instr, ctx->compressed_word_size);
r_strbuf_appendf (sb, "snapshot_hash=%s dart_version=%s version_source=%s vm_data=0x%" PFMT64x " vm_instr=0x%" PFMT64x " iso_data=0x%" PFMT64x " iso_instr=0x%" PFMT64x " cws=%d", ctx->snapshot_hash[0]? ctx->snapshot_hash: "(unknown)", version, version_source, (ut64)ctx->vm_data, (ut64)ctx->vm_instr, (ut64)ctx->iso_data, (ut64)ctx->iso_instr, ctx->compressed_word_size);
if (ctx->layout) {
const DartVerLayout *l = ctx->layout;
r_strbuf_appendf (sb, " tag_style=%s alignment=%d", dart_tag_style_to_string (l->tag_style), l->max_alignment);
@@ -690,7 +727,8 @@ char *dart_pool_dump_header(DartCtx *ctx, int fmt) {
}
r_strbuf_appendf (sb, "# Dart AOT Snapshot Header\n\n");
r_strbuf_appendf (sb, "snapshot_hash: %s\n", ctx->snapshot_hash[0]? ctx->snapshot_hash: "(unknown)");
r_strbuf_appendf (sb, "dart_version: %s\n", version? version: "unknown");
r_strbuf_appendf (sb, "dart_version: %s\n", version);
r_strbuf_appendf (sb, "version_source: %s\n", version_source);
if (ctx->layout) {
const DartVerLayout *l = ctx->layout;
r_strbuf_appendf (sb, "tag_style: %s\n", dart_tag_style_to_string_verbose (l->tag_style));
@@ -811,7 +849,8 @@ static void dump_header_snapshot_json(DartCtx *ctx, PJ *pj, const char *label, u
}
static char *dump_header_ext_json(DartCtx *ctx, int detail) {
const char *version = dart_ctx_effective_version (ctx);
char vbuf[48];
const char *version = dart_ctx_version_label (ctx, vbuf, sizeof (vbuf));
DartSnapshotHeader iso = { 0 };
if (ctx->iso_data) {
dart_snapshot_header_read (ctx, ctx->iso_data, &iso);
@@ -828,7 +867,8 @@ static char *dump_header_ext_json(DartCtx *ctx, int detail) {
pj_kn (pj, "iso_data", ctx->iso_data);
pj_kn (pj, "iso_instr", ctx->iso_instr);
pj_ki (pj, "cws", ctx->compressed_word_size);
pj_ks (pj, "dart_version", version? version: "unknown");
pj_ks (pj, "dart_version", version);
pj_ks (pj, "version_source", dart_version_source_str (ctx? ctx->version_source: DART_VERSION_SOURCE_UNKNOWN));
pj_ki (pj, "detail", detail);
if (ctx->container_kind[0]) {
pj_k (pj, "container");
+31 -9
View File
@@ -163,6 +163,27 @@ const char *dart_version_from_hash(const char *hash) {
return NULL;
}
const DartVerLayout *dart_newest_profile(void) {
const DartVerLayout *newest = version_profiles;
for (int i = 0; version_profiles[i].dart_version; i++) {
newest = &version_profiles[i];
}
return newest;
}
const char *dart_version_source_str(int source) {
switch (source) {
case DART_VERSION_SOURCE_EXACT:
return "exact-hash";
case DART_VERSION_SOURCE_OVERRIDE:
return "override";
case DART_VERSION_SOURCE_FINGERPRINT:
return "fingerprint";
default:
return "unknown";
}
}
const DartVerLayout *dart_profile_from_version(const char *version) {
if (!version) {
return NULL;
@@ -195,16 +216,17 @@ DartVerLayout *dart_pick_layout_by_hash(const char *hash) {
}
return dvl;
}
// Default to v3.9.2 layout for unknown hashes
dvl->compressed_word_size = 4;
dvl->heap_object_tag = 1;
dvl->max_alignment = 16;
dvl->it_cap = 20000;
dvl->tag_style = DART_TAG_STYLE_OBJECT_HEADER;
dvl->header_fields = 5;
dvl->dart_version = "unknown";
// Unknown hash: fingerprint against the newest known profile. Snapshot
// formats (CID tables, tag style) only change at release boundaries, so an
// unlisted git/dev build almost always matches the most recent release; the
// caller still refines compressed_word_size from the feature flags.
const DartVerLayout *newest = dart_newest_profile ();
memcpy (dvl, newest, sizeof (DartVerLayout));
if (hash && *hash) {
r_str_ncpy (dvl->hash, hash, sizeof (dvl->hash));
}
if (G_VERBOSE > 0) {
fprintf (stderr, "[r2flutter] Unknown snapshot hash, using v3.9.2 defaults (hash=%s)\n", hash? hash: "(null)");
fprintf (stderr, "[r2flutter] Unknown snapshot hash, fingerprinting as newest known profile %s (hash=%s)\n", newest->dart_version, hash? hash: "(null)");
}
return dvl;
}
+1 -1
View File
@@ -4,6 +4,6 @@ CMDS=<<EOF
!!../bin/r2flutter -j -H bins/android/first 2>/dev/null
EOF
EXPECT=<<EOF
{"kind":3,"hash":"f91b8b03bf7f30a5e983fd19b23d978d","vm_data":512,"vm_instr":327680,"iso_data":36544,"iso_instr":419584,"cluster":{"base":1154,"objs":11342,"clusters":47,"it_len":1311,"it_off":64,"total":91657},"cws":8,"dart_version":"2.18.2","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
{"kind":3,"hash":"f91b8b03bf7f30a5e983fd19b23d978d","vm_data":512,"vm_instr":327680,"iso_data":36544,"iso_instr":419584,"cluster":{"base":1154,"objs":11342,"clusters":47,"it_len":1311,"it_off":64,"total":91657},"cws":8,"dart_version":"2.18.2","version_source":"exact-hash","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
EOF
RUN
+1 -1
View File
@@ -4,6 +4,6 @@ CMDS=<<EOF
!!../bin/r2flutter -j -H bins/ios/AuthPass.app
EOF
EXPECT=<<EOF
{"kind":3,"hash":"f71c76320d35b65f1164dbaa6d95fe09","vm_data":7333504,"vm_instr":23168,"iso_data":7368576,"iso_instr":64960,"cluster":{"base":1129,"objs":277373,"clusters":1044,"it_len":37092,"it_off":64,"total":3159106},"cws":8,"dart_version":"3.2.5","tag_style":"CID_SHIFT1","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":92,"cid_one_byte_string":93,"cid_two_byte_string":94,"cid_array":89,"cid_mint":60,"cid_object_pool":22,"num_predefined":176}}
{"kind":3,"hash":"f71c76320d35b65f1164dbaa6d95fe09","vm_data":7333504,"vm_instr":23168,"iso_data":7368576,"iso_instr":64960,"cluster":{"base":1129,"objs":277373,"clusters":1044,"it_len":37092,"it_off":64,"total":3159106},"cws":8,"dart_version":"3.2.5","version_source":"exact-hash","tag_style":"CID_SHIFT1","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":92,"cid_one_byte_string":93,"cid_two_byte_string":94,"cid_array":89,"cid_mint":60,"cid_object_pool":22,"num_predefined":176}}
EOF
RUN
+1 -1
View File
@@ -4,6 +4,6 @@ CMDS=<<EOF
!!../bin/r2flutter -j -H bins/ios/Runner.app 2>/dev/null
EOF
EXPECT=<<EOF
{"kind":3,"hash":"d20a1be77c3d3c41b2a5accaee1ce549","vm_data":1771776,"vm_instr":16384,"iso_data":1807872,"iso_instr":59136,"cluster":{"base":1156,"objs":51434,"clusters":335,"it_len":7670,"it_off":64,"total":536223},"cws":8,"dart_version":"3.4.3","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":92,"cid_one_byte_string":93,"cid_two_byte_string":94,"cid_array":89,"cid_mint":60,"cid_object_pool":22,"num_predefined":174}}
{"kind":3,"hash":"d20a1be77c3d3c41b2a5accaee1ce549","vm_data":1771776,"vm_instr":16384,"iso_data":1807872,"iso_instr":59136,"cluster":{"base":1156,"objs":51434,"clusters":335,"it_len":7670,"it_off":64,"total":536223},"cws":8,"dart_version":"3.4.3","version_source":"exact-hash","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":92,"cid_one_byte_string":93,"cid_two_byte_string":94,"cid_array":89,"cid_mint":60,"cid_object_pool":22,"num_predefined":174}}
EOF
RUN
+1 -1
View File
@@ -4,6 +4,6 @@ CMDS=<<EOF
!!../bin/r2flutter -j -H bins/android/mafia 2>/dev/null
EOF
EXPECT=<<EOF
{"kind":3,"hash":"97ff04a728735e6b6b098bdf983faaba","vm_data":832,"vm_instr":5898240,"iso_data":19072,"iso_instr":5990720,"cluster":{"base":1177,"objs":237774,"clusters":1105,"it_len":37258,"it_off":64,"total":4871502},"cws":4,"dart_version":"3.9.2","tag_style":"OBJECT_HEADER","alignment":64,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
{"kind":3,"hash":"97ff04a728735e6b6b098bdf983faaba","vm_data":832,"vm_instr":5898240,"iso_data":19072,"iso_instr":5990720,"cluster":{"base":1177,"objs":237774,"clusters":1105,"it_len":37258,"it_off":64,"total":4871502},"cws":4,"dart_version":"3.9.2","version_source":"exact-hash","tag_style":"OBJECT_HEADER","alignment":64,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
EOF
RUN
+1 -1
View File
@@ -4,6 +4,6 @@ CMDS=<<EOF
!!../bin/r2flutter -j -D 830f4f59e7969c70b595182826435c19 -H bins/android/first 2>/dev/null
EOF
EXPECT=<<EOF
{"kind":3,"hash":"830f4f59e7969c70b595182826435c19","vm_data":512,"vm_instr":327680,"iso_data":36544,"iso_instr":419584,"cluster":{"base":1154,"objs":11342,"clusters":47,"it_len":1311,"it_off":64,"total":91657},"cws":8,"dart_version":"3.8.1","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
{"kind":3,"hash":"830f4f59e7969c70b595182826435c19","vm_data":512,"vm_instr":327680,"iso_data":36544,"iso_instr":419584,"cluster":{"base":1154,"objs":11342,"clusters":47,"it_len":1311,"it_off":64,"total":91657},"cws":8,"dart_version":"3.8.1","version_source":"exact-hash","tag_style":"OBJECT_HEADER","alignment":16,"header_fields":5,"it_capacity":20000,"cid_table":{"cid_class":5,"cid_function":7,"cid_code":18,"cid_string":93,"cid_one_byte_string":94,"cid_two_byte_string":95,"cid_array":90,"cid_mint":61,"cid_object_pool":23,"num_predefined":175}}
EOF
RUN