[GH-ISSUE #6697] [Bug] Flutter build identification drops valid GNU build IDs shorter than four bytes #4072

Closed
opened 2026-10-31 00:46:35 +00:00 by bsakaguchi · 1 comment
Owner

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

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

identify_flutter_build returns libflutter.build_id: null, no candidates, and partial coverage when a declared GNU build-ID note contains the valid three-byte ID 112233. The four-byte control 11223344 returns its ID and complete coverage. The failure says GNU build-id descriptor exceeds the 4–64 byte projection even though this descriptor is smaller, not larger.

Executed through FlutterBuildAnalysisService.execute, the shared application workflow used by the CLI/MCP adapters, against inert synthetic APKs. The public CLI/MCP transports and a real Flutter engine were not run. No target code was executed.

Reproduction

From the source revision below, with its locked dependencies already installed, save this script as /tmp/rea-repro/repro.mjs, then run node --import tsx /tmp/rea-repro/repro.mjs. It creates two tiny APKs using the repository's existing fixture builders and runs each case once.

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-repro';
await mkdir(root, { recursive: true });
process.env.TMPDIR = root;
const source = (path) => import(pathToFileURL(join(process.cwd(), path)).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, engineFixtureImage } = await source('src/flutter/FlutterBuild.fixture.ts');
const { elfNote } = await source('src/native/elf/ElfNotes.fixture.ts');
const { FlutterBuildProvider } = await source('src/flutter/FlutterBuildProvider.ts');
const { FlutterBuildAnalysisService } = await source('src/application/flutter/FlutterBuildAnalysisService.ts');
for (const id of ['11223344', '112233']) {
  const engine = engineFixtureImage({ notes: elfNote('GNU', 3, Buffer.from(id, 'hex')) });
  engine.writeUInt16LE(62, 0x12);
  engine.writeUInt32LE(4, 68);
  engine.writeBigUInt64LE(BigInt(engine.length), 104);
  await writeFile(join(root, `engine-${id}.so`), engine);
  const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false });
  await writer.add('lib/x86_64/libapp.so', new Uint8ArrayReader(aotFixtureImage()));
  await writer.add('lib/x86_64/libflutter.so', new Uint8ArrayReader(engine));
  const apk = join(root, `build-${id}.apk`);
  await writeFile(apk, await writer.close());
  const provider = new FlutterBuildProvider();
  try {
    const result = await new FlutterBuildAnalysisService(provider).execute('identify_flutter_build', { path: apk });
    if (!result.ok) throw result.error;
    const value = result.value.normalized_result;
    console.log(JSON.stringify({ id, build_id: value.abis[0].libflutter.build_id, candidates: value.abis[0].libflutter.build_id_candidates, coverage: value.coverage, failures: result.value.limitations.filter((text) => text.includes('descriptor exceeds')) }));
  } finally { await provider.close(); }
}

The executed run used a disposable scratch root selected with REPRO_ROOT; the commands above use a generic root. Independently inspect the short-ID engine with:

readelf -n /tmp/rea-repro/engine-112233.so

Expected behavior

Preserve 112233 as the single observed build ID when its declared note is fully examined and unambiguous. The documented Flutter contract says the candidate list preserves distinct GNU build IDs from declared ELF note ranges; it does not impose a four-byte minimum.

GNU ld's documented --build-id=0xhexstring option accepts a chosen bit string expressed as an even number of hex digits. A three-byte descriptor is valid metadata. GNU readelf decoded the expected short ID without an error in this reproduction.

Impact and scope

A bounded engine-provenance failure discards an existing ID and makes otherwise complete identification partial. Both synthetic cases ran once. Observed scope: little-endian ELF64/x86-64 inside APKs. Real-world prevalence, other architectures, and other hosts were not measured. A practical manual workaround is to inspect the extracted engine with readelf -n, which recovered this ID locally.

REA version

Source revision 8c4f96ef4759fd78da66c4b9726825ca71d7ee0c (package.json version 6.7.0). Source execution; the published npm runtime was not executed.

Runtime and platform

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

Target and artifact context

Two synthetic APKs containing lib/x86_64/libapp.so and lib/x86_64/libflutter.so. The AOT fixture is identical; the engine's declared PT_NOTE contains one GNU type-3 note with either a four-byte control or a three-byte descriptor. These fixtures establish metadata parsing behavior, not executable Flutter engine behavior.

Interface

Other — shared application service used by CLI/MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Actual compact output:

