mirror of
https://github.com/morluto/rea
synced 2026-10-25 14:01:13 -04:00
fix(flutter): qualify unsupported unified snapshot profiles
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user