mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(flutter): derive dynamic symbol tables from their source
Decode only consumed dynamic-entry payloads. Discover PT_DYNAMIC symbols when retained sections omit SHT_DYNSYM, with table provenance governing section-index applicability. Preserve malformed present-table failures and shared source fixtures. Fixes #6851 Fixes #6884
This commit is contained in:
@@ -154,9 +154,16 @@ server; older servers keep the complete request after schema discovery.
|
||||
- **Selected ABI.** AOT inspection applies the library budget to the selected
|
||||
ABI after selection; an oversized unselected library does not block it.
|
||||
Undefined dynamic-symbol imports are not treated as snapshot exports.
|
||||
A retained section directory without `SHT_DYNSYM` uses the same bounded
|
||||
`PT_DYNAMIC` symbol description as a section-stripped image. Malformed
|
||||
present symbol-table sections remain failures.
|
||||
Section-stripped images require a bounded symbol count from `DT_HASH` or
|
||||
`DT_GNU_HASH`; layouts without either are refused. Symbol names and their
|
||||
terminators must fit their linked string table.
|
||||
terminators must fit their linked string table. Only dynamic entries used for
|
||||
symbol discovery are interpreted as numeric bounds. ABI-ignored payloads,
|
||||
including [`DT_BIND_NOW`](https://gabi.xinuos.com/elf/08-dynamic.html#dynamic-section),
|
||||
do not change discovery; their stored bytes still contribute to the selected
|
||||
artifact's digest.
|
||||
- REA performs **no hash-to-SDK lookup**: mapping the snapshot hash to a
|
||||
Dart SDK release requires an external dataset and is deliberately out of
|
||||
scope for this family.
|
||||
|
||||
@@ -40,11 +40,16 @@ const imageWithSections = (): Buffer => {
|
||||
};
|
||||
|
||||
/** The same layout without section headers: symbols live in PT_DYNAMIC. */
|
||||
const strippedImageWithSections = (): Buffer => {
|
||||
const elf = buildStrippedElf64Image([
|
||||
{ name: "_kDartVmSnapshotData", value: 0x400, size: 52 },
|
||||
{ name: "_kDartIsolateSnapshotData", value: 0x800, size: 52 },
|
||||
]);
|
||||
const strippedImageWithSections = (
|
||||
extraDynamicEntries: ReadonlyArray<readonly [number, bigint]> = [],
|
||||
): Buffer => {
|
||||
const elf = buildStrippedElf64Image(
|
||||
[
|
||||
{ name: "_kDartVmSnapshotData", value: 0x400, size: 52 },
|
||||
{ name: "_kDartIsolateSnapshotData", value: 0x800, size: 52 },
|
||||
],
|
||||
extraDynamicEntries,
|
||||
);
|
||||
const section = snapshotSection(GALLERY_HASH);
|
||||
return Buffer.concat([
|
||||
elf,
|
||||
@@ -90,6 +95,38 @@ describe("readElfSymbol", () => {
|
||||
expect(header.kind).toBe(3);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[16, "DT_SYMBOLIC"],
|
||||
[22, "DT_TEXTREL"],
|
||||
[24, "DT_BIND_NOW"],
|
||||
] as const)(
|
||||
"ignores the payload of %s (%s) without losing symbols",
|
||||
async (tag, _name) => {
|
||||
const names = ["_kDartVmSnapshotData", "_kDartIsolateSnapshotData"];
|
||||
const control = await readElfSymbols(
|
||||
strippedImageWithSections([[tag, 0n]]),
|
||||
names,
|
||||
);
|
||||
expect(control.failures).toEqual([]);
|
||||
expect(control.symbols).toHaveLength(2);
|
||||
const image = strippedImageWithSections([[tag, 0xffffffffffffffffn]]);
|
||||
const original = Buffer.from(image);
|
||||
expect(await readElfSymbols(image, names)).toEqual(control);
|
||||
expect(image).toEqual(original);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([4, 5, 6, 10, 11, 0x6ffffef5])(
|
||||
"keeps representability checks for consumed dynamic tag %s",
|
||||
async (tag) => {
|
||||
const image = strippedImageWithSections([[tag, 0xffffffffffffffffn]]);
|
||||
expect(await readElfSymbols(image, ["_kDartVmSnapshotData"])).toEqual({
|
||||
symbols: [],
|
||||
failures: [{ reason: "Dynamic entry is not representable" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a stripped image whose PT_DYNAMIC carries no symbols", async () => {
|
||||
// Drop the PT_DYNAMIC program header by leaving only PT_LOAD.
|
||||
const image = strippedImageWithSections();
|
||||
@@ -144,6 +181,53 @@ describe("readElfSymbol", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("dynamic symbols without a SHT_DYNSYM section", () => {
|
||||
it.each([false, true])(
|
||||
"uses PT_DYNAMIC with a retained directory (data sections: %s)",
|
||||
async (retainDataSections) => {
|
||||
const original = strippedImageWithSections();
|
||||
const image = appendElfRanges(
|
||||
original,
|
||||
"section",
|
||||
retainDataSections
|
||||
? [
|
||||
{ type: 1, offset: 0x400, size: 52 },
|
||||
{ type: 1, offset: 0x800, size: 52 },
|
||||
]
|
||||
: [],
|
||||
);
|
||||
const names = ["_kDartVmSnapshotData", "_kDartIsolateSnapshotData"];
|
||||
const control = await readElfSymbols(original, names);
|
||||
expect(control.failures).toEqual([]);
|
||||
expect(control.symbols).toHaveLength(2);
|
||||
const before = Buffer.from(image);
|
||||
expect(await readElfSymbols(image, names)).toEqual(control);
|
||||
expect(image).toEqual(before);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps malformed present dynsym sections as failures despite usable PT_DYNAMIC", async () => {
|
||||
const image = appendElfRanges(strippedImageWithSections(), "section", [
|
||||
{ type: 11, offset: 0, size: 0 },
|
||||
]);
|
||||
expect(await readElfSymbols(image, ["_kDartVmSnapshotData"])).toEqual({
|
||||
symbols: [],
|
||||
failures: [{ reason: "Dynamic symbol table is malformed" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bounded dynamic metadata requirements with a retained directory", async () => {
|
||||
const image = appendElfRanges(strippedImageWithSections(), "section", []);
|
||||
// The first entry is DT_SYMTAB, whose address must remain representable.
|
||||
const dynamic = Number(image.readBigUInt64LE(64 + 56 + 8));
|
||||
image.writeBigUInt64LE(0xffffffffffffffffn, dynamic + 8);
|
||||
expect(await readElfSymbols(image, ["_kDartVmSnapshotData"])).toEqual({
|
||||
symbols: [],
|
||||
failures: [{ reason: "Dynamic entry is not representable" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ELF symbol-table ownership", () => {
|
||||
it.each([
|
||||
{ address: 0x400, offset: 0x700, size: 52 },
|
||||
|
||||
+35
-17
@@ -65,10 +65,12 @@ const PT_DYNAMIC = 2;
|
||||
/** Position-independent executable images carry PT_DYNAMIC. */
|
||||
const ET_DYN = 3;
|
||||
const DT_NULL = 0;
|
||||
const DT_HASH = 4;
|
||||
const DT_STRTAB = 5;
|
||||
const DT_SYMTAB = 6;
|
||||
const DT_STRSZ = 10;
|
||||
const DT_SYMENT = 11;
|
||||
const DT_GNU_HASH = 0x6ffffef5;
|
||||
|
||||
/** ELF32 needs 52 bytes, ELF64 needs 64; retain at most this prefix. */
|
||||
export const ELF_HEADER_MAX_BYTES = 64;
|
||||
@@ -115,17 +117,18 @@ export const readElfHeader = (
|
||||
count > 0 &&
|
||||
count <= Math.floor((totalBytes - offset) / width);
|
||||
const MIN_SHENT = is64 ? 64 : 40;
|
||||
// Section-stripped ET_DYN images keep their exported symbols reachable
|
||||
// through PT_DYNAMIC instead of section headers; every other shape still
|
||||
// needs a usable section header table.
|
||||
// Section-stripped ET_DYN images may omit the section directory. This
|
||||
// describes header availability, not which symbol-table source is usable.
|
||||
// A nonzero shoff with shnum=0 uses extended numbering, which this
|
||||
// parser does not support; it must not masquerade as a stripped image.
|
||||
const dynamicFallback = e_type === ET_DYN && e_shoff === 0 && e_shnum === 0;
|
||||
const sectionTableAbsent =
|
||||
e_type === ET_DYN && e_shoff === 0 && e_shnum === 0;
|
||||
if (
|
||||
e_shoff === null ||
|
||||
e_shentsize === null ||
|
||||
e_shnum === null ||
|
||||
(!dynamicFallback && !tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT))
|
||||
(!sectionTableAbsent &&
|
||||
!tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT))
|
||||
)
|
||||
return {
|
||||
failure: {
|
||||
@@ -149,7 +152,7 @@ export const readElfHeader = (
|
||||
return {
|
||||
header: {
|
||||
is64,
|
||||
dynamicFallback,
|
||||
sectionTableAbsent,
|
||||
e_shoff,
|
||||
e_shentsize,
|
||||
e_shnum,
|
||||
@@ -180,7 +183,7 @@ export const readElfSymbols = async (
|
||||
if (parsed.failure !== undefined) return failure(parsed.failure.reason);
|
||||
const {
|
||||
is64,
|
||||
dynamicFallback,
|
||||
sectionTableAbsent,
|
||||
e_shoff,
|
||||
e_shentsize,
|
||||
e_shnum,
|
||||
@@ -221,6 +224,7 @@ export const readElfSymbols = async (
|
||||
if (segments.length === 0)
|
||||
return failure("ELF image has no PT_LOAD segments");
|
||||
const tables: {
|
||||
source: "section" | "dynamic-segment";
|
||||
offset: number;
|
||||
count: number;
|
||||
stringOffset: number;
|
||||
@@ -248,7 +252,7 @@ export const readElfSymbols = async (
|
||||
? undefined
|
||||
: "section offset disagrees with its PT_LOAD mapping";
|
||||
};
|
||||
if (!dynamicFallback) {
|
||||
if (!sectionTableAbsent) {
|
||||
for (let index = 0; index < e_shnum; index += 1) {
|
||||
if (index % 4096 === 0) {
|
||||
await yieldToEventLoop();
|
||||
@@ -303,13 +307,18 @@ export const readElfSymbols = async (
|
||||
if (stringMapping !== undefined)
|
||||
return failure(`Dynamic symbol string table ${stringMapping}`);
|
||||
tables.push({
|
||||
source: "section",
|
||||
offset,
|
||||
count: size / entrySize,
|
||||
stringOffset,
|
||||
stringSize,
|
||||
});
|
||||
}
|
||||
} else if (dynamicSegment !== null) {
|
||||
}
|
||||
// A retained section directory may omit SHT_DYNSYM while PT_DYNAMIC still
|
||||
// describes the runtime symbols. A malformed present section already failed
|
||||
// above; it must not be bypassed by an alternative interpretation.
|
||||
if (tables.length === 0 && dynamicSegment !== null) {
|
||||
const tags = new Map<number, number>();
|
||||
const dynamicEntrySize = is64 ? 16 : 8;
|
||||
let terminated = false;
|
||||
@@ -330,14 +339,22 @@ export const readElfSymbols = async (
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
const value = is64 ? reader.u64(base + 8) : reader.u32(base + 4);
|
||||
if (value === null) return failure("Dynamic entry is not representable");
|
||||
// Interpret d_un only for the symbol-table description consumed here.
|
||||
// In particular, DT_SYMBOLIC, DT_TEXTREL and DT_BIND_NOW have ignored
|
||||
// payloads under the ELF ABI; their arbitrary bits are not numeric bounds.
|
||||
if (
|
||||
![DT_SYMTAB, DT_STRTAB, DT_STRSZ, DT_SYMENT, 4, 0x6ffffef5].includes(
|
||||
tag,
|
||||
)
|
||||
![
|
||||
DT_SYMTAB,
|
||||
DT_STRTAB,
|
||||
DT_STRSZ,
|
||||
DT_SYMENT,
|
||||
DT_HASH,
|
||||
DT_GNU_HASH,
|
||||
].includes(tag)
|
||||
)
|
||||
continue;
|
||||
const value = is64 ? reader.u64(base + 8) : reader.u32(base + 4);
|
||||
if (value === null) return failure("Dynamic entry is not representable");
|
||||
if (tags.has(tag) && tags.get(tag) !== value)
|
||||
return failure("Conflicting dynamic table tags");
|
||||
tags.set(tag, value);
|
||||
@@ -358,8 +375,8 @@ export const readElfSymbols = async (
|
||||
// DT_HASH's nchain is the dynsym count. GNU hash instead records the
|
||||
// first hashed index and bounded buckets/chains. No payload-to-EOF walk.
|
||||
let count: number;
|
||||
const sysv = tags.get(4);
|
||||
const gnu = tags.get(0x6ffffef5);
|
||||
const sysv = tags.get(DT_HASH);
|
||||
const gnu = tags.get(DT_GNU_HASH);
|
||||
if (sysv !== undefined) {
|
||||
const at = toOffset(sysv, 8);
|
||||
if (!at.ok) return failure(`DT_HASH header ${at.error.reason}`);
|
||||
@@ -420,6 +437,7 @@ export const readElfSymbols = async (
|
||||
if (!stringOffset.ok)
|
||||
return failure(`Dynamic string table ${stringOffset.error.reason}`);
|
||||
tables.push({
|
||||
source: "dynamic-segment",
|
||||
offset: offset.value,
|
||||
count,
|
||||
stringOffset: stringOffset.value,
|
||||
@@ -444,7 +462,7 @@ export const readElfSymbols = async (
|
||||
sectionIndex === 0xfff1 ||
|
||||
(sectionIndex > 0 &&
|
||||
sectionIndex < 0xff00 &&
|
||||
(dynamicFallback || sectionIndex < e_shnum));
|
||||
(table.source === "dynamic-segment" || sectionIndex < e_shnum));
|
||||
if (!defined || nameOffset >= table.stringSize) continue;
|
||||
const name = reader.cString(
|
||||
table.stringOffset + nameOffset,
|
||||
|
||||
@@ -179,13 +179,14 @@ export const buildElf32Image = (
|
||||
*/
|
||||
export const buildStrippedElf64Image = (
|
||||
symbols: ReadonlyArray<{ name: string; value: number; size: number }>,
|
||||
extraDynamicEntries: ReadonlyArray<readonly [number, bigint]> = [],
|
||||
): Buffer => {
|
||||
const ELF_HEADER = 64;
|
||||
const PHENT = 56;
|
||||
const PHNUM = 2;
|
||||
const dynamicEntry = 16;
|
||||
// DT_SYMTAB, DT_STRTAB, DT_STRSZ, DT_SYMENT, DT_HASH, DT_NULL.
|
||||
const DYNAMIC_ENTRIES = 6;
|
||||
const DYNAMIC_ENTRIES = 6 + extraDynamicEntries.length;
|
||||
const phoff = ELF_HEADER;
|
||||
const dynamicOff = phoff + PHENT * PHNUM;
|
||||
const dynsymOff = dynamicOff + DYNAMIC_ENTRIES * dynamicEntry;
|
||||
@@ -228,12 +229,13 @@ export const buildStrippedElf64Image = (
|
||||
image.writeBigUInt64LE(BigInt(dynamicOff), dyn + 8); // p_offset
|
||||
image.writeBigUInt64LE(BigInt(dynamicOff), dyn + 0x10); // p_vaddr
|
||||
image.writeBigUInt64LE(BigInt(DYNAMIC_ENTRIES * dynamicEntry), dyn + 0x20);
|
||||
const pairs: ReadonlyArray<readonly [number, number]> = [
|
||||
const pairs: ReadonlyArray<readonly [number, number | bigint]> = [
|
||||
[6, dynsymOff],
|
||||
[5, dynstrOff],
|
||||
[10, strtabBytes],
|
||||
[11, 24],
|
||||
[4, hashOff],
|
||||
...extraDynamicEntries,
|
||||
];
|
||||
pairs.forEach(([tag, value], index) => {
|
||||
const base = dynamicOff + index * dynamicEntry;
|
||||
@@ -259,6 +261,38 @@ export const buildStrippedElf64Image = (
|
||||
return image;
|
||||
};
|
||||
|
||||
/** Four snapshot exports and their pool, discovered without section headers. */
|
||||
export const strippedAotFixtureImage = (
|
||||
extraDynamicEntries: ReadonlyArray<readonly [number, bigint]> = [],
|
||||
): Buffer => {
|
||||
const symbols = buildStrippedElf64Image(
|
||||
[
|
||||
{ name: "_kDartVmSnapshotData", value: 0x1000, size: 52 },
|
||||
{ name: "_kDartVmSnapshotInstructions", value: 0x1800, size: 16 },
|
||||
{ name: "_kDartIsolateSnapshotData", value: 0x2000, size: 0x600 },
|
||||
{ name: "_kDartIsolateSnapshotInstructions", value: 0x2600, size: 16 },
|
||||
],
|
||||
extraDynamicEntries,
|
||||
);
|
||||
const vmSection = snapshotSection(REFERENCE_HASH);
|
||||
const isolateSection = Buffer.concat([
|
||||
snapshotSection(REFERENCE_HASH),
|
||||
Buffer.from([0]),
|
||||
Buffer.from("package:example/main.dart\0dart:core\0Example\0"),
|
||||
]);
|
||||
return Buffer.concat([
|
||||
symbols,
|
||||
Buffer.alloc(0x1000 - symbols.length),
|
||||
vmSection,
|
||||
Buffer.alloc(0x1800 - 0x1000 - vmSection.length),
|
||||
Buffer.alloc(16),
|
||||
Buffer.alloc(0x2000 - 0x1800 - 16),
|
||||
isolateSection,
|
||||
Buffer.alloc(0x600 - isolateSection.length),
|
||||
Buffer.alloc(16),
|
||||
]);
|
||||
};
|
||||
|
||||
/** A minimal Dart snapshot data section carrying a 32-hex hash. */
|
||||
export const snapshotSection = (hash: string): Buffer =>
|
||||
Buffer.concat([
|
||||
|
||||
@@ -6,6 +6,7 @@ import { parseEvidence } from "../../src/domain/evidence.js";
|
||||
import { flutterResultSchemas } from "../../src/domain/flutter/flutterBuildAnalysis.js";
|
||||
import {
|
||||
aotFixtureImage,
|
||||
strippedAotFixtureImage,
|
||||
engineFixtureImage,
|
||||
REFERENCE_HASH,
|
||||
appendElfRanges,
|
||||
@@ -18,6 +19,61 @@ import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"CLI and stdio MCP preserve AOT facts and exact artifact identity across ignored DT_BIND_NOW values",
|
||||
async ({ cli }) => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const reports = [];
|
||||
const digests = [];
|
||||
for (const ignoredValue of [0n, 0xffffffffffffffffn]) {
|
||||
const image = strippedAotFixtureImage([[24, ignoredValue]]);
|
||||
const path = await createFlutterApk(
|
||||
`bind-now-${String(ignoredValue)}.apk`,
|
||||
[{ path: "lib/x86_64/libapp.so", bytes: image }],
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-dart-aot", path, "--abi", "x86_64", "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_dart_aot").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
const report = flutterResultSchemas.inspect_dart_aot.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const response = await call("inspect_dart_aot", { path, abi: "x86_64" });
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
expect(parseEvidence(response.structuredContent)).toEqual(evidence);
|
||||
const digest = createHash("sha256").update(image).digest("hex");
|
||||
expect(report).toMatchObject({
|
||||
coverage: "complete",
|
||||
libapp: {
|
||||
sha256: digest,
|
||||
coverage: "complete",
|
||||
snapshot_hash: REFERENCE_HASH,
|
||||
snapshot_hash_sources: 2,
|
||||
},
|
||||
string_pool: {
|
||||
coverage: "complete",
|
||||
package_uris: ["package:example/main.dart"],
|
||||
},
|
||||
});
|
||||
expect(report.libapp.sections).toHaveLength(4);
|
||||
expect(evidence.subject).toMatchObject({
|
||||
local_path: path,
|
||||
digest: { sha256: report.target.sha256 },
|
||||
});
|
||||
reports.push({
|
||||
sections: report.libapp.sections,
|
||||
string_pool: report.string_pool,
|
||||
});
|
||||
digests.push(digest);
|
||||
}
|
||||
expect(reports[1]).toEqual(reports[0]);
|
||||
expect(digests[1]).not.toBe(digests[0]);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"CLI and stdio MCP retain Flutter identity, selected ABI and duplicate-member refusals",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { FlutterBuildAnalysisService } from "../../../src/application/flutter/FlutterBuildAnalysisService.js";
|
||||
@@ -14,40 +15,14 @@ import { flutterResultSchemas } from "../../../src/domain/flutter/flutterBuildAn
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
import {
|
||||
REFERENCE_HASH,
|
||||
appendElfRanges,
|
||||
aotFixtureImage,
|
||||
buildStrippedElf64Image,
|
||||
snapshotSection,
|
||||
strippedAotFixtureImage,
|
||||
} from "../../../src/flutter/FlutterBuild.fixture.js";
|
||||
import { createFlutterApk } from "../../fixtures/flutterApk.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
const strippedAotImage = (): Buffer => {
|
||||
const symbols = buildStrippedElf64Image([
|
||||
{ name: "_kDartVmSnapshotData", value: 0x1000, size: 52 },
|
||||
{ name: "_kDartVmSnapshotInstructions", value: 0x1800, size: 16 },
|
||||
{ name: "_kDartIsolateSnapshotData", value: 0x2000, size: 0x600 },
|
||||
{ name: "_kDartIsolateSnapshotInstructions", value: 0x2600, size: 16 },
|
||||
]);
|
||||
const vmSection = snapshotSection(REFERENCE_HASH);
|
||||
const isolateSection = Buffer.concat([
|
||||
snapshotSection(REFERENCE_HASH),
|
||||
Buffer.from([0]),
|
||||
Buffer.from("package:example/main.dart\0dart:core\0Example\0"),
|
||||
]);
|
||||
return Buffer.concat([
|
||||
symbols,
|
||||
Buffer.alloc(0x1000 - symbols.length),
|
||||
vmSection,
|
||||
Buffer.alloc(0x1800 - 0x1000 - vmSection.length),
|
||||
Buffer.alloc(16),
|
||||
Buffer.alloc(0x2000 - 0x1800 - 16),
|
||||
isolateSection,
|
||||
Buffer.alloc(0x600 - isolateSection.length),
|
||||
Buffer.alloc(16),
|
||||
]);
|
||||
};
|
||||
|
||||
const dynamicSegmentOffset = (image: Buffer): number =>
|
||||
Number(image.readBigUInt64LE(64 + 56 + 8));
|
||||
|
||||
@@ -333,14 +308,24 @@ it("rejects unavailable retention, invalid delivery and cancellation before prod
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([0n, 0xffffffffffffffffn])(
|
||||
"accepts a complete PT_DYNAMIC DT_NULL entry with ignored value %s through MCP",
|
||||
async (ignoredValue) => {
|
||||
const image = strippedAotImage();
|
||||
it.each([
|
||||
{ ignoredValue: 0n, retainedSections: false },
|
||||
{ ignoredValue: 0xffffffffffffffffn, retainedSections: false },
|
||||
{ ignoredValue: 0n, retainedSections: true },
|
||||
])(
|
||||
"accepts bounded PT_DYNAMIC through MCP (ignored value: $ignoredValue, retained sections: $retainedSections)",
|
||||
async ({ ignoredValue, retainedSections }) => {
|
||||
const image = strippedAotFixtureImage();
|
||||
const dynamic = dynamicSegmentOffset(image);
|
||||
image.writeBigUInt64LE(ignoredValue, dynamic + 5 * 16 + 8);
|
||||
const selected = retainedSections
|
||||
? appendElfRanges(image, "section", [
|
||||
{ type: 1, offset: 0x1000, size: 52 },
|
||||
{ type: 1, offset: 0x2000, size: 0x600 },
|
||||
])
|
||||
: image;
|
||||
const path = await createFlutterApk("dt-null.apk", [
|
||||
{ path: "lib/arm64-v8a/libapp.so", bytes: image },
|
||||
{ path: "lib/arm64-v8a/libapp.so", bytes: selected },
|
||||
]);
|
||||
const provider = createFlutterBuildAnalysisProvider();
|
||||
onTestFinished(() => provider.close());
|
||||
@@ -360,6 +345,12 @@ it.each([0n, 0xffffffffffffffffn])(
|
||||
coverage: "complete",
|
||||
snapshot_hash: REFERENCE_HASH,
|
||||
snapshot_hash_sources: 2,
|
||||
sha256: createHash("sha256").update(selected).digest("hex"),
|
||||
});
|
||||
expect(report.libapp.sections).toHaveLength(4);
|
||||
expect(report.string_pool).toMatchObject({
|
||||
coverage: "complete",
|
||||
package_uris: ["package:example/main.dart"],
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -378,7 +369,7 @@ it.each([
|
||||
])(
|
||||
"refuses unusable dynamic tables for $label",
|
||||
async ({ truncate, reason }) => {
|
||||
const image = strippedAotImage();
|
||||
const image = strippedAotFixtureImage();
|
||||
const dynamic = dynamicSegmentOffset(image);
|
||||
if (truncate) {
|
||||
image.writeBigUInt64LE(5n * 16n + 15n, 64 + 56 + 0x20);
|
||||
|
||||
Reference in New Issue
Block a user