{"id":"11223344","build_id":"11223344","candidates":["11223344"],"coverage":"complete","failures":[]}
{"id":"112233","build_id":null,"candidates":[],"coverage":"partial","failures":["lib/x86_64/libflutter.so: GNU build-id descriptor exceeds the 4–64 byte projection"]}

The independent format observation was:

GNU  0x00000003  NT_GNU_BUILD_ID (unique build ID bitstring)
  Build ID: 112233

Typed outcome or recovery expected

The normal successful identification result should retain build_id: "112233" and build_id_candidates: ["112233"], with complete coverage for this fixture.

Analysis or suspected cause

Source-derived cause: the parser rejects any descriptor shorter than four bytes before retaining the candidate. The result schema also requires at least eight hex characters, so a parser-only correction would still leave the output boundary unable to represent valid short IDs. The existing upper size/read budgets can remain independent of a minimum descriptor length.

Additional context

Related but distinct: #6563 concerns eight-byte ELF note alignment; this fixture uses ordinary four-byte note padding and reaches the explicit descriptor-length rejection. Open and closed searches for Flutter, build-ID, and descriptor reports did not identify a report of this minimum-length failure. No source fixes, commits, submissions, target execution, or full test suite were performed. No bug reproduction was rerun after this draft was written.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed shared workflow drops a valid present build ID and reports partial coverage for the three-byte case; other identification facts survive and the target is unchanged.
  • Workaround status: available
  • Workaround basis: GNU readelf recovered 112233 from the same engine artifact with readelf -n; users can inspect an extracted engine's note manually.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6697 ### 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 `identify_flutter_build` returns `libflutter.build_id: null`, no candidates, and partial coverage when a declared GNU build-ID note contains the valid three-byte ID `112233`. The four-byte control `11223344` returns its ID and complete coverage. The failure says `GNU build-id descriptor exceeds the 4–64 byte projection` even though this descriptor is smaller, not larger. Executed through `FlutterBuildAnalysisService.execute`, the shared application workflow used by the CLI/MCP adapters, against inert synthetic APKs. The public CLI/MCP transports and a real Flutter engine were not run. No target code was executed. ### Reproduction From the source revision below, with its locked dependencies already installed, save this script as `/tmp/rea-repro/repro.mjs`, then run `node --import tsx /tmp/rea-repro/repro.mjs`. It creates two tiny APKs using the repository's existing fixture builders and runs each case once. ```js 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-repro'; await mkdir(root, { recursive: true }); process.env.TMPDIR = root; const source = (path) => import(pathToFileURL(join(process.cwd(), path)).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, engineFixtureImage } = await source('src/flutter/FlutterBuild.fixture.ts'); const { elfNote } = await source('src/native/elf/ElfNotes.fixture.ts'); const { FlutterBuildProvider } = await source('src/flutter/FlutterBuildProvider.ts'); const { FlutterBuildAnalysisService } = await source('src/application/flutter/FlutterBuildAnalysisService.ts'); for (const id of ['11223344', '112233']) { const engine = engineFixtureImage({ notes: elfNote('GNU', 3, Buffer.from(id, 'hex')) }); engine.writeUInt16LE(62, 0x12); engine.writeUInt32LE(4, 68); engine.writeBigUInt64LE(BigInt(engine.length), 104); await writeFile(join(root, `engine-${id}.so`), engine); const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false }); await writer.add('lib/x86_64/libapp.so', new Uint8ArrayReader(aotFixtureImage())); await writer.add('lib/x86_64/libflutter.so', new Uint8ArrayReader(engine)); const apk = join(root, `build-${id}.apk`); await writeFile(apk, await writer.close()); const provider = new FlutterBuildProvider(); try { const result = await new FlutterBuildAnalysisService(provider).execute('identify_flutter_build', { path: apk }); if (!result.ok) throw result.error; const value = result.value.normalized_result; console.log(JSON.stringify({ id, build_id: value.abis[0].libflutter.build_id, candidates: value.abis[0].libflutter.build_id_candidates, coverage: value.coverage, failures: result.value.limitations.filter((text) => text.includes('descriptor exceeds')) })); } finally { await provider.close(); } } ``` The executed run used a disposable scratch root selected with `REPRO_ROOT`; the commands above use a generic root. Independently inspect the short-ID engine with: ```sh readelf -n /tmp/rea-repro/engine-112233.so ``` ### Expected behavior Preserve `112233` as the single observed build ID when its declared note is fully examined and unambiguous. [The documented Flutter contract](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/docs/flutter-analysis.md#L102-L111) says the candidate list preserves distinct GNU build IDs from declared ELF note ranges; it does not impose a four-byte minimum. [GNU ld's documented `--build-id=0xhexstring` option](https://sourceware.org/binutils/docs/ld/Options.html#index-_002d_002dbuild_002did) accepts a chosen bit string expressed as an even number of hex digits. A three-byte descriptor is valid metadata. GNU readelf decoded the expected short ID without an error in this reproduction. ### Impact and scope A bounded engine-provenance failure discards an existing ID and makes otherwise complete identification partial. Both synthetic cases ran once. Observed scope: little-endian ELF64/x86-64 inside APKs. Real-world prevalence, other architectures, and other hosts were not measured. A practical manual workaround is to inspect the extracted engine with `readelf -n`, which recovered this ID locally. ### REA version Source revision `8c4f96ef4759fd78da66c4b9726825ca71d7ee0c` (`package.json` version `6.7.0`). Source execution; the published npm runtime was not executed. ### Runtime and platform Node.js v26.12.1; Linux x64; GNU readelf (GNU Binutils) 2.47. No external analysis provider was started. ### Target and artifact context Two synthetic APKs containing `lib/x86_64/libapp.so` and `lib/x86_64/libflutter.so`. The AOT fixture is identical; the engine's declared `PT_NOTE` contains one GNU type-3 note with either a four-byte control or a three-byte descriptor. These fixtures establish metadata parsing behavior, not executable Flutter engine behavior. ### Interface Other — shared application service used by CLI/MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Actual compact output: ```json {"id":"11223344","build_id":"11223344","candidates":["11223344"],"coverage":"complete","failures":[]} {"id":"112233","build_id":null,"candidates":[],"coverage":"partial","failures":["lib/x86_64/libflutter.so: GNU build-id descriptor exceeds the 4–64 byte projection"]} ``` The independent format observation was: ```text GNU 0x00000003 NT_GNU_BUILD_ID (unique build ID bitstring) Build ID: 112233 ``` ### Typed outcome or recovery expected The normal successful identification result should retain `build_id: "112233"` and `build_id_candidates: ["112233"]`, with complete coverage for this fixture. ### Analysis or suspected cause Source-derived cause: [the parser rejects any descriptor shorter than four bytes](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/flutter/ElfBuildId.ts#L165-L174) before retaining the candidate. [The result schema also requires at least eight hex characters](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/domain/flutter/flutterBuildAnalysis.ts#L121-L132), so a parser-only correction would still leave the output boundary unable to represent valid short IDs. The existing upper size/read budgets can remain independent of a minimum descriptor length. ### Additional context Related but distinct: [#6563](https://github.com/morluto/rea/issues/6563) concerns eight-byte ELF note alignment; this fixture uses ordinary four-byte note padding and reaches the explicit descriptor-length rejection. Open and closed searches for Flutter, build-ID, and descriptor reports did not identify a report of this minimum-length failure. No source fixes, commits, submissions, target execution, or full test suite were performed. No bug reproduction was rerun after this draft was written. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed shared workflow drops a valid present build ID and reports partial coverage for the three-byte case; other identification facts survive and the target is unchanged. - Workaround status: available - Workaround basis: GNU readelf recovered `112233` from the same engine artifact with `readelf -n`; users can inspect an extracted engine's note manually.
Author
Owner

@ezrabrilliant commented on GitHub (Oct 23, 2026):

I'd like to take this one. In src/flutter/ElfBuildId.ts the GNU build-id guard is if (descsz < 4 || descsz > 64), so a valid 3-byte descriptor is rejected with the (inaccurate) "exceeds the 4–64 byte projection" message even though it is under, not over, the range. A GNU NT_GNU_BUILD_ID descriptor only needs to be non-empty.

Planned fix: lower the minimum to 1 byte (keep the 64-byte projection cap) and correct the message to describe the range accurately, with regression coverage for a sub-4-byte ID, a normal 20-byte ID, and an empty descriptor.

<!-- gh-comment-id:6364848644 --> @ezrabrilliant commented on GitHub (Oct 23, 2026): I'd like to take this one. In `src/flutter/ElfBuildId.ts` the GNU build-id guard is `if (descsz < 4 || descsz > 64)`, so a valid 3-byte descriptor is rejected with the (inaccurate) "exceeds the 4–64 byte projection" message even though it is under, not over, the range. A GNU `NT_GNU_BUILD_ID` descriptor only needs to be non-empty. Planned fix: lower the minimum to 1 byte (keep the 64-byte projection cap) and correct the message to describe the range accurately, with regression coverage for a sub-4-byte ID, a normal 20-byte ID, and an empty descriptor.
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#4072
No description provided.