[GH-ISSUE #6723] [Bug] ARM FAT slice order changes the resolved dylib dependency graph #4091

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

The production filesystem reader for trace_dylib_resolution follows a different child dependency when the same ARMv7 and ARMv7s library slices are reordered in a FAT container. The process-root header declares CPU type 12, subtype 11 (ARMv7s). With the ARMv7 slice first, the trace follows generic-child.dylib; with ARMv7s first, it follows specialized-child.dylib. Both results report resolved edges and complete coverage. The process-root bytes and each subtype's thin library bytes are identical between cases.

Reproduction

Save this script as $WORK/arm-fat-order.mts. From a source checkout prepared with its locked dependencies, run node node_modules/tsx/dist/cli.mjs "$WORK/arm-fat-order.mts" "$PWD" "$WORK/fixtures". This exact script was run against both baselines below, once per FAT order on each. It writes disposable inert files and calls the same filesystem reader used by the artifact provider.

import { mkdir, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = resolve(process.argv[2]!);
const work = resolve(process.argv[3]!);
const load = (p: string) => import(pathToFileURL(join(repo, p)).href);
const { machoImage, fatImage, dylibCommand, versionMinCommand, LC, FILE_TYPE } = await load('src/artifacts/apple/MachoImage.fixture.ts');
const { traceDylibResolution } = await load('src/artifacts/apple/DylibResolutionReader.ts');
const image = (subtype: number, fileType: number, name: string) => machoImage({wide: false, cpu: {type: 12, subtype}, fileType, commands: [versionMinCommand(LC.VERSION_MIN_IPHONEOS), ...(name === '' ? [] : [dylibCommand(LC.LOAD_DYLIB, '@loader_path/'+name)])]});
const app = image(11, FILE_TYPE.execute, 'lib.dylib');
const generic = {cpu: {type: 12, subtype: 9}, bytes: image(9, FILE_TYPE.dylib, 'generic-child.dylib')};
const specialized = {cpu: {type: 12, subtype: 11}, bytes: image(11, FILE_TYPE.dylib, 'specialized-child.dylib')};
for (const [name, slices] of [['generic-first', [generic,specialized]], ['specialized-first', [specialized,generic]]] as const) {
  const root = join(work, name); await mkdir(root, {recursive:true});
  await writeFile(join(root, 'App'), app);
  await writeFile(join(root, 'lib.dylib'), fatImage(slices));
  await writeFile(join(root, 'generic-child.dylib'), image(9, FILE_TYPE.dylib, ''));
  await writeFile(join(root, 'specialized-child.dylib'), image(11, FILE_TYPE.dylib, ''));
  const result = await traceDylibResolution({rootPath:root,targetPath:join(root,'App'),targetSha256:createHash('sha256').update(app).digest('hex'),enumerateRoots:false,parameters:{}});
  console.log(JSON.stringify({name,root:result.images.find((i:any)=>i.path==='App')?.slices.map((s:any)=>({arch:s.architecture,subtype:s.cpu_subtype})),lib:result.images.find((i:any)=>i.path==='lib.dylib')?.slices.map((s:any)=>({arch:s.architecture,subtype:s.cpu_subtype})),edges:result.edges.map((e:any)=>({loader:e.loader,name:e.install_name,resolution:e.resolution})),coverage:result.coverage}));
}

Expected behavior

Resolve according to CPU subtype grading, or preserve uncertainty when the applicable target-loader policy is unknown. Under an ARMv7s target policy, the preferred ARMv7s slice must win regardless of FAT order; ARMv7 remains a fallback when the preferred slice is absent. docs/apple-application-analysis.md:223-229 describes graded slice selection and resolved image loading. Apple's legacy loader preference and selection code orders ARMv7s before ARMv7 and searches preference order before FAT order (kARM, fatFindBestFromOrderedList, fatFindBest). That loader uses host subtype to select its policy: the fixture's root subtype alone does not establish an actual running host or loader.

Impact and scope

Changing only FAT slice order changes which library slice's child dependencies enter a supposedly resolved graph. A consumer can receive the fallback slice's dependencies and omit those of the preferred slice. This is a bounded static metadata failure on the two synthetic FAT layouts, observed on both source baselines. Real-world frequency, actual Apple loader execution, and downstream analyst consequences were not measured. Repacking a library changes the analyzed artifact's identity; no practical workaround was verified.

REA version

Current source: 5d6b8a0ab693c03ab85cf0807d40bedf021e76d8. Latest published release tag: rea-agents-6.7.0, commit 27dc42d10249f0410904a75c0997842eb03b931c. Both package manifests report 6.7.0. The release observation used its checked-out source reader and locked dependencies, not an installed npm CLI.

Runtime and platform

Linux x86_64. Current source used Node.js v24.22.1; release source used its pinned Node.js v24.18.0. Both used npm 11.16.0 and their respective locked dependencies. No Apple runtime or native analysis provider was used.

Target and artifact context

Synthetic 32-bit little-endian Mach-O header/load-command fixtures, a FAT dylib with matching subtype declarations and thin headers, iOS version-minimum commands, and two existing child dylibs. These fixtures exercise static parsing and tracing; they are not established runnable or fully dyld-valid images. No target code was executed.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Projected observations from both runs:

baseline        FAT subtype order   child traversed              edge status   coverage
current source  9, 11               generic-child.dylib          resolved      complete
current source  11, 9               specialized-child.dylib      resolved      complete
release source  9, 11               generic-child.dylib          resolved      complete
release source  11, 9               specialized-child.dylib      resolved      complete

Both orders retain the same root subtype, and both library subtype records survive in images[].slices. An independently inspected byte comparison confirmed identical root bytes and identical thin bytes per subtype across orders. A separate current-source ARMv7s-root / ARMv7-only library control resolved successfully.

  • Expected source: the documented graded-resolution contract and Apple's ordered subtype policy described above.
  • Failure source: executed traceDylibResolution filesystem-reader results for the inline fixture generator; the implementation selects the first same-named arm slice.
  • Evidence provenance: observed reader results, with source-derived grading and public-path reachability.
  • Local verification: reproduced at the production filesystem-reader boundary.
  • Reproduction completeness: complete for the stated static reader/order-dependence case; no CLI/MCP, compiler-produced artifact, Apple dyld, or target-runtime reproduction was run.

Typed outcome or recovery expected

Under an established ARMv7s target policy, both FAT orders should traverse specialized-child.dylib, and an ARMv7-only library should remain an accepted fallback. If the policy cannot be established, expose that uncertainty instead of reporting an order-selected dependency graph as resolved. These are proposed restoration checks; no fix was made.

Analysis or suspected cause

Source-derived: src/artifacts/apple/MachoLoadCommandReader.ts:1006-1018 maps all CPU type 12 subtypes to the display architecture arm. src/domain/apple/dyldPaths.ts:120-138 matches that display name and checks raw subtype only for arm64e; find therefore takes the first ARM32 slice. Its explicit grading table handles x86_64h but no ARM32 subtype preferences. The CLI's omitted architecture filter reaches this reader through ArtifactProvider; no new public architecture selector is required to demonstrate the internal selection failure.

Additional context

Related #5191 concerns format/platform presentation and ARM64 slice names. This report concerns choosing another ARM32 slice's dependency graph; changing only the displayed names is not the requested outcome. Independent source/evidence review completed before this draft was written. No runtime re-verification was performed after drafting.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded ARM32 FAT input changes resolved child-dependency evidence solely with slice order; both complete observations retain enough raw subtype information to show the mismatch. No severe downstream consequence was observed.
  • Workaround status: unverified
  • Workaround basis: Reordering or thinning the library changes the analyzed artifact. No supported option or end-to-end consumer workflow was verified to preserve its identity while correcting the selection.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6723 ## 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 Evidence, artifacts, or reconstruction ## Observed behavior The production filesystem reader for `trace_dylib_resolution` follows a different child dependency when the same ARMv7 and ARMv7s library slices are reordered in a FAT container. The process-root header declares CPU type 12, subtype 11 (ARMv7s). With the ARMv7 slice first, the trace follows `generic-child.dylib`; with ARMv7s first, it follows `specialized-child.dylib`. Both results report resolved edges and complete coverage. The process-root bytes and each subtype's thin library bytes are identical between cases. ## Reproduction Save this script as `$WORK/arm-fat-order.mts`. From a source checkout prepared with its locked dependencies, run `node node_modules/tsx/dist/cli.mjs "$WORK/arm-fat-order.mts" "$PWD" "$WORK/fixtures"`. This exact script was run against both baselines below, once per FAT order on each. It writes disposable inert files and calls the same filesystem reader used by the artifact provider. ```ts import { mkdir, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = resolve(process.argv[2]!); const work = resolve(process.argv[3]!); const load = (p: string) => import(pathToFileURL(join(repo, p)).href); const { machoImage, fatImage, dylibCommand, versionMinCommand, LC, FILE_TYPE } = await load('src/artifacts/apple/MachoImage.fixture.ts'); const { traceDylibResolution } = await load('src/artifacts/apple/DylibResolutionReader.ts'); const image = (subtype: number, fileType: number, name: string) => machoImage({wide: false, cpu: {type: 12, subtype}, fileType, commands: [versionMinCommand(LC.VERSION_MIN_IPHONEOS), ...(name === '' ? [] : [dylibCommand(LC.LOAD_DYLIB, '@loader_path/'+name)])]}); const app = image(11, FILE_TYPE.execute, 'lib.dylib'); const generic = {cpu: {type: 12, subtype: 9}, bytes: image(9, FILE_TYPE.dylib, 'generic-child.dylib')}; const specialized = {cpu: {type: 12, subtype: 11}, bytes: image(11, FILE_TYPE.dylib, 'specialized-child.dylib')}; for (const [name, slices] of [['generic-first', [generic,specialized]], ['specialized-first', [specialized,generic]]] as const) { const root = join(work, name); await mkdir(root, {recursive:true}); await writeFile(join(root, 'App'), app); await writeFile(join(root, 'lib.dylib'), fatImage(slices)); await writeFile(join(root, 'generic-child.dylib'), image(9, FILE_TYPE.dylib, '')); await writeFile(join(root, 'specialized-child.dylib'), image(11, FILE_TYPE.dylib, '')); const result = await traceDylibResolution({rootPath:root,targetPath:join(root,'App'),targetSha256:createHash('sha256').update(app).digest('hex'),enumerateRoots:false,parameters:{}}); console.log(JSON.stringify({name,root:result.images.find((i:any)=>i.path==='App')?.slices.map((s:any)=>({arch:s.architecture,subtype:s.cpu_subtype})),lib:result.images.find((i:any)=>i.path==='lib.dylib')?.slices.map((s:any)=>({arch:s.architecture,subtype:s.cpu_subtype})),edges:result.edges.map((e:any)=>({loader:e.loader,name:e.install_name,resolution:e.resolution})),coverage:result.coverage})); } ``` ## Expected behavior Resolve according to CPU subtype grading, or preserve uncertainty when the applicable target-loader policy is unknown. Under an ARMv7s target policy, the preferred ARMv7s slice must win regardless of FAT order; ARMv7 remains a fallback when the preferred slice is absent. `docs/apple-application-analysis.md:223-229` describes graded slice selection and resolved image loading. Apple's [legacy loader preference and selection code](https://github.com/apple-oss-distributions/dyld/blob/dyld-852.2/src/dyld2.cpp#L2824-L2828) orders ARMv7s before ARMv7 and searches preference order before FAT order (`kARM`, `fatFindBestFromOrderedList`, `fatFindBest`). That loader uses host subtype to select its policy: the fixture's root subtype alone does not establish an actual running host or loader. ## Impact and scope Changing only FAT slice order changes which library slice's child dependencies enter a supposedly resolved graph. A consumer can receive the fallback slice's dependencies and omit those of the preferred slice. This is a bounded static metadata failure on the two synthetic FAT layouts, observed on both source baselines. Real-world frequency, actual Apple loader execution, and downstream analyst consequences were not measured. Repacking a library changes the analyzed artifact's identity; no practical workaround was verified. ## REA version Current source: `5d6b8a0ab693c03ab85cf0807d40bedf021e76d8`. Latest published release tag: `rea-agents-6.7.0`, commit `27dc42d10249f0410904a75c0997842eb03b931c`. Both package manifests report 6.7.0. The release observation used its checked-out source reader and locked dependencies, not an installed npm CLI. ## Runtime and platform Linux x86_64. Current source used Node.js v24.22.1; release source used its pinned Node.js v24.18.0. Both used npm 11.16.0 and their respective locked dependencies. No Apple runtime or native analysis provider was used. ## Target and artifact context Synthetic 32-bit little-endian Mach-O header/load-command fixtures, a FAT dylib with matching subtype declarations and thin headers, iOS version-minimum commands, and two existing child dylibs. These fixtures exercise static parsing and tracing; they are not established runnable or fully dyld-valid images. No target code was executed. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Projected observations from both runs: ```text baseline FAT subtype order child traversed edge status coverage current source 9, 11 generic-child.dylib resolved complete current source 11, 9 specialized-child.dylib resolved complete release source 9, 11 generic-child.dylib resolved complete release source 11, 9 specialized-child.dylib resolved complete ``` Both orders retain the same root subtype, and both library subtype records survive in `images[].slices`. An independently inspected byte comparison confirmed identical root bytes and identical thin bytes per subtype across orders. A separate current-source ARMv7s-root / ARMv7-only library control resolved successfully. - Expected source: the documented graded-resolution contract and Apple's ordered subtype policy described above. - Failure source: executed `traceDylibResolution` filesystem-reader results for the inline fixture generator; the implementation selects the first same-named `arm` slice. - Evidence provenance: observed reader results, with source-derived grading and public-path reachability. - Local verification: reproduced at the production filesystem-reader boundary. - Reproduction completeness: complete for the stated static reader/order-dependence case; no CLI/MCP, compiler-produced artifact, Apple dyld, or target-runtime reproduction was run. ## Typed outcome or recovery expected Under an established ARMv7s target policy, both FAT orders should traverse `specialized-child.dylib`, and an ARMv7-only library should remain an accepted fallback. If the policy cannot be established, expose that uncertainty instead of reporting an order-selected dependency graph as resolved. These are proposed restoration checks; no fix was made. ## Analysis or suspected cause Source-derived: `src/artifacts/apple/MachoLoadCommandReader.ts:1006-1018` maps all CPU type 12 subtypes to the display architecture `arm`. `src/domain/apple/dyldPaths.ts:120-138` matches that display name and checks raw subtype only for `arm64e`; `find` therefore takes the first ARM32 slice. Its explicit grading table handles x86_64h but no ARM32 subtype preferences. The CLI's omitted architecture filter reaches this reader through `ArtifactProvider`; no new public architecture selector is required to demonstrate the internal selection failure. ## Additional context Related [#5191](https://github.com/morluto/rea/issues/5191) concerns format/platform presentation and ARM64 slice names. This report concerns choosing another ARM32 slice's dependency graph; changing only the displayed names is not the requested outcome. Independent source/evidence review completed before this draft was written. No runtime re-verification was performed after drafting. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A bounded ARM32 FAT input changes resolved child-dependency evidence solely with slice order; both complete observations retain enough raw subtype information to show the mismatch. No severe downstream consequence was observed. - Workaround status: unverified - Workaround basis: Reordering or thinning the library changes the analyzed artifact. No supported option or end-to-end consumer workflow was verified to preserve its identity while correcting the selection.
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#4091
No description provided.