fix(flutter): qualify unsupported unified snapshot profiles

This commit is contained in:
morluto committed 2026-10-24 04:28:54 +08:00
1 parent 094d39926b
commit 9cd46d6192
4 files changed
+106 -5

No files matched your search

+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). [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 Kind numbers vary across Dart releases; other values are preserved with
partial coverage, without guessing their interpretation. 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, Address resolution examines every overlapping `PT_LOAD` interpretation,
including partial overlaps. Equivalent mappings are coalesced; conflicting including partial overlaps. Equivalent mappings are coalesced; conflicting
mappings leave affected symbols unresolved and qualify coverage. Dynamic 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 operation, and when they **disagree** the distinct values are
kept in `snapshot_hash_candidates` with `snapshot_hash: null` and kept in `snapshot_hash_candidates` with `snapshot_hash: null` and
`coverage: "partial"` rather than silently picking one. `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 - **`elf_header_valid`** checks the supported little-endian ELF32/ELF64
header, table entry widths, and full table extents against the library's header, table entry widths, and full table extents against the library's
actual byte length. Truncated, malformed, or unsupported headers report actual byte length. Truncated, malformed, or unsupported headers report
@@ -15,7 +15,7 @@ export const FLUTTER_TOOL_CONTRACTS = [
name: "identify_flutter_build", name: "identify_flutter_build",
...toolContractMetadata("identify_flutter_build"), ...toolContractMetadata("identify_flutter_build"),
description: 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", kind: "flutter-provider",
inputSchema: flutterInputSchemas.identify_flutter_build, inputSchema: flutterInputSchemas.identify_flutter_build,
outputSchema: evidenceResultOf(flutterResultSchemas.identify_flutter_build), outputSchema: evidenceResultOf(flutterResultSchemas.identify_flutter_build),
@@ -30,7 +30,7 @@ export const FLUTTER_TOOL_CONTRACTS = [
name: "inspect_dart_aot", name: "inspect_dart_aot",
...toolContractMetadata("inspect_dart_aot"), ...toolContractMetadata("inspect_dart_aot"),
description: 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", kind: "flutter-provider",
inputSchema: dartAotInspectionInputSchema, inputSchema: dartAotInspectionInputSchema,
outputSchema: evidenceResultOf( outputSchema: evidenceResultOf(
+31 -3
View File
@@ -932,7 +932,7 @@ export class FlutterBuildProvider {
) )
.map( .map(
(entry) => (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 ...abis
.filter( .filter(
@@ -1137,14 +1137,42 @@ export class FlutterBuildProvider {
section.offset < previous.offset + previous.size, 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( return err(
new AnalysisUnsupportedTargetError( new AnalysisUnsupportedTargetError(
"inspect_dart_aot", "inspect_dart_aot",
path, 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 pool = await projectStringPool(isolateData, signal);
const snapshotPartial = const snapshotPartial =
inventory.failures.length > 0 || inventory.failures.length > 0 ||
@@ -112,6 +112,71 @@ const analyzeImage = async (bytes: Buffer, operation: FlutterOperation) => {
return result.value; 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)( it.each([32, 64] as const)(
"reads only ELF-owned notes in an ELF%s engine", "reads only ELF-owned notes in an ELF%s engine",
async (bits) => { async (bits) => {