Skip to content

Latest commit

 

History

History
166 lines (135 loc) · 13.1 KB

PROFILES.md

File metadata and controls

166 lines (135 loc) · 13.1 KB

fill DSL & Profile Specification

The engine (src/engine/) is invariant across Dart versions/platforms; everything that varies by version/platform lives in the JSON files under profiles/. This document specifies both configuration kinds and defines the contract that "adding a new Dart version/platform requires configuration changes only."

Struct layouts are a separate, compile-time tier: profiles/struct/*.h holds the version × architecture DartThread field layouts (generated by compiling the Dart VM) and is embedded into the binary via src/struct_tables.rs; the DartObjectPool struct is generated per target at export time. A missing struct layout degrades gracefully to a generic DartThread, so it does not participate in the fill-DSL contract below.

1. SDK profile (profiles/sdk/*.json)

Keyed by (Dart version, word_size, compressed_pointers). All 26 versions in profiles/sdk/ (1.24.3 → 3.14β), as 47 files once the 21 compressed-pointer variants are counted, are compiled into the binary: at runtime dae matches the exact version by snapshot version fingerprint (32-byte hash, see version_hashes.json); on a miss it runs structural probes against the embedded versions with fill scoring (see src/profile/detect.rs). The byte-comparison baseline is dart-3.3.4-w64-no-compressed.json (a macOS Flutter app / arm64 / 3.3.4 / no pointer compression / --obfuscate — full comparison passed).

Top-level fields (SdkProfile; serde definition in src/profile/mod.rs):

field meaning
abi version identifier (documentation use)
status verified / unverified: export-time notice when a version's sample comparison is incomplete
format snapshot-format switches (see §1.1)
word_size / compressed_pointers affect frida constants and runtime offsets
tagging heap_object_tag / object_alignment(_log2) / smi_mask / smi_shift / cid_tag_pos / cid_tag_mask / num_predefined_cids
header_fields snapshot stream header fields, read order (name → header map); each entry is either "name" (unsigned) or {"name": ..., "kind": "signed"} (1.24.3 reads everything signed)
full_aot_kind / object_start_alignment kind=3 (Snapshot::kFullAOT); data_image alignment 64
alloc allocation-phase classification (see §1.2)
cluster_layouts fill layout per cid (fill DSL, see §3)
runtime_offsets object layout offsets used by asm IL (thread_field_table_values / array_data_minus_tag, etc.)
frida_cid_constants header constants for frida.js (values emitted verbatim; ClassIdTagMask kept as the string "0xfffff" to preserve the original form)
frida_special_layouts special class entries in the frida Classes array (static names + offsets for bool/int/String/List/Map/Closure/Object; key order = output order)
frida_int_typed_cids the 8 integer TypedData cids that carry lenOffset/dataOffset
class_id_names cid → predefined class name (sparse map, from the class_id.h enum + expanded TypedData names)

1.1 FormatConfig (format)

Snapshot-format differences that used to be engine code branches; all serde-defaulted:

field values / meaning
ref_encoding ref_id_128 (2.19+, big-endian 7-bit groups +128) / unsigned_leb128 (≤2.17)
instructions_table_source snapshot_rodata (default) / image (2.15: table lives in the AOT image) / code_text_offsets (2.15-2.16 bare instructions)
cluster_header cid_and_canonical (≤3.5: cid=value>>1, canonical=value&1) / cid_tags (3.6+: full object tags, ClassIdTag decode)
single_snapshot 3.13+: VM/ISO merged into a single snapshot
class_alloc pre_ids (≤3.12) / fixed (3.13+)
code_leading_refs 3.13+: 2 leading refs before Code fill objects
string_clusters_separate ≤2.14: string data spread over three cids
code_alloc state_deferred (2.10+) / count_only (≤2.9)
type_dual_count ≤2.9: Type cluster dual-segment dual-count alloc
instance_legacy / instance_alloc_nfo_only ≤2.9 no-bitmap instances / 2.10 alloc reads nfo only
objectpool_legacy / objectpool_type_low7 / objectpool_type_low7_swapped ObjectPool entry bitfield eras (≤2.9 / 2.10–3.1 / 3.2.x swap)
code_refs refs per Code object (default 6; 2.15 = 7)
code_has_text_offset ≤2.16: leading text-offset uvarint in Code fill
class_fill_adj / code_fill_adj 2.18.x-only fill start corrections (bytes; -2 / -4, recovered by byte-diffing)
elf_name_backfill transitional versions (2.16/2.18/2.19.6): backfill function names from ELF symbols by entry address

1.2 alloc classification

key meaning
rodata_cids ROData classes ({23,24,25,92,93,94}): alloc reads delta offsets; fill is empty
var_cids variable-length classes ({17,22,27,28,29,46,66,89,90}): alloc reads count lengths
mint_cid(60) / code_cid(18) / class_cid(5) / instance_cid(44) / library_cid(13) / function_cid(7) special alloc sections
instance_min(176) / ffi_cids instance alloc (first reads the nfo/isize pair as svarints)
type_cid / type_parameter_cid Type / TypeParameter cluster cids (≤2.10 dual-segment dual-count alloc)
canonical_table_cids additionally read the canonical table when canonical=1
canonical_subset_table_cids subset-style canonical tables (2.13/2.14 only Type): extra first_element varint
cid_only_alloc_cids clusters whose alloc writes cid only, no count (2.13 WeakSerializationReference)
typed_data_first/count/stride TypedData classification: slot=(cid-first)/stride, rem=(cid-first)%stride
typed_data_elem_widths/var_rem/view_rem elem widths per slot; rem ∈ var_rem → variable-length, view_rem → view
string_cid(92) / one_byte_string_cid(93) / two_byte_string_cid(94) string decoding

1.3 Fields whose presence depends on the version

The generator starts from the 3.3.4 baseline and remaps class ids for other versions. Remapping does not remove fields that a given version's serializer never wrote, so every one of these had to be pinned per version by reading that version's app_snapshot.cc. Get one wrong and the whole snapshot drifts from that cluster onward — silently, with warnings 0.

Cluster Field Present from Layout consequence
TypeParameter base_/index_ as uint8 vs uint16 int32 + 3×uint8 ≤2.19; int32 + 2×uint16 + uint8 3.0; 2×uint16 + uint8 ≥3.2 1 svarint + 3 raw bytes / 3 svarints + 1 byte / 2 svarints + 1 byte
Function packed_fields_ written unconditionally ≤2.17; moved inside if (kind != kFullAOT) from 2.18 2 trailing svarints ≤2.17, 1 from 2.18
SubtypeTestCache num_inputs, num_occupied 3.2.0 refs(1) only before that
FfiTrampolineData ffi_function_kind_ 3.2.0 no trailing byte before that
Type trailing combined byte present 2.15–2.18, gone from 2.19 refs(3) + uvarint + byte vs refs(3) + uvarint

Two encoding rules from datastream.h are easy to get backwards and cost hours:

  • Serializer::Write<T> is documented as "raw data", but Raw<sizeof(T),T> only writes a genuine raw byte when sizeof(T) == 1. Raw<2>/Raw<4>/Raw<8> forward to BaseWriteStream::Write<T>, which is a varint. So Write<uint8_t> is raw; Write<uint16_t> / Write<int32_t> / Write<int64_t> / Write<double> are varints. Only WriteFixed<T> is fixed-width.
  • WriteUnsigned sets the high bit on the terminator, not on continuation bytes (kEndUnsignedByteMarker = 255 − 127 = 0x80) — the inverse of standard ULEB128. Encoding a length as ULEB128 while searching for it yields plausible-looking false hits.

check_profiles.sh regenerates all 47 files (26 versions × word size, plus 21 compressed-pointer variants; 2.7/2.10/2.12 predate pointer compression) from SDK source and diffs them, so a hand-edited or stale profile fails. It hard-codes the expected compressed-variant count: "the file isn't there, so skip it" is the same false-pass shape the analyze gates closed.

2. Platform profile (profiles/platform/*.json)

Table of (container, arch): macho/elf/pe × arm64/x64/…. Fields (PlatformProfile):

field meaning
container.kind macho / elf / pe (selects the parser)
symbols the three entry symbol names (_kDartVmSnapshotData etc., same names across platforms)
symbols_alt backup symbol set (e.g. the 3.13 single-snapshot three-segment names); used when the primary set is incomplete
registers role registers for IL/disassembly (thr/pp/null/barrier/fp/sp/lr/code_reg…)
register_aliases op_str rewrite alias table (x29→fp etc., matching blutter's output style)
non_field_base fixed-role registers that must never be used as an object-field base
polymorphic_entry_offset_aot monomorphic entry offset (arm64 AOT = 24)
frida_heap_address_reg HeapAddressReg rewrite value for the frida template
r2_app_base optional override for f app.base in addNames.r2 (default: the container's text-segment VM address)
code_floor minimum file offset for function addresses (entry_for filter): the stripped-executable fallback path can leak bogus entries from the tail of the instruction table into container-header offsets; Mach-O uses 4096, other containers 0 (default)
status verified / unverified (currently only macho-arm64 is verified; pe-arm64 is an unverified structural combination, no real device sample)

3. fill DSL (cluster_layouts[<cid>].steps)

The step list is interpreted per object (k = 0..count); captured values go into the per-object ctx (a key/value table) and the collect lists. Allocation-phase products are referenced with times:"lengths".

Atomic reads

op fields semantics
refs n, aliases (capture names in order, null skipped), collect (with n=1, append the value to the named list) read n ref-ids
uvarint alias / collect / transform:"shr4_and_mask" ReadUnsigned; transform = Type's (flags>>4)&cid_tag_mask
svarint alias / collect signed variable-length Read<T>
byte alias / collect 1 byte
u32 alias / collect fixed 4-byte little-endian uint32 (2.15–2.17 Function packed_fields_/kind_tag_)
text_offset signed(bool, 2.10 special) read one varint and accumulate it into the snapshot-level text_offsets (≤2.16 bare-instruction entry addressing, code_text_offsets source)
skip_raw_elem_width — skip lengths[k] * elem_width(cid) raw bytes (TypedData)
skip_align n alignment padding: jump to position % n == 0 (≤2.9 ExternalTypedData, n=8)
skip_const n skip n bytes

Structure

op fields semantics
loop times = "lengths" (alloc lengths) or a constant; steps sub-loop
cond on (captured field), lt/ge numeric tests, then; set_default (defaults captured when not matched) conditional execution (used for the Class top-level nullable bitmap)
objectpool — special: length + per-entry bits dispatch (captures pp.txt entries)
instance_fields — special: cluster-level bitmap + nfo-1 slots (ref or word32x2)
code — special: payload_info (read only when k<nondef) + 6 refs
emit store, fields (record field → alias name / `{"builtin":"cid" "ref"}`)

Built-in layouts (no JSON declaration needed): string classes (cid==string_cid, decoded via rodata offsets), rodata/mint (empty fill), instance classes (handled by the instance_fields step), TypedData and View (atomic skip / 3 refs + 2 svarint).

emit store tables

store record fields produced
functions name_ref, owner_ref, code_index, kind_tag function table
classes name_ref, library_ref, class_id, super_type_ref, next_field_off, type_arg_off, field_bitmap class table
libraries name_ref, url_ref library table
patch_classes wrapped_class PatchClass → wrapped class
type_cids type_cid class id of Type objects
map_data data_ref, used_ref (cid taken automatically from the cluster cid) ConstMap/ConstSet
array_elements ta, elems (collect list) Array/ImmutableArray

4. Fault tolerance and validation

  • After alloc, next_ref - 1 == num_objects is checked; a mismatch raises a warning (corresponds to the reference implementation's alloc mismatch).
  • A cid > 60000 in the stream is treated as drift: parsing stops with a warning.
  • fill fails explicitly for a cid without a layout, suggesting to add one to cluster_layouts (the reference implementation aborts silently; we fail loudly instead).
  • The engine errors on any uncaptured field of an emitted record (a cond missing set_default is the most common cause).
  • Version auto-detection is two-stage: exact snapshot version-hash match (version_hashes.json), then a structural probe (alloc trial-parse + fill scoring) when the hash is unknown; if neither produces a result the embedded dart/3.3.4 profile is used with an explicit warning (see src/profile/detect.rs).