[GH-ISSUE #2661] fix(apple): retain later same-named slice settings in dylib findings #963

Closed
opened 2026-10-15 18:29:56 +00:00 by bsakaguchi · 0 comments
Owner

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

Summary

When I trace a universal Mach-O containing arm64e and arm64e.x1 executable slices, the findings can omit an environment command from the later slice. With different commands in both slices, the findings repeat the first slice's command and omit the second. The result's raw per-slice command arrays still contain the correct observations, so callers must inspect those arrays to recover the missing information.

Steps to reproduce

Use the source baseline and prepared dependencies stated under Runtime or environment. Save the following as a new src/artifacts/apple/dylibEnvironment.repro.test.ts. It builds tiny header-only fixtures from the repository's existing fixture helpers and calls the production filesystem reader used by the public tool. It reads the fixtures without executing their contents. The assertions capture the current incorrect behavior, so a passing run reproduces the defect rather than proving restoration.

import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { traceDylibResolution } from "./DylibResolutionReader.js";
import { CPU, machoImage, fatImage, dyldEnvironmentCommand } from "./MachoImage.fixture.js";

const base = process.env.FIXTURE_ROOT;
if (!base) throw new Error("Set FIXTURE_ROOT to an owned fixture directory");
const setting = "DYLD_PRINT_LIBRARIES=1";
it("checks same-named slice settings at the filesystem reader boundary", async () => {
  const cases = [
    { name: "second-only", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [false, true], findings: 0 },
    { name: "first-only", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [true, false], findings: 2 },
    { name: "both-distinct", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [true, true], findings: 2 },
    { name: "thin-second", cpus: [CPU.arm64eX1], settings: [true], findings: 1 },
  ];
  for (const fixture of cases) {
    const directory = join(base, fixture.name);
    await mkdir(directory, { recursive: true });
    const slices = fixture.cpus.map((cpu, index) => ({
      cpu,
      bytes: machoImage({ cpu, commands: fixture.settings[index] ? [dyldEnvironmentCommand(index === 0 ? setting : "DYLD_PRINT_RPATHS=1")] : [] }),
    }));
    const bytes = slices.length === 1 ? slices[0]!.bytes : fatImage(slices);
    const targetPath = join(directory, "Fixture");
    await writeFile(targetPath, bytes);
    const result = await traceDylibResolution({
      rootPath: directory, targetPath,
      targetSha256: createHash("sha256").update(bytes).digest("hex"),
      enumerateRoots: false, parameters: {},
    });
    const observation = {
      case: fixture.name, roots: result.roots,
      slices: result.images.flatMap(image => image.slices.map(slice => ({
        architecture: slice.architecture, cpu_subtype: slice.cpu_subtype,
        dyld_environment: slice.dyld_environment,
      }))),
      findings: result.findings, coverage: result.coverage,
    };
    console.log(JSON.stringify(observation));
    expect(result.images[0]?.parse_status).toBe("parsed");
    expect(result.roots).toHaveLength(slices.length);
    expect(result.findings).toHaveLength(fixture.findings);
    expect(result.coverage.status).toBe("complete");
    if (fixture.name === "second-only") {
      expect(result.images[0]?.slices[1]?.dyld_environment).toEqual(["DYLD_PRINT_RPATHS=1"]);
      expect(result.findings).toEqual([]);
    }
    if (fixture.name === "both-distinct") {
      expect(result.findings.every(finding => finding.explanation.includes(setting))).toBe(true);
      expect(result.findings.some(finding => finding.explanation.includes("DYLD_PRINT_RPATHS=1"))).toBe(false);
    }
  }
});

Run this exact source test with an owned fixture directory:

fixture_root="$(mktemp -d)"
FIXTURE_ROOT="$fixture_root" npm run test:focused -- src/artifacts/apple/dylibEnvironment.repro.test.ts

The verifier executed this test through a serialized launcher for the same source focused runner. The public CLI and MCP adapters were traced in source, rather than executed. No installation or build is required by this source-only test when locked dependencies are already prepared.

Expected behavior

The documented dyld-environment-present finding preserves observed LC_DYLD_ENVIRONMENT entries. Both parsed executable slices are traced roots, so findings must retain the later slice's diagnostic command and must not repeat a first-slice command as though it came from a different slice. Diagnostic settings do not require uncertain load-path coverage merely because they are present.

Actual behavior

The focused run passed one test covering four independent fixtures. In second-only, both slices parsed and became roots, but the later slice's observed command had no finding:

{
  "roots": [
    {"image": "Fixture", "architecture": "arm64e"},
    {"image": "Fixture", "architecture": "arm64e"}
  ],
  "slices": [
    {"architecture": "arm64e", "cpu_subtype": 2147483650, "dyld_environment": []},
    {"architecture": "arm64e", "cpu_subtype": 2147483660, "dyld_environment": ["DYLD_PRINT_RPATHS=1"]}
  ],
  "findings": [],
  "coverage": {"status": "complete", "unparsed_images": [], "roots_without_architecture": []}
}

With a command in the first slice only, the result contained two identical findings naming DYLD_PRINT_LIBRARIES=1. With distinct commands in both slices, both findings named that first-slice value and neither named DYLD_PRINT_RPATHS=1. A thin arm64e.x1 fixture returned one correct finding, which rules out rejection of that subtype or diagnostic command as the explanation. Complete coverage alone is not the claimed failure for these diagnostic-only fixtures.

Evidence

  • Expected source: docs/apple-application-analysis.md:213 documents preservation of observed commands in dyld-environment-present; src/contracts/artifactToolContracts.ts:110 exposes the header-only trace_dylib_resolution tool for active Mach-O targets.
  • Failure source: The executed filesystem-reader test and its output above; implementation locators are src/domain/apple/dylibResolutionFindings.ts:35, src/domain/apple/dylibResolution.ts:603, and src/artifacts/apple/MachoLoadCommandReader.ts:490.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Public-path applicability for an active universal Mach-O on an arm64 host was checked statically at src/cli/artifactCommands.ts:68, src/artifacts/ArtifactProvider.ts:296, and src/domain/binaryTarget.ts:109. The executed reader run passed request parsing, root selection, FAT/thin identity validation, file-digest validation and output schema parsing. This does not claim an executed public CLI/MCP or released-package reproduction.

Restoration check

Run the same fixtures after the change. The second-only result must include the observed later-slice command in findings. The first-only result must not manufacture a second observation of the first-slice command. The distinct-settings result must retain both commands with truthful slice association, and the thin control must continue to return its single correct finding. Preserve the raw per-slice observations. Changing only coverage to partial while still dropping a documented finding does not restore the failed contract; the implementation can choose how to represent slice identity.

Runtime or environment

Current upstream recheck at 3ae826ceba6f25b08bce50bcb51001209bc16b57 confirms the failure remains in the unchanged parser, reader, findings derivation, target admission and operation contracts; no additional source test, public CLI/MCP or published-package run was performed.

Source checkout at a2d8b94dd50dab8d966239dd63ab84718aa8b4b4, tested with Node 24.18.0 / npm 11.16.0 and locked dependencies. The focused runner used Vitest 5.0.3. This is source-adapter verification; the published rea-agents package was not exercised. The tiny FAT fixtures use CPU type 0x0100000c and subtypes 0x80000002 and 0x8000000c, with 4 KiB-aligned slices. Host architecture matters only to the statically checked active-target admission path, which was checked for arm64.

Additional context

Related: #1765, #1168.

The related architecture-label report morluto/rea#1765 concerns artifact inventory rejecting arm64e.x1 metadata. This report concerns a separate result contract after both slices have already parsed successfully: retaining each slice's observed commands in derived findings.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Derived analyst findings omit a parsed setting and can repeat a different slice's setting. The demonstrated failure is bounded to traced slices sharing a display architecture name; no executable launch or load result was observed.
  • Workaround status: available
  • Workaround basis: The same executed results retain the exact command values in images[].slices[].dyld_environment, alongside CPU subtype and physical slice metadata, so callers can inspect those raw observations without modifying the artifact or running its code.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2661 ## Summary When I trace a universal Mach-O containing arm64e and arm64e.x1 executable slices, the findings can omit an environment command from the later slice. With different commands in both slices, the findings repeat the first slice's command and omit the second. The result's raw per-slice command arrays still contain the correct observations, so callers must inspect those arrays to recover the missing information. ## Steps to reproduce Use the source baseline and prepared dependencies stated under Runtime or environment. Save the following as a new `src/artifacts/apple/dylibEnvironment.repro.test.ts`. It builds tiny header-only fixtures from the repository's existing fixture helpers and calls the production filesystem reader used by the public tool. It reads the fixtures without executing their contents. The assertions capture the current incorrect behavior, so a passing run reproduces the defect rather than proving restoration. ```ts import { createHash } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; import { traceDylibResolution } from "./DylibResolutionReader.js"; import { CPU, machoImage, fatImage, dyldEnvironmentCommand } from "./MachoImage.fixture.js"; const base = process.env.FIXTURE_ROOT; if (!base) throw new Error("Set FIXTURE_ROOT to an owned fixture directory"); const setting = "DYLD_PRINT_LIBRARIES=1"; it("checks same-named slice settings at the filesystem reader boundary", async () => { const cases = [ { name: "second-only", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [false, true], findings: 0 }, { name: "first-only", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [true, false], findings: 2 }, { name: "both-distinct", cpus: [CPU.arm64e, CPU.arm64eX1], settings: [true, true], findings: 2 }, { name: "thin-second", cpus: [CPU.arm64eX1], settings: [true], findings: 1 }, ]; for (const fixture of cases) { const directory = join(base, fixture.name); await mkdir(directory, { recursive: true }); const slices = fixture.cpus.map((cpu, index) => ({ cpu, bytes: machoImage({ cpu, commands: fixture.settings[index] ? [dyldEnvironmentCommand(index === 0 ? setting : "DYLD_PRINT_RPATHS=1")] : [] }), })); const bytes = slices.length === 1 ? slices[0]!.bytes : fatImage(slices); const targetPath = join(directory, "Fixture"); await writeFile(targetPath, bytes); const result = await traceDylibResolution({ rootPath: directory, targetPath, targetSha256: createHash("sha256").update(bytes).digest("hex"), enumerateRoots: false, parameters: {}, }); const observation = { case: fixture.name, roots: result.roots, slices: result.images.flatMap(image => image.slices.map(slice => ({ architecture: slice.architecture, cpu_subtype: slice.cpu_subtype, dyld_environment: slice.dyld_environment, }))), findings: result.findings, coverage: result.coverage, }; console.log(JSON.stringify(observation)); expect(result.images[0]?.parse_status).toBe("parsed"); expect(result.roots).toHaveLength(slices.length); expect(result.findings).toHaveLength(fixture.findings); expect(result.coverage.status).toBe("complete"); if (fixture.name === "second-only") { expect(result.images[0]?.slices[1]?.dyld_environment).toEqual(["DYLD_PRINT_RPATHS=1"]); expect(result.findings).toEqual([]); } if (fixture.name === "both-distinct") { expect(result.findings.every(finding => finding.explanation.includes(setting))).toBe(true); expect(result.findings.some(finding => finding.explanation.includes("DYLD_PRINT_RPATHS=1"))).toBe(false); } } }); ``` Run this exact source test with an owned fixture directory: ```sh fixture_root="$(mktemp -d)" FIXTURE_ROOT="$fixture_root" npm run test:focused -- src/artifacts/apple/dylibEnvironment.repro.test.ts ``` The verifier executed this test through a serialized launcher for the same source focused runner. The public CLI and MCP adapters were traced in source, rather than executed. No installation or build is required by this source-only test when locked dependencies are already prepared. ## Expected behavior The documented `dyld-environment-present` finding preserves observed `LC_DYLD_ENVIRONMENT` entries. Both parsed executable slices are traced roots, so findings must retain the later slice's diagnostic command and must not repeat a first-slice command as though it came from a different slice. Diagnostic settings do not require uncertain load-path coverage merely because they are present. ## Actual behavior The focused run passed one test covering four independent fixtures. In `second-only`, both slices parsed and became roots, but the later slice's observed command had no finding: ```json { "roots": [ {"image": "Fixture", "architecture": "arm64e"}, {"image": "Fixture", "architecture": "arm64e"} ], "slices": [ {"architecture": "arm64e", "cpu_subtype": 2147483650, "dyld_environment": []}, {"architecture": "arm64e", "cpu_subtype": 2147483660, "dyld_environment": ["DYLD_PRINT_RPATHS=1"]} ], "findings": [], "coverage": {"status": "complete", "unparsed_images": [], "roots_without_architecture": []} } ``` With a command in the first slice only, the result contained two identical findings naming `DYLD_PRINT_LIBRARIES=1`. With distinct commands in both slices, both findings named that first-slice value and neither named `DYLD_PRINT_RPATHS=1`. A thin arm64e.x1 fixture returned one correct finding, which rules out rejection of that subtype or diagnostic command as the explanation. Complete coverage alone is not the claimed failure for these diagnostic-only fixtures. ## Evidence - Expected source: `docs/apple-application-analysis.md:213` documents preservation of observed commands in `dyld-environment-present`; `src/contracts/artifactToolContracts.ts:110` exposes the header-only `trace_dylib_resolution` tool for active Mach-O targets. - Failure source: The executed filesystem-reader test and its output above; implementation locators are `src/domain/apple/dylibResolutionFindings.ts:35`, `src/domain/apple/dylibResolution.ts:603`, and `src/artifacts/apple/MachoLoadCommandReader.ts:490`. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Public-path applicability for an active universal Mach-O on an arm64 host was checked statically at `src/cli/artifactCommands.ts:68`, `src/artifacts/ArtifactProvider.ts:296`, and `src/domain/binaryTarget.ts:109`. The executed reader run passed request parsing, root selection, FAT/thin identity validation, file-digest validation and output schema parsing. This does not claim an executed public CLI/MCP or released-package reproduction. ## Restoration check Run the same fixtures after the change. The second-only result must include the observed later-slice command in findings. The first-only result must not manufacture a second observation of the first-slice command. The distinct-settings result must retain both commands with truthful slice association, and the thin control must continue to return its single correct finding. Preserve the raw per-slice observations. Changing only coverage to partial while still dropping a documented finding does not restore the failed contract; the implementation can choose how to represent slice identity. ## Runtime or environment Current upstream recheck at `3ae826ceba6f25b08bce50bcb51001209bc16b57` confirms the failure remains in the unchanged parser, reader, findings derivation, target admission and operation contracts; no additional source test, public CLI/MCP or published-package run was performed. Source checkout at `a2d8b94dd50dab8d966239dd63ab84718aa8b4b4`, tested with Node 24.18.0 / npm 11.16.0 and locked dependencies. The focused runner used Vitest 5.0.3. This is source-adapter verification; the published `rea-agents` package was not exercised. The tiny FAT fixtures use CPU type `0x0100000c` and subtypes `0x80000002` and `0x8000000c`, with 4 KiB-aligned slices. Host architecture matters only to the statically checked active-target admission path, which was checked for arm64. ## Additional context Related: #1765, #1168. The related architecture-label report [morluto/rea#1765](https://github.com/morluto/rea/issues/1765) concerns artifact inventory rejecting arm64e.x1 metadata. This report concerns a separate result contract after both slices have already parsed successfully: retaining each slice's observed commands in derived findings. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Derived analyst findings omit a parsed setting and can repeat a different slice's setting. The demonstrated failure is bounded to traced slices sharing a display architecture name; no executable launch or load result was observed. - Workaround status: available - Workaround basis: The same executed results retain the exact command values in `images[].slices[].dyld_environment`, alongside CPU subtype and physical slice metadata, so callers can inspect those raw observations without modifying the artifact or running its code.
bsakaguchi 2026-10-15 18:29:56 +00:00
  • closed this issue
  • added the
    bug
    label
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#963
No description provided.