[GH-ISSUE #6768] [Bug] inspect_dart_aot rejects ELF64 DT_NULL entries with large ignored values #4125

Closed
opened 2026-10-31 00:46:59 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6768

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

FlutterBuildAnalysisService.execute('inspect_dart_aot', {path, abi: 'x86_64'}) rejects an otherwise accepted section-stripped ELF64 AOT fixture when the ignored d_un word of its terminating DT_NULL entry is 0xffffffffffffffff. The result is AnalysisUnsupportedTargetError with No recognizable Dart snapshot symbols were found: Dynamic entry is not representable. With that one unused word set to zero, the same fixture returns all four snapshot symbols, its agreeing snapshot hash, two package URIs, and complete coverage.

These are executed shared-application/provider observations using inert synthetic APKs. CLI/MCP transports, a packaged installation, and an executable Flutter engine were not run.

Reproduction

From the source checkout below with its locked dependencies present, save the script as /tmp/rea-flutter-probe.mts and run ./node_modules/.bin/tsx /tmp/rea-flutter-probe.mts. It uses the existing AOT and stripped-ELF fixture builders, supplies a null symbol and a linked SysV hash chain, and creates both APKs under a disposable /tmp root. No target code executes.

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
const root = process.env.REPRO_ROOT ?? '/tmp/rea-flutter-repro';
await mkdir(root, { recursive: true });
process.env.TMPDIR = root;
const source = (p: string) => import(pathToFileURL(join(process.cwd(), p)).href);
const require = createRequire(join(process.cwd(), 'package.json'));
const { ZipWriter, Uint8ArrayWriter, Uint8ArrayReader } = await import(pathToFileURL(require.resolve('@zip.js/zip.js')).href);
const { aotFixtureImage, buildStrippedElf64Image } = await source('src/flutter/FlutterBuild.fixture.ts');
const { FlutterBuildProvider } = await source('src/flutter/FlutterBuildProvider.ts');
const { FlutterBuildAnalysisService } = await source('src/application/flutter/FlutterBuildAnalysisService.ts');
const declarations = [
  { name: '', value: 0, size: 0 },
  { 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 results = [];
for (const [label, ignored] of [['control', 0n], ['ignored-high-word', 0xffffffffffffffffn]] as const) {
  const image = aotFixtureImage();
  image.fill(0, 0, 0x1000);
  buildStrippedElf64Image(declarations).copy(image);
  const dynamic = Number(image.readBigUInt64LE(64 + 56 + 8));
  const symbols = Number(image.readBigUInt64LE(dynamic + 8));
  const hash = Number(image.readBigUInt64LE(dynamic + 4 * 16 + 8));
  image.writeUInt32LE(1, hash + 8); // one bucket owns all four exports
  for (let i = 1; i < declarations.length - 1; i++) image.writeUInt32LE(i + 1, hash + 12 + i * 4);
  image.fill(0, symbols, symbols + 24); // mandatory STN_UNDEF entry
  for (let i = 1; i < declarations.length; i++) image[symbols + i * 24 + 4] = 0x11; // STB_GLOBAL/STT_OBJECT
  image.writeUInt32LE(5, 64 + 4); // readable executable load
  image.writeBigUInt64LE(BigInt(image.length), 64 + 40); // p_memsz
  image.writeBigUInt64LE(0x1000n, 64 + 48); // p_align
  image.writeUInt32LE(4, 64 + 56 + 4); // readable PT_DYNAMIC
  image.writeBigUInt64LE(96n, 64 + 56 + 40);
  image.writeBigUInt64LE(8n, 64 + 56 + 48);
  image.writeBigUInt64LE(ignored, dynamic + 5 * 16 + 8);
  await writeFile(join(root, label + '.so'), image);
  const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false });
  await writer.add('lib/x86_64/libapp.so', new Uint8ArrayReader(image));
  const apk = join(root, label + '.apk');
  await writeFile(apk, await writer.close());
  const provider = new FlutterBuildProvider();
  try {
    const result = await new FlutterBuildAnalysisService(provider).execute('inspect_dart_aot', { path: apk, abi: 'x86_64' });
    const facts = result.ok ? {
      label, ok: true, coverage: result.value.normalized_result.coverage,
      hash: result.value.normalized_result.libapp.snapshot_hash,
      symbols: result.value.normalized_result.libapp.sections.length,
      package_uris: result.value.normalized_result.string_pool.package_uris,
    } : { label, ok: false, error_tag: result.error._tag, message: result.error.message };
    results.push(facts);
    console.log(JSON.stringify(facts));
  } finally { await provider.close(); }
}

The final fixture pair was executed once. An earlier exploratory pair also showed the same failure before the SysV hash chains were completed; the report relies on the completed pair above. GNU readelf independently inspected the failing ELF with readelf -W -d -sD /tmp/rea-flutter-repro/ignored-high-word.so and listed the terminating NULL entry and all four exports without an error.

Expected behavior

Ignore DT_NULL.d_un, terminate the dynamic-array walk, and return the same snapshot symbols, hash, package URI observations, and complete structural coverage as the zero-valued control, while retaining each fixture's own file identity.

The documented Flutter contract supports little-endian ELF64 dynamic-symbol inspection, including section-stripped images with a bounded DT_HASH or DT_GNU_HASH count. The ELF dynamic-section specification marks DT_NULL's d_un as ignored. Its contents therefore do not make this dynamic-table terminator invalid or require numeric interpretation.

Impact and scope

A valid unused metadata word prevents the entire AOT inspection, losing otherwise available headers and string-pool observations. Observed scope is one synthetic little-endian ELF64/x86-64 fixture and its zero-valued control. Real APK prevalence and other platforms were not measured. A manual readelf inspection recovered all four symbol records for the failing fixture, but recovery of the full REA observation without changing input identity was not evaluated.

REA version

Source revision 5f702dd6ccd9e8e505e224854b91678439c3b41f. The relevant src/flutter/DartElf.ts is byte-identical to the fetched rea-agents-6.7.0 release tag; this establishes source applicability only. The published package runtime was not executed.

Runtime and platform

Node.js v24.22.1; Linux x86_64; GNU readelf (GNU Binutils) 2.47. Source execution through the checkout's locked tsx dependency. No external analysis engine was started.

Target and artifact context

Two synthetic APKs, each containing lib/x86_64/libapp.so. Both have section-stripped ELF64 dynamic symbol/hash/string tables, four legacy Dart snapshot exports, and the existing complete Dart 2.14-layout snapshot payload. The only difference is the terminating DT_NULL entry's ignored eight-byte word. These are parser fixtures, not runnable Flutter applications.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: the documented Flutter contract and ELF dynamic-section rule linked above.
  • Failure source: the executed fixture comparison below and the dynamic-entry loop linked under Analysis.
  • Evidence provenance: observed; source reasoning and the external format rule are separately attributed.
  • Local verification: reproduced.
  • Reproduction completeness: complete.

Observed compact output, with the fixture root generalized:

{"label":"control","ok":true,"coverage":"complete","hash":"65817c30a78bb44c3dc3771876b6010a","symbols":4,"package_uris":["package:adaptive_breakpoints/src/adaptive_breakpoints.dart","package:animations/src/fade_scale_transition.dart"]}
{"label":"ignored-high-word","ok":false,"error_tag":"AnalysisUnsupportedTargetError","message":"Unsupported target for inspect_dart_aot at /tmp/rea-flutter-repro/ignored-high-word.apk: No recognizable Dart snapshot symbols were found: Dynamic entry is not representable"}

GNU readelf reported the dynamic terminator as (NULL) 0xffffffffffffffff and the four global object exports at offsets/addresses 0x1000, 0x1800, 0x2000, and 0x2600, with sizes 52, 16, 1536, and 16. It exited successfully.

Typed outcome or recovery expected

Return normal successful AOT Evidence for both cases, preserving four snapshot sections and the hash/string observations. As a restoration check, compare the two logical results while allowing their target/library digests and paths to differ. Bounds and representability checks for fields that actually carry table addresses or sizes must remain effective.

Analysis or suspected cause

Source-derived: the dynamic-entry loop decodes both the tag and d_un with the safe-integer reader, then rejects an unrepresentable value before recognizing DT_NULL. Thus the ignored terminator payload is treated as a required numeric fact. No implementation was changed or tested.

Additional context

Open issue #3629 concerns renamed snapshot exports. This fixture deliberately uses all four supported legacy exports, independently listed by readelf, so that is a different failure. Open issues #6697 and #6563 concern GNU build-ID notes during build identification; no engine library or build-ID parser participates here.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded valid-metadata compatibility failure blocks AOT inspection for this ELF64 input; real-world frequency and broader applicability are unknown.
  • Workaround status: unverified
  • Workaround basis: Readelf recovered the symbol records, but a practical workaround preserving the exact input and the complete REA observation was not established.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6768 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Native analysis or decompilation ## Observed behavior `FlutterBuildAnalysisService.execute('inspect_dart_aot', {path, abi: 'x86_64'})` rejects an otherwise accepted section-stripped ELF64 AOT fixture when the ignored `d_un` word of its terminating `DT_NULL` entry is `0xffffffffffffffff`. The result is `AnalysisUnsupportedTargetError` with `No recognizable Dart snapshot symbols were found: Dynamic entry is not representable`. With that one unused word set to zero, the same fixture returns all four snapshot symbols, its agreeing snapshot hash, two package URIs, and complete coverage. These are executed shared-application/provider observations using inert synthetic APKs. CLI/MCP transports, a packaged installation, and an executable Flutter engine were not run. ## Reproduction From the source checkout below with its locked dependencies present, save the script as `/tmp/rea-flutter-probe.mts` and run `./node_modules/.bin/tsx /tmp/rea-flutter-probe.mts`. It uses the existing AOT and stripped-ELF fixture builders, supplies a null symbol and a linked SysV hash chain, and creates both APKs under a disposable `/tmp` root. No target code executes. ```ts import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createRequire } from 'node:module'; const root = process.env.REPRO_ROOT ?? '/tmp/rea-flutter-repro'; await mkdir(root, { recursive: true }); process.env.TMPDIR = root; const source = (p: string) => import(pathToFileURL(join(process.cwd(), p)).href); const require = createRequire(join(process.cwd(), 'package.json')); const { ZipWriter, Uint8ArrayWriter, Uint8ArrayReader } = await import(pathToFileURL(require.resolve('@zip.js/zip.js')).href); const { aotFixtureImage, buildStrippedElf64Image } = await source('src/flutter/FlutterBuild.fixture.ts'); const { FlutterBuildProvider } = await source('src/flutter/FlutterBuildProvider.ts'); const { FlutterBuildAnalysisService } = await source('src/application/flutter/FlutterBuildAnalysisService.ts'); const declarations = [ { name: '', value: 0, size: 0 }, { 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 results = []; for (const [label, ignored] of [['control', 0n], ['ignored-high-word', 0xffffffffffffffffn]] as const) { const image = aotFixtureImage(); image.fill(0, 0, 0x1000); buildStrippedElf64Image(declarations).copy(image); const dynamic = Number(image.readBigUInt64LE(64 + 56 + 8)); const symbols = Number(image.readBigUInt64LE(dynamic + 8)); const hash = Number(image.readBigUInt64LE(dynamic + 4 * 16 + 8)); image.writeUInt32LE(1, hash + 8); // one bucket owns all four exports for (let i = 1; i < declarations.length - 1; i++) image.writeUInt32LE(i + 1, hash + 12 + i * 4); image.fill(0, symbols, symbols + 24); // mandatory STN_UNDEF entry for (let i = 1; i < declarations.length; i++) image[symbols + i * 24 + 4] = 0x11; // STB_GLOBAL/STT_OBJECT image.writeUInt32LE(5, 64 + 4); // readable executable load image.writeBigUInt64LE(BigInt(image.length), 64 + 40); // p_memsz image.writeBigUInt64LE(0x1000n, 64 + 48); // p_align image.writeUInt32LE(4, 64 + 56 + 4); // readable PT_DYNAMIC image.writeBigUInt64LE(96n, 64 + 56 + 40); image.writeBigUInt64LE(8n, 64 + 56 + 48); image.writeBigUInt64LE(ignored, dynamic + 5 * 16 + 8); await writeFile(join(root, label + '.so'), image); const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false }); await writer.add('lib/x86_64/libapp.so', new Uint8ArrayReader(image)); const apk = join(root, label + '.apk'); await writeFile(apk, await writer.close()); const provider = new FlutterBuildProvider(); try { const result = await new FlutterBuildAnalysisService(provider).execute('inspect_dart_aot', { path: apk, abi: 'x86_64' }); const facts = result.ok ? { label, ok: true, coverage: result.value.normalized_result.coverage, hash: result.value.normalized_result.libapp.snapshot_hash, symbols: result.value.normalized_result.libapp.sections.length, package_uris: result.value.normalized_result.string_pool.package_uris, } : { label, ok: false, error_tag: result.error._tag, message: result.error.message }; results.push(facts); console.log(JSON.stringify(facts)); } finally { await provider.close(); } } ``` The final fixture pair was executed once. An earlier exploratory pair also showed the same failure before the SysV hash chains were completed; the report relies on the completed pair above. GNU readelf independently inspected the failing ELF with `readelf -W -d -sD /tmp/rea-flutter-repro/ignored-high-word.so` and listed the terminating NULL entry and all four exports without an error. ## Expected behavior Ignore `DT_NULL.d_un`, terminate the dynamic-array walk, and return the same snapshot symbols, hash, package URI observations, and complete structural coverage as the zero-valued control, while retaining each fixture's own file identity. [The documented Flutter contract](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/docs/flutter-analysis.md#L52-L70) supports little-endian ELF64 dynamic-symbol inspection, including [section-stripped images with a bounded DT_HASH or DT_GNU_HASH count](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/docs/flutter-analysis.md#L152-L157). [The ELF dynamic-section specification](https://gabi.xinuos.com/elf/08-dynamic.html#dynamic-section) marks `DT_NULL`'s `d_un` as ignored. Its contents therefore do not make this dynamic-table terminator invalid or require numeric interpretation. ## Impact and scope A valid unused metadata word prevents the entire AOT inspection, losing otherwise available headers and string-pool observations. Observed scope is one synthetic little-endian ELF64/x86-64 fixture and its zero-valued control. Real APK prevalence and other platforms were not measured. A manual `readelf` inspection recovered all four symbol records for the failing fixture, but recovery of the full REA observation without changing input identity was not evaluated. ## REA version Source revision `5f702dd6ccd9e8e505e224854b91678439c3b41f`. The relevant `src/flutter/DartElf.ts` is byte-identical to the fetched `rea-agents-6.7.0` release tag; this establishes source applicability only. The published package runtime was not executed. ## Runtime and platform Node.js v24.22.1; Linux x86_64; GNU readelf (GNU Binutils) 2.47. Source execution through the checkout's locked `tsx` dependency. No external analysis engine was started. ## Target and artifact context Two synthetic APKs, each containing `lib/x86_64/libapp.so`. Both have section-stripped ELF64 dynamic symbol/hash/string tables, four legacy Dart snapshot exports, and the existing complete Dart 2.14-layout snapshot payload. The only difference is the terminating `DT_NULL` entry's ignored eight-byte word. These are parser fixtures, not runnable Flutter applications. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: the documented Flutter contract and ELF dynamic-section rule linked above. - Failure source: the executed fixture comparison below and the dynamic-entry loop linked under Analysis. - Evidence provenance: observed; source reasoning and the external format rule are separately attributed. - Local verification: reproduced. - Reproduction completeness: complete. Observed compact output, with the fixture root generalized: ```json {"label":"control","ok":true,"coverage":"complete","hash":"65817c30a78bb44c3dc3771876b6010a","symbols":4,"package_uris":["package:adaptive_breakpoints/src/adaptive_breakpoints.dart","package:animations/src/fade_scale_transition.dart"]} {"label":"ignored-high-word","ok":false,"error_tag":"AnalysisUnsupportedTargetError","message":"Unsupported target for inspect_dart_aot at /tmp/rea-flutter-repro/ignored-high-word.apk: No recognizable Dart snapshot symbols were found: Dynamic entry is not representable"} ``` GNU readelf reported the dynamic terminator as `(NULL) 0xffffffffffffffff` and the four global object exports at offsets/addresses `0x1000`, `0x1800`, `0x2000`, and `0x2600`, with sizes `52`, `16`, `1536`, and `16`. It exited successfully. ## Typed outcome or recovery expected Return normal successful AOT Evidence for both cases, preserving four snapshot sections and the hash/string observations. As a restoration check, compare the two logical results while allowing their target/library digests and paths to differ. Bounds and representability checks for fields that actually carry table addresses or sizes must remain effective. ## Analysis or suspected cause Source-derived: [the dynamic-entry loop](https://github.com/morluto/rea/blob/5f702dd6ccd9e8e505e224854b91678439c3b41f/src/flutter/DartElf.ts#L325-L338) decodes both the tag and `d_un` with the safe-integer reader, then rejects an unrepresentable value before recognizing `DT_NULL`. Thus the ignored terminator payload is treated as a required numeric fact. No implementation was changed or tested. ## Additional context Open issue [#3629](https://github.com/morluto/rea/issues/3629) concerns renamed snapshot exports. This fixture deliberately uses all four supported legacy exports, independently listed by readelf, so that is a different failure. Open issues [#6697](https://github.com/morluto/rea/issues/6697) and [#6563](https://github.com/morluto/rea/issues/6563) concern GNU build-ID notes during build identification; no engine library or build-ID parser participates here. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A bounded valid-metadata compatibility failure blocks AOT inspection for this ELF64 input; real-world frequency and broader applicability are unknown. - Workaround status: unverified - Workaround basis: Readelf recovered the symbol records, but a practical workaround preserving the exact input and the complete REA observation was not established.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4125
No description provided.