fix(flutter): qualify unsupported unified snapshot profiles

This commit is contained in:
morluto
2026-10-24 04:26:42 +08:00
parent 094d39926b
commit 9cd46d6192
4 changed files with 106 additions and 5 deletions
+8
View File
@@ -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
@@ -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/<abi>/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/<abi>/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(
+31 -3
View File
@@ -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 ||
@@ -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) => {