[GH-ISSUE #3176] [Bug] Bundle dylib tracing reports a valid JVM class resource as malformed Mach-O #1114

Closed
opened 2026-10-15 18:31:12 +00:00 by bsakaguchi · 2 comments
Owner

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

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

Automatic process-root discovery in trace-dylib-resolution reports a valid JVM class resource as a malformed Mach-O when its constant pool contains a known Mach-O CPU value. The production CLI completed successfully but returned normalized_result.coverage.status: partial and included Contents/Resources/Look.class in unparsed_images, with the reason FAT architecture 0 slice range extends beyond the file. The bundle executable parsed successfully. A paired class without the CPU-shaped constant remained absent from the image list.

Removing only the lookalike resource returned complete coverage. Selecting the executable with --root Contents/MacOS/Example also returned complete coverage with both classes present. These observations came from the CLI using the artifact graph provider, without Hopper or another external analysis engine. No MCP runtime replay was performed.

Reproduction

  1. Start from a clean checkout at 58300885207133422af235e0f2b10658cf958b03, use the repository's pinned toolchain, and prepare the runtime. The independent verifier used an already prepared runtime and did not rerun these preparation commands.

    npm ci
    npm run build:cached
    
  2. From the checkout root, create a disposable bundle with a minimal arm64 executable header and two Java 21 class resources. The lookalike class contains two additional legal unused constants.

    python3 - <<'PY'
    from pathlib import Path
    import struct
    root = Path('repro/Example.app/Contents')
    (root / 'MacOS').mkdir(parents=True, exist_ok=True)
    (root / 'Resources').mkdir(exist_ok=True)
    (root / 'MacOS/Example').write_bytes(
        struct.pack('<8I', 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0))
    (root / 'Info.plist').write_text(
        '<?xml version="1.0"?><plist version="1.0"><dict>'
        '<key>CFBundleExecutable</key><string>Example</string>'
        '<key>CFBundlePackageType</key><string>APPL</string></dict></plist>')
    def utf8(value):
        data = value.encode()
        return b'\x01' + struct.pack('>H', len(data)) + data
    def java_class(lookalike):
        pool = [b'\x07\x00\x02', utf8('Hello'),
                b'\x07\x00\x04', utf8('java/lang/Object')]
        if lookalike:
            pool += [utf8('x'), b'\x03\x00\x00\x00\x07']
        pool += [utf8('x' * 4000)]
        return (struct.pack('>I3H', 0xcafebabe, 0, 65, len(pool) + 1)
                + b''.join(pool) + struct.pack('>7H', 0x21, 1, 3, 0, 0, 0, 0))
    for name, lookalike in [('Look', True), ('Plain', False)]:
        (root / 'Resources' / (name + '.class')).write_bytes(java_class(lookalike))
    PY
    
  3. Trace automatic bundle roots and inspect normalized_result.images and normalized_result.coverage in the evidence envelope. Observe the false malformed record for Look.class and partial coverage.

    node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json
    
  4. Move only the lookalike class outside the bundle and repeat. Coverage becomes complete, with only the executable in images. Restore the class before the next step.

    mv repro/Example.app/Contents/Resources/Look.class repro/Look.class
    node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json
    mv repro/Look.class repro/Example.app/Contents/Resources/Look.class
    
  5. With both classes present, select the known executable root explicitly. This returned complete coverage and only the parsed executable for this fixture.

    node scripts/rea.mjs trace-dylib-resolution repro/Example.app --root Contents/MacOS/Example --format json
    
  6. Add a deliberately malformed FAT resource and repeat automatic enumeration. On the verified baseline, both Broken.fat and Look.class appear as malformed and coverage remains partial. After a fix, Broken.fat must still make coverage partial while both JVM classes remain absent.

    python3 - <<'PY'
    from pathlib import Path
    import struct
    Path('repro/Example.app/Contents/Resources/Broken.fat').write_bytes(
        struct.pack('>7I', 0xcafebabe, 1, 0x0100000c, 0, 4096, 32, 12))
    PY
    node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json
    

Expected behavior

A valid JVM class resource must be excluded from the Mach-O image list and coverage.unparsed_images. Automatic enumeration of the executable and these unrelated class resources should report complete coverage.

docs/apple-application-analysis.md:145-169 documents tracing every executable in an app bundle and parsing Mach-O and FAT headers in TypeScript without executing the target or requiring Apple tools. The existing parser contract in src/artifacts/apple/MachoLoadCommandReader.test.ts:462-468 requires JVM class inputs sharing the FAT magic to return not-mach-o. tests/boundary/go/goBinaryImage.test.ts:331-341 explicitly covers the same constant-pool lookalike. That Go test corroborates the format distinction; its unsupported-image refusal is specific to the Go workflow and is not the expected dylib-trace response.

Impact and scope

The verified impact is P2. A bounded resource-file pattern produces a false malformed-image record and false partial-coverage result. The executable still parses and its trace remains usable. No execution or security boundary violation was demonstrated.

Explicit process-root selection worked for the single-root fixture. Users who know their process roots can supply them with --root, but must list every intended root because this bypasses automatic enumeration. Removing the resource also demonstrated that it caused the false partial result; this comparison is not a recommendation to change an application under analysis.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node 26.10.0; macOS 27.0 arm64

Target and artifact context

Synthetic Apple app bundle with a 32-byte arm64 MH_EXECUTE header, Info.plist, and two valid Java 21 class resources. Look.class is 4069 bytes and Plain.class is 4060 bytes. Their JVM structures differ by two legal unused constants. The version bytes resemble a 65-architecture FAT count, and the additional integer constant supplies a CPU-shaped value in the falsely interpreted table. The malformed FAT control declares a slice outside its file.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The following fields were observed in the production CLI evidence envelope:

normalized_result.images entry:
{"path":"Contents/Resources/Look.class","parse_status":"malformed","reason":"FAT architecture 0 slice range extends beyond the file","slices":[]}

normalized_result.coverage:
{"status":"partial","unparsed_images":["Contents/Resources/Look.class"],"roots_without_architecture":[]}

Without Look.class: complete; unparsed_images=[]; only the parsed executable appears.
With both classes and explicit executable root: complete; unparsed_images=[]; only the parsed executable appears.
With malformed FAT control: Broken.fat and Look.class are malformed; coverage is partial.

Typed outcome or recovery expected

Repeat automatic enumeration with the original bundle containing both classes. normalized_result.coverage.status must be complete, unparsed_images must be empty, neither JVM class may appear in images, and the executable must still appear as parsed. With the malformed FAT control present, Broken.fat must remain an unparsed malformed image and make coverage partial, while both JVM classes remain absent. This distinguishes correct JVM exclusion from suppressing all FAT failures.

Analysis or suspected cause

Source-derived reasoning at 58300885207133422af235e0f2b10658cf958b03: src/artifacts/apple/DylibResolutionReader.ts:175-205 scans magic-bearing bundle files and retains malformed results as unclassified. src/domain/apple/machoContainer.ts:115-124 treats a known CPU-shaped value as identity evidence and returns the range failure before confirming any slice header. src/artifacts/apple/MachoLoadCommandReader.ts:157-179 preserves that malformed classification. src/domain/apple/dylibResolution.ts:627-656 reports the image and derives partial coverage.

All 27 existing Mach-O reader tests passed in the independent verification. Their JVM cases omit the CPU-shaped lookalike. The Go boundary test containing that variant was inspected but not executed because the permitted source-test runner rejected the built test project. The production CLI replay establishes this dylib-trace failure independently of that unexecuted test.

Additional context

Related #2317, merged #2327, and merged #2458 address inventory labels and Go admission; this report concerns false malformed-image evidence and partial coverage during automatic dylib-trace enumeration after both merged fixes are already present.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3176 ### 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 Automatic process-root discovery in `trace-dylib-resolution` reports a valid JVM class resource as a malformed Mach-O when its constant pool contains a known Mach-O CPU value. The production CLI completed successfully but returned `normalized_result.coverage.status: partial` and included `Contents/Resources/Look.class` in `unparsed_images`, with the reason `FAT architecture 0 slice range extends beyond the file`. The bundle executable parsed successfully. A paired class without the CPU-shaped constant remained absent from the image list. Removing only the lookalike resource returned complete coverage. Selecting the executable with `--root Contents/MacOS/Example` also returned complete coverage with both classes present. These observations came from the CLI using the artifact graph provider, without Hopper or another external analysis engine. No MCP runtime replay was performed. ### Reproduction 1. Start from a clean checkout at `58300885207133422af235e0f2b10658cf958b03`, use the repository's pinned toolchain, and prepare the runtime. The independent verifier used an already prepared runtime and did not rerun these preparation commands. ```sh npm ci npm run build:cached ``` 2. From the checkout root, create a disposable bundle with a minimal arm64 executable header and two Java 21 class resources. The lookalike class contains two additional legal unused constants. ```sh python3 - <<'PY' from pathlib import Path import struct root = Path('repro/Example.app/Contents') (root / 'MacOS').mkdir(parents=True, exist_ok=True) (root / 'Resources').mkdir(exist_ok=True) (root / 'MacOS/Example').write_bytes( struct.pack('<8I', 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0)) (root / 'Info.plist').write_text( '<?xml version="1.0"?><plist version="1.0"><dict>' '<key>CFBundleExecutable</key><string>Example</string>' '<key>CFBundlePackageType</key><string>APPL</string></dict></plist>') def utf8(value): data = value.encode() return b'\x01' + struct.pack('>H', len(data)) + data def java_class(lookalike): pool = [b'\x07\x00\x02', utf8('Hello'), b'\x07\x00\x04', utf8('java/lang/Object')] if lookalike: pool += [utf8('x'), b'\x03\x00\x00\x00\x07'] pool += [utf8('x' * 4000)] return (struct.pack('>I3H', 0xcafebabe, 0, 65, len(pool) + 1) + b''.join(pool) + struct.pack('>7H', 0x21, 1, 3, 0, 0, 0, 0)) for name, lookalike in [('Look', True), ('Plain', False)]: (root / 'Resources' / (name + '.class')).write_bytes(java_class(lookalike)) PY ``` 3. Trace automatic bundle roots and inspect `normalized_result.images` and `normalized_result.coverage` in the evidence envelope. Observe the false malformed record for `Look.class` and partial coverage. ```sh node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json ``` 4. Move only the lookalike class outside the bundle and repeat. Coverage becomes complete, with only the executable in `images`. Restore the class before the next step. ```sh mv repro/Example.app/Contents/Resources/Look.class repro/Look.class node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json mv repro/Look.class repro/Example.app/Contents/Resources/Look.class ``` 5. With both classes present, select the known executable root explicitly. This returned complete coverage and only the parsed executable for this fixture. ```sh node scripts/rea.mjs trace-dylib-resolution repro/Example.app --root Contents/MacOS/Example --format json ``` 6. Add a deliberately malformed FAT resource and repeat automatic enumeration. On the verified baseline, both `Broken.fat` and `Look.class` appear as malformed and coverage remains partial. After a fix, `Broken.fat` must still make coverage partial while both JVM classes remain absent. ```sh python3 - <<'PY' from pathlib import Path import struct Path('repro/Example.app/Contents/Resources/Broken.fat').write_bytes( struct.pack('>7I', 0xcafebabe, 1, 0x0100000c, 0, 4096, 32, 12)) PY node scripts/rea.mjs trace-dylib-resolution repro/Example.app --format json ``` ### Expected behavior A valid JVM class resource must be excluded from the Mach-O image list and `coverage.unparsed_images`. Automatic enumeration of the executable and these unrelated class resources should report complete coverage. `docs/apple-application-analysis.md:145-169` documents tracing every executable in an app bundle and parsing Mach-O and FAT headers in TypeScript without executing the target or requiring Apple tools. The existing parser contract in `src/artifacts/apple/MachoLoadCommandReader.test.ts:462-468` requires JVM class inputs sharing the FAT magic to return `not-mach-o`. `tests/boundary/go/goBinaryImage.test.ts:331-341` explicitly covers the same constant-pool lookalike. That Go test corroborates the format distinction; its unsupported-image refusal is specific to the Go workflow and is not the expected dylib-trace response. ### Impact and scope The verified impact is P2. A bounded resource-file pattern produces a false malformed-image record and false partial-coverage result. The executable still parses and its trace remains usable. No execution or security boundary violation was demonstrated. Explicit process-root selection worked for the single-root fixture. Users who know their process roots can supply them with `--root`, but must list every intended root because this bypasses automatic enumeration. Removing the resource also demonstrated that it caused the false partial result; this comparison is not a recommendation to change an application under analysis. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node 26.10.0; macOS 27.0 arm64 ### Target and artifact context Synthetic Apple app bundle with a 32-byte arm64 `MH_EXECUTE` header, `Info.plist`, and two valid Java 21 class resources. `Look.class` is 4069 bytes and `Plain.class` is 4060 bytes. Their JVM structures differ by two legal unused constants. The version bytes resemble a 65-architecture FAT count, and the additional integer constant supplies a CPU-shaped value in the falsely interpreted table. The malformed FAT control declares a slice outside its file. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The following fields were observed in the production CLI evidence envelope: ```text normalized_result.images entry: {"path":"Contents/Resources/Look.class","parse_status":"malformed","reason":"FAT architecture 0 slice range extends beyond the file","slices":[]} normalized_result.coverage: {"status":"partial","unparsed_images":["Contents/Resources/Look.class"],"roots_without_architecture":[]} Without Look.class: complete; unparsed_images=[]; only the parsed executable appears. With both classes and explicit executable root: complete; unparsed_images=[]; only the parsed executable appears. With malformed FAT control: Broken.fat and Look.class are malformed; coverage is partial. ``` ### Typed outcome or recovery expected Repeat automatic enumeration with the original bundle containing both classes. `normalized_result.coverage.status` must be `complete`, `unparsed_images` must be empty, neither JVM class may appear in `images`, and the executable must still appear as parsed. With the malformed FAT control present, `Broken.fat` must remain an unparsed malformed image and make coverage partial, while both JVM classes remain absent. This distinguishes correct JVM exclusion from suppressing all FAT failures. ### Analysis or suspected cause Source-derived reasoning at `58300885207133422af235e0f2b10658cf958b03`: `src/artifacts/apple/DylibResolutionReader.ts:175-205` scans magic-bearing bundle files and retains malformed results as unclassified. `src/domain/apple/machoContainer.ts:115-124` treats a known CPU-shaped value as identity evidence and returns the range failure before confirming any slice header. `src/artifacts/apple/MachoLoadCommandReader.ts:157-179` preserves that malformed classification. `src/domain/apple/dylibResolution.ts:627-656` reports the image and derives partial coverage. All 27 existing Mach-O reader tests passed in the independent verification. Their JVM cases omit the CPU-shaped lookalike. The Go boundary test containing that variant was inspected but not executed because the permitted source-test runner rejected the built test project. The production CLI replay establishes this dylib-trace failure independently of that unexecuted test. ### Additional context Related [#2317](https://github.com/morluto/rea/issues/2317), merged [#2327](https://github.com/morluto/rea/pull/2327), and merged [#2458](https://github.com/morluto/rea/pull/2458) address inventory labels and Go admission; this report concerns false malformed-image evidence and partial coverage during automatic dylib-trace enumeration after both merged fixes are already present.
bsakaguchi 2026-10-15 18:31:12 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@Sriram-PR commented on GitHub (Oct 14, 2026):

I'll take this one, PR shortly.

<!-- gh-comment-id:6164530696 --> @Sriram-PR commented on GitHub (Oct 14, 2026): I'll take this one, PR shortly.
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Delivered on main in fffb07419.

CAFEBABE identity was inferred as FAT Mach-O before checking JVM framing. A shared class parser now distinguishes valid classes, FAT images and unresolved/truncated data at the owning boundary.

Validation: JVM archive acceptance and Mach-O parser controls, including complete large class framing, sparse uncertainty, and real Apple artifact CLI/MCP qualification. Full exact-revision qualification passed.

<!-- gh-comment-id:6177609471 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). CAFEBABE identity was inferred as FAT Mach-O before checking JVM framing. A shared class parser now distinguishes valid classes, FAT images and unresolved/truncated data at the owning boundary. Validation: JVM archive acceptance and Mach-O parser controls, including complete large class framing, sparse uncertainty, and real Apple artifact CLI/MCP qualification. Full exact-revision [qualification](https://github.com/morluto/rea/actions/runs/38629313022) passed.
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#1114
No description provided.