[GH-ISSUE #6563] [Bug] Flutter build identification loses GNU build IDs in 8-byte-aligned ELF notes #3954

Closed
opened 2026-10-31 00:45:33 +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/6563

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

The public identify-flutter-build CLI returns libflutter.build_id: null, an empty build_id_candidates list, and partial coverage for a synthetic APK whose engine ELF contains a valid GNU build-ID note in an eight-byte-aligned note table. Its limitation says ELF note descriptor exceeds its declared note range. GNU readelf -n decodes the expected ID from that engine without an error. The four-byte-aligned control returns that ID and complete coverage.

This is an executed release reproduction plus source-derived applicability to current main. The current-main CLI was not built or run. No Flutter engine or target code was executed.

Reproduction

The executed case used the repository's engineFixtureImage and aotFixtureImage builders, wrote disposable files, packaged them with Python's zipfile, and invoked the installed package's scripts/rea.mjs. The fixture construction below uses generic paths; run it from a source checkout with a supported Node runtime.

mkdir -p /tmp/rea-example
node --input-type=module <<'JS'
import { writeFile } from 'node:fs/promises';
import { aotFixtureImage, engineFixtureImage } from './src/flutter/FlutterBuild.fixture.ts';
const id = '0123456789abcdef0123456789abcdef01234567';
function note(owner, type, descriptor, alignment) {
  const name = Buffer.from(owner + '\0');
  const align = n => Math.ceil(n / alignment) * alignment;
  const descriptorAt = align(12 + name.length);
  const b = Buffer.alloc(align(descriptorAt + descriptor.length));
  b.writeUInt32LE(name.length); b.writeUInt32LE(descriptor.length, 4); b.writeUInt32LE(type, 8);
  name.copy(b, 12); descriptor.copy(b, descriptorAt);
  return b;
}
await writeFile('/tmp/rea-example/app.so', aotFixtureImage());
for (const alignment of [4, 8]) {
  const notes = Buffer.concat([
    note('Example', 1, Buffer.from([1,2,3,4]), alignment),
    note('GNU', 3, Buffer.from(id, 'hex'), alignment),
  ]);
  const image = engineFixtureImage({ notes, ownership: 'both' });
  image.writeUInt16LE(62, 18);
  image.writeBigUInt64LE(BigInt(image.length), 64 + 40);
  image.writeBigUInt64LE(BigInt(notes.length), 64 + 56 + 40);
  image.writeUInt32LE(4, 64 + 4);
  image.writeUInt32LE(4, 64 + 56 + 4);
  image.writeBigUInt64LE(BigInt(alignment), 64 + 56 + 48);
  image.writeBigUInt64LE(BigInt(alignment), 0x100 + 64 + 48);
  await writeFile(`/tmp/rea-example/engine-${alignment}.so`, image);
}
JS
python3 - <<'PYTHON'
from pathlib import Path
from zipfile import ZipFile
r = Path('/tmp/rea-example')
for alignment in (4, 8):
    with ZipFile(r / f'notes-{alignment}.apk', 'w') as z:
        z.write(r / 'app.so', 'lib/x86_64/libapp.so')
        z.write(r / f'engine-{alignment}.so', 'lib/x86_64/libflutter.so')
PYTHON

Invoke the installed package launcher with identify-flutter-build /tmp/rea-example/notes-8.apk --format json, then the four-byte control with identify-flutter-build /tmp/rea-example/notes-4.apk --format json. Both executed commands exited zero. readelf -n /tmp/rea-example/engine-8.so provides the independent format control. Each CLI variant was run once; no natural occurrence rate was measured.

Expected behavior

The Flutter analysis contract preserves GNU build IDs from declared PT_NOTE and SHT_NOTE ranges and populates the single-ID field after complete examination. Valid eight-byte note padding should preserve the ID just as the four-byte control does. REA's general ELF note parser already recognizes this alignment, and its record framing aligns the complete header/name and descriptor extents.

Impact and scope

P2: a bounded engine-identification defect loses a present build ID and incorrectly labels valid metadata as malformed. The target remains unchanged and other Flutter facts survive. The observed scope is synthetic little-endian ELF64/x86-64 with both note declarations; actual Flutter engine prevalence, other architectures, and other operating systems were not tested. A manual workaround is to inspect the extracted engine with readelf -n, which recovered the ID in the executed case.

REA version

Executed npm package rea-agents@6.7.0; release tag rea-agents-6.7.0 at 27dc42d10249f0410904a75c0997842eb03b931c. Source inspected at main 6bb4f4bf7d64e5d2f47479d46225a5383b57cdad. src/flutter/ElfBuildId.ts and src/flutter/DartElf.ts are unchanged between that tag and main.

Runtime and platform

Node.js v26.12.1; Linux x64; GNU readelf (GNU Binutils) 2.47. No external analysis provider was installed or started.

Target and artifact context

Two synthetic APKs with the same lib/x86_64/libapp.so and engine note contents; note framing and declared alignment differ. The engine has an Example owner note with a four-byte descriptor followed by one GNU build-ID note with a 20-byte descriptor. Both p_align and sh_addralign match the selected alignment. The synthetic libraries establish metadata parsing behavior, not loadable Flutter engine behavior.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed normalized CLI fields:

alignment 4: coverage = complete
alignment 4: libflutter.build_id = 0123456789abcdef0123456789abcdef01234567
alignment 8: coverage = partial
alignment 8: libflutter.build_id = null
alignment 8: libflutter.build_id_candidates = []
alignment 8 limitation: lib/x86_64/libflutter.so: ELF note descriptor exceeds its declared note range
readelf -n, alignment 8: Build ID: 0123456789abcdef0123456789abcdef01234567

The original direct readElfBuildIds probe returned the same distinction: one candidate and no failures for the control, versus no candidates and the descriptor-range failure for eight-byte framing. Source comparison found identical reader bytes between the release tag and main; the reader's SHA-256 was 105a32aa2280c66258184c96724ee05dc7300ba3e0342fa2b58f6b09a9f70ad4.

  • Failure provenance: executed released CLI and production parser on disposable synthetic fixtures; source inspection establishes unchanged reader logic on main.
  • Local verification: reproduced.
  • Reproduction completeness: complete for the release fixture; current-main runtime execution and natural engine prevalence remain untested.

Typed outcome or recovery expected

Return the present single build ID without the spurious note-range diagnostic. Preserve complete coverage for the four-byte control and this otherwise fully examined eight-byte fixture; continue reporting genuine malformed or bounded note data as partial.

Analysis or suspected cause

Source-derived: the range inventory retains offsets and sizes without note alignment. The parser always uses align4, so it reads descriptor padding as the next header in the eight-byte case. The Flutter projection then suppresses the ID when that reader reports a failure.

Additional context

Open and closed issue searches covered readElfBuildIds, ElfBuildId, Flutter alignment, and build-id padding; no matching report was found. NOT_ENOUGH_INFO: introducing revision, last known good release, or frequency in real Flutter engine distributions.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The released public CLI loses an existing engine build ID and reports valid note metadata as malformed for the executed eight-byte fixture, while the four-byte control succeeds; the shared reader is unchanged on main.
  • Workaround status: available
  • Workaround basis: GNU readelf decoded the expected build ID from the same eight-byte engine fixture without modifying it; other Flutter metadata remains available.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6563 ### 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 The public `identify-flutter-build` CLI returns `libflutter.build_id: null`, an empty `build_id_candidates` list, and partial coverage for a synthetic APK whose engine ELF contains a valid GNU build-ID note in an eight-byte-aligned note table. Its limitation says `ELF note descriptor exceeds its declared note range`. GNU `readelf -n` decodes the expected ID from that engine without an error. The four-byte-aligned control returns that ID and complete coverage. This is an executed release reproduction plus source-derived applicability to current main. The current-main CLI was not built or run. No Flutter engine or target code was executed. ### Reproduction The executed case used the repository's `engineFixtureImage` and `aotFixtureImage` builders, wrote disposable files, packaged them with Python's `zipfile`, and invoked the installed package's `scripts/rea.mjs`. The fixture construction below uses generic paths; run it from a source checkout with a supported Node runtime. ```sh mkdir -p /tmp/rea-example node --input-type=module <<'JS' import { writeFile } from 'node:fs/promises'; import { aotFixtureImage, engineFixtureImage } from './src/flutter/FlutterBuild.fixture.ts'; const id = '0123456789abcdef0123456789abcdef01234567'; function note(owner, type, descriptor, alignment) { const name = Buffer.from(owner + '\0'); const align = n => Math.ceil(n / alignment) * alignment; const descriptorAt = align(12 + name.length); const b = Buffer.alloc(align(descriptorAt + descriptor.length)); b.writeUInt32LE(name.length); b.writeUInt32LE(descriptor.length, 4); b.writeUInt32LE(type, 8); name.copy(b, 12); descriptor.copy(b, descriptorAt); return b; } await writeFile('/tmp/rea-example/app.so', aotFixtureImage()); for (const alignment of [4, 8]) { const notes = Buffer.concat([ note('Example', 1, Buffer.from([1,2,3,4]), alignment), note('GNU', 3, Buffer.from(id, 'hex'), alignment), ]); const image = engineFixtureImage({ notes, ownership: 'both' }); image.writeUInt16LE(62, 18); image.writeBigUInt64LE(BigInt(image.length), 64 + 40); image.writeBigUInt64LE(BigInt(notes.length), 64 + 56 + 40); image.writeUInt32LE(4, 64 + 4); image.writeUInt32LE(4, 64 + 56 + 4); image.writeBigUInt64LE(BigInt(alignment), 64 + 56 + 48); image.writeBigUInt64LE(BigInt(alignment), 0x100 + 64 + 48); await writeFile(`/tmp/rea-example/engine-${alignment}.so`, image); } JS python3 - <<'PYTHON' from pathlib import Path from zipfile import ZipFile r = Path('/tmp/rea-example') for alignment in (4, 8): with ZipFile(r / f'notes-{alignment}.apk', 'w') as z: z.write(r / 'app.so', 'lib/x86_64/libapp.so') z.write(r / f'engine-{alignment}.so', 'lib/x86_64/libflutter.so') PYTHON ``` Invoke the installed package launcher with `identify-flutter-build /tmp/rea-example/notes-8.apk --format json`, then the four-byte control with `identify-flutter-build /tmp/rea-example/notes-4.apk --format json`. Both executed commands exited zero. `readelf -n /tmp/rea-example/engine-8.so` provides the independent format control. Each CLI variant was run once; no natural occurrence rate was measured. ### Expected behavior [The Flutter analysis contract](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/docs/flutter-analysis.md#L102-L111) preserves GNU build IDs from declared `PT_NOTE` and `SHT_NOTE` ranges and populates the single-ID field after complete examination. Valid eight-byte note padding should preserve the ID just as the four-byte control does. [REA's general ELF note parser](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/src/native/elf/ElfNoteParser.ts#L143-L146) already recognizes this alignment, and its [record framing](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/src/native/elf/ElfNoteParser.ts#L182-L196) aligns the complete header/name and descriptor extents. ### Impact and scope P2: a bounded engine-identification defect loses a present build ID and incorrectly labels valid metadata as malformed. The target remains unchanged and other Flutter facts survive. The observed scope is synthetic little-endian ELF64/x86-64 with both note declarations; actual Flutter engine prevalence, other architectures, and other operating systems were not tested. A manual workaround is to inspect the extracted engine with `readelf -n`, which recovered the ID in the executed case. ### REA version Executed npm package `rea-agents@6.7.0`; release tag `rea-agents-6.7.0` at `27dc42d10249f0410904a75c0997842eb03b931c`. Source inspected at main `6bb4f4bf7d64e5d2f47479d46225a5383b57cdad`. `src/flutter/ElfBuildId.ts` and `src/flutter/DartElf.ts` are unchanged between that tag and main. ### Runtime and platform Node.js v26.12.1; Linux x64; GNU readelf (GNU Binutils) 2.47. No external analysis provider was installed or started. ### Target and artifact context Two synthetic APKs with the same `lib/x86_64/libapp.so` and engine note contents; note framing and declared alignment differ. The engine has an `Example` owner note with a four-byte descriptor followed by one GNU build-ID note with a 20-byte descriptor. Both `p_align` and `sh_addralign` match the selected alignment. The synthetic libraries establish metadata parsing behavior, not loadable Flutter engine behavior. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed normalized CLI fields: ```text alignment 4: coverage = complete alignment 4: libflutter.build_id = 0123456789abcdef0123456789abcdef01234567 alignment 8: coverage = partial alignment 8: libflutter.build_id = null alignment 8: libflutter.build_id_candidates = [] alignment 8 limitation: lib/x86_64/libflutter.so: ELF note descriptor exceeds its declared note range readelf -n, alignment 8: Build ID: 0123456789abcdef0123456789abcdef01234567 ``` The original direct `readElfBuildIds` probe returned the same distinction: one candidate and no failures for the control, versus no candidates and the descriptor-range failure for eight-byte framing. Source comparison found identical reader bytes between the release tag and main; the reader's SHA-256 was `105a32aa2280c66258184c96724ee05dc7300ba3e0342fa2b58f6b09a9f70ad4`. - Failure provenance: executed released CLI and production parser on disposable synthetic fixtures; source inspection establishes unchanged reader logic on main. - Local verification: reproduced. - Reproduction completeness: complete for the release fixture; current-main runtime execution and natural engine prevalence remain untested. ### Typed outcome or recovery expected Return the present single build ID without the spurious note-range diagnostic. Preserve complete coverage for the four-byte control and this otherwise fully examined eight-byte fixture; continue reporting genuine malformed or bounded note data as partial. ### Analysis or suspected cause Source-derived: [the range inventory](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/src/flutter/ElfBuildId.ts#L90-L125) retains offsets and sizes without note alignment. [The parser](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/src/flutter/ElfBuildId.ts#L125-L168) always uses `align4`, so it reads descriptor padding as the next header in the eight-byte case. [The Flutter projection](https://github.com/morluto/rea/blob/6bb4f4bf7d64e5d2f47479d46225a5383b57cdad/src/flutter/FlutterBuildProvider.ts#L820-L837) then suppresses the ID when that reader reports a failure. ### Additional context Open and closed issue searches covered `readElfBuildIds`, `ElfBuildId`, `Flutter alignment`, and `build-id padding`; no matching report was found. NOT_ENOUGH_INFO: introducing revision, last known good release, or frequency in real Flutter engine distributions. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The released public CLI loses an existing engine build ID and reports valid note metadata as malformed for the executed eight-byte fixture, while the four-byte control succeeds; the shared reader is unchanged on main. - Workaround status: available - Workaround basis: GNU readelf decoded the expected build ID from the same eight-byte engine fixture without modifying it; other Flutter metadata remains available.
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#3954
No description provided.