diff --git a/docs/flutter-analysis.md b/docs/flutter-analysis.md index 4c11a8e074..2d18753d63 100644 --- a/docs/flutter-analysis.md +++ b/docs/flutter-analysis.md @@ -62,6 +62,12 @@ server; older servers keep the complete request after schema discovery. [Dart 2.14 layout](https://github.com/dart-lang/sdk/blob/2.14.0/runtime/vm/snapshot.h). Kind numbers vary across Dart releases; other values are preserved with partial coverage, without guessing their interpretation. + Unified `_kDartSnapshotData` / `_kDartSnapshotText` exports are a different + unsupported profile. The [upstream VM-isolate removal](https://github.com/dart-lang/sdk/commit/d36adbacafee01d9852cf8e960cf2d8654d91e7e) + changes both the source layout and the FullAOT kind value. REA reports the + observed export names and library identity with a typed unsupported-profile + refusal when only unified exports are available; it does not alias one header + into two independent sources. Address resolution examines every overlapping `PT_LOAD` interpretation, including partial overlaps. Equivalent mappings are coalesced; conflicting mappings leave affected symbols unresolved and qualify coverage. Dynamic @@ -94,6 +100,8 @@ server; older servers keep the complete request after schema discovery. operation, and when they **disagree** the distinct values are kept in `snapshot_hash_candidates` with `snapshot_hash: null` and `coverage: "partial"` rather than silently picking one. + Identification can retain a single byte-scanned hash from a unified payload; + its partial status does not establish truncation or support for that profile. - **`elf_header_valid`** checks the supported little-endian ELF32/ELF64 header, table entry widths, and full table extents against the library's actual byte length. Truncated, malformed, or unsupported headers report diff --git a/src/contracts/flutter/flutterToolContracts.ts b/src/contracts/flutter/flutterToolContracts.ts index 9ade1057b6..8976971118 100644 --- a/src/contracts/flutter/flutterToolContracts.ts +++ b/src/contracts/flutter/flutterToolContracts.ts @@ -15,7 +15,7 @@ export const FLUTTER_TOOL_CONTRACTS = [ name: "identify_flutter_build", ...toolContractMetadata("identify_flutter_build"), description: - "Identify a Flutter app's build facts by pure parsing: the Dart VM snapshot hash from every lib//libapp.so AOT section (vm and isolate, cross-checked), per-ABI library digests, the engine library's GNU build-id, and any labeled Dart version or toolchain strings libflutter.so carries. The snapshot hash is the artifact that maps to a Dart SDK release through an external dataset; REA performs no lookup. No engine, no execution, no external tools.", + "Identify a Flutter app's build facts by pure parsing: observed Dart snapshot hashes from lib//libapp.so, per-ABI library digests, the engine library's GNU build-id, and any labeled Dart version or toolchain strings libflutter.so carries. Complete snapshot coverage currently expects two independent headers in the legacy split VM/isolate profile; unified and other layouts can yield partial build facts. The snapshot hash maps to a Dart SDK release through an external dataset; REA performs no lookup. No engine, no execution, no external tools.", kind: "flutter-provider", inputSchema: flutterInputSchemas.identify_flutter_build, outputSchema: evidenceResultOf(flutterResultSchemas.identify_flutter_build), @@ -30,7 +30,7 @@ export const FLUTTER_TOOL_CONTRACTS = [ name: "inspect_dart_aot", ...toolContractMetadata("inspect_dart_aot"), description: - "Inspect one ABI's Dart AOT snapshot inside an APK: the four snapshot symbols located through the ELF dynamic symbol table with their offsets, sizes, and section headers (magic, kind, length), the snapshot hash, and a bounded string-pool projection from the isolate data section (package: source URIs, dart: SDK URIs, identifier-like tokens) exposing the app's dependency structure. String-pool entries are byte-pattern observations, not deserialized cluster semantics; typed class and function recovery needs an SDK-specific parser. Omitted or complete detail returns the full observation. Select detail: \"summary\" for target/ABI/library identity, all snapshot headers, coverage and original lower-bound counts without string lists. The complete source stays retained; read returned strings via inspect_analysis_view with parent_evidence_id and kind dart-aot, collection package_uris/dart_uris/class_like_tokens, offset and limit. Exhausting pages does not imply complete pool coverage. No engine, no execution.", + 'Inspect one ABI\'s legacy split VM/isolate Dart AOT snapshot inside an APK (complete structural coverage: Dart 2.14 FullAOT kind 3). Unified _kDartSnapshotData/_kDartSnapshotText profiles are unsupported. Returns the four snapshot exports through the ELF dynamic symbol table with offsets, sizes and headers (magic, kind, length), snapshot hash, and a bounded string-pool projection from isolate data (package: source URIs, dart: SDK URIs, identifier-like tokens). String-pool entries are byte-pattern observations, not deserialized cluster semantics; typed class and function recovery needs an SDK-specific parser. Omitted or complete detail returns the full observation. Select detail: "summary" for target/ABI/library identity, all snapshot headers, coverage and original lower-bound counts without string lists. The complete source stays retained; read returned strings via inspect_analysis_view with parent_evidence_id and kind dart-aot, collection package_uris/dart_uris/class_like_tokens, offset and limit. Exhausting pages does not imply complete pool coverage. No engine, no execution.', kind: "flutter-provider", inputSchema: dartAotInspectionInputSchema, outputSchema: evidenceResultOf( diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts index 6fc1eadd27..b4b5ea7fd4 100644 --- a/src/flutter/FlutterBuildProvider.ts +++ b/src/flutter/FlutterBuildProvider.ts @@ -932,7 +932,7 @@ export class FlutterBuildProvider { ) .map( (entry) => - `lib/${entry.abi}/libapp.so carries ${String(entry.libapp.snapshot_hash_sources)} snapshot section(s); a complete AOT payload carries two, so the library is truncated or nonstandard`, + `lib/${entry.abi}/libapp.so carries ${String(entry.libapp.snapshot_hash_sources)} observed snapshot header(s); complete coverage of the supported legacy split VM/isolate profile requires two independent sources. Unified and other layouts remain unverified`, ), ...abis .filter( @@ -1137,14 +1137,42 @@ export class FlutterBuildProvider { section.offset < previous.offset + previous.size, ), ); - if (sections.length === 0) + if (sections.length === 0) { + // This is a profile diagnostic, not aliases for the split data sources. + // Probe only after the legacy selection fails, leaving its normal path + // and two-source completeness rule unchanged. + const unified = await readElfSymbols( + bounded, + ["_kDartSnapshotData", "_kDartSnapshotText"], + signal, + ); + if (unified.symbols.length > 0) + return err( + new AnalysisUnsupportedTargetError( + "inspect_dart_aot", + path, + "Observed unified Dart snapshot exports; this reader supports the legacy split VM/isolate profile (Dart 2.14 FullAOT kind 3). The unified profile is unsupported, not missing legacy data", + { + diagnostics: { + supported_profile: "dart-2.14-split-full-aot", + observed_exports: unified.symbols.map( + ({ name, offset, size }) => ({ name, offset, size }), + ), + abi: selected.abi, + libapp_sha256: imageSha256, + libapp_bytes: imageLength, + }, + }, + ), + ); return err( new AnalysisUnsupportedTargetError( "inspect_dart_aot", path, - `No recognizable Dart snapshot symbols were found: ${inventory.failures.map((failure) => failure.reason).join("; ")}`, + `No recognizable legacy split Dart snapshot symbols were found: ${inventory.failures.map((failure) => failure.reason).join("; ")}`, ), ); + } const pool = await projectStringPool(isolateData, signal); const snapshotPartial = inventory.failures.length > 0 || diff --git a/tests/boundary/filesystem/flutterBuildProvider.test.ts b/tests/boundary/filesystem/flutterBuildProvider.test.ts index 12c09649fa..a4268978cb 100644 --- a/tests/boundary/filesystem/flutterBuildProvider.test.ts +++ b/tests/boundary/filesystem/flutterBuildProvider.test.ts @@ -112,6 +112,71 @@ const analyzeImage = async (bytes: Buffer, operation: FlutterOperation) => { return result.value; }; +it.each([false, true])( + "qualifies unified snapshot exports without aliasing legacy sources (text=%s)", + async (includeText) => { + const symbols = [ + { name: "_kDartSnapshotData", value: 0x1000, size: 52 }, + ...(includeText + ? [{ name: "_kDartSnapshotText", value: 0x1100, size: 16 }] + : []), + ]; + const image = Buffer.alloc(0x1110); + buildElf64Image(symbols).copy(image); + const data = snapshotSection(REFERENCE_HASH); + data.writeBigUInt64LE(2n, 12); // Pinned unified producer enum; support is not inferred. + data.copy(image, 0x1000); + const path = await buildApk("unified.apk", [ + { path: "lib/arm64-v8a/libapp.so", bytes: image }, + ]); + const provider = new FlutterBuildProvider(); + onTestFinished(() => provider.close()); + const inspected = await provider.execute({ + operation: "inspect_dart_aot", + input: { path }, + }); + expect(inspected.ok).toBe(false); + if (inspected.ok) + throw new Error("Unified profile was admitted as split VM/isolate"); + expect(projectAnalysisError(inspected.error)).toMatchObject({ + code: "target_unavailable", + category: "unsupported_target", + details: { + diagnostics: { + supported_profile: "dart-2.14-split-full-aot", + observed_exports: symbols.map(({ name, value, size }) => ({ + name, + offset: value, + size, + })), + abi: "arm64-v8a", + libapp_bytes: image.length, + libapp_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u), + }, + }, + }); + const identified = await provider.execute({ + operation: "identify_flutter_build", + input: { path }, + }); + if (!identified.ok) throw identified.error; + const value = flutterResultSchemas.identify_flutter_build.parse( + identified.value.result, + ); + expect(value.coverage).toBe("partial"); + expect(value.abis[0]?.libapp).toMatchObject({ + snapshot_hash: REFERENCE_HASH, + snapshot_hash_sources: 1, + }); + expect(identified.value.limitations.join(" ")).toContain( + "supported legacy split VM/isolate profile", + ); + expect(identified.value.limitations.join(" ")).not.toContain( + "library is truncated", + ); + }, +); + it.each([32, 64] as const)( "reads only ELF-owned notes in an ELF%s engine", async (bits) => {