[PR #1196] [MERGED] fix(native): refuse truncated executable headers at target admission #2709

Closed
opened 2026-10-17 21:09:19 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1196
Author: @kaoru0822-kitauji
Created: 10/9/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: fix/target-truncated-headers


📝 Commits (2)

  • 0b04d38 fix(native): refuse truncated executable headers at target admission
  • facaae8 test(native): build the .app fixture executable with a complete Mach-O header

📊 Changes

7 files changed (+140 additions, -21 deletions)

View changed files

📝 src/application/BinaryTargetResolver.ts (+5 -2)
📝 src/domain/binaryTarget.fixture.ts (+4 -2)
📝 src/domain/binaryTarget.test.ts (+49 -0)
📝 src/domain/binaryTarget.ts (+51 -12)
📝 tests/boundary/filesystem/binaryTarget.test.ts (+26 -0)
📝 tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts (+2 -4)
📝 tests/composition/analysis-sessions/appleApplication.test.ts (+3 -1)

📄 Description

Summary

open_binary (and every CLI analysis command) admitted a 12-byte prefix of a thin Mach-O as a valid arm64 executable. The first provider query then failed as a generic execution_failure with "retry or run doctor" advice: Ghidra's import failed, and on macOS inspect_macho failed because lipo couldn't read the file. parseThinMachO required only 8 bytes (magic and CPU type) before returning executable metadata.

Fixes #1189

Adjacent representations

The same structure appears in the other header parsers:

Format Before Now
Thin Mach-O 8 bytes full 28/32-byte header, and sizeofcmds must fit in the file
ELF 20 bytes full 52-byte (ELF32) or 64-byte (ELF64) header
FAT Mach-O architecture table only the selected host slice's offset + size must lie within the file
PE / DOS MZ already validated their headers and declared tables unchanged

Each refusal names the failed constraint, for example truncated Mach-O header: a 64-bit header needs 32 bytes; the file has 12, or truncated FAT slice: the arm64 slice ends at byte 154208; the file has 20000. It surfaces through the existing BinaryTargetError path, the same path that already refuses a 7-byte file, so no provider starts. Only the slice REA would analyze must be present; a missing non-host slice does not block admission.

BinaryTargetResolver now passes the real file size for Mach-O, ELF, and FAT headers, not just DOS images. The resolver reads only a 4 KiB prefix, and header commitments such as load commands and slices extend past it. A boundary test covers load commands that end beyond the prefix.

Tests

  • src/domain/binaryTarget.test.ts (executable header completeness): headers shorter than their fixed size (Mach-O 32/64-bit, ELF32/64), load commands past the file, and a host FAT slice past the file are refused with their constraint. The same bytes are admitted when the file is long enough, and a missing non-host slice is ignored.
  • tests/boundary/filesystem/binaryTarget.test.ts: parseBinaryTarget refuses the issue's 12-byte prefix with the named reason, and admits a Mach-O whose load commands end beyond the 4 KiB probe.
  • 7 of the new cases fail on main.
  • src/domain/binaryTarget.fixture.ts: elf() and thinMach() now build complete headers. artifactProvider.directoryErrors.test.ts used a hand-written 8-byte Mach-O and now uses thinMach().

Verification

  • npm run check:fast, npm run jscpd
  • npm run test:focused -- 57 test files that use these fixtures, parseBinaryTarget, parseExecutableHeader, open_binary, or the Mach-O image builders, plus 22 files that write raw ELF/Mach-O/FAT bytes. All pass except the ibtool-backed NIB tests, which need full Xcode and fail identically on main on this machine (Command Line Tools only).
  • Built CLI, inspect-macho: the issue's 12-byte prefix → target_unavailable with the header constraint (on main: execution_failure from lipo); /bin/ls cut to 20,000 bytes → the FAT slice constraint; /bin/ls, a thin arm64e slice, and /usr/lib/dyld still open.

No real Ghidra verification was run (Ghidra is not installed here). The change happens before any provider starts, and the issue's Ghidra reproduction fails at the same admission step this now refuses.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1196 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/9/2026 **Status:** ✅ Merged **Merged:** 10/9/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `fix/target-truncated-headers` --- ### 📝 Commits (2) - [`0b04d38`](https://github.com/morluto/rea/commit/0b04d384cca4f22f271d60bcfe67f79cab89c8de) fix(native): refuse truncated executable headers at target admission - [`facaae8`](https://github.com/morluto/rea/commit/facaae865af243656ab8f557aeb42f5909748f2b) test(native): build the .app fixture executable with a complete Mach-O header ### 📊 Changes **7 files changed** (+140 additions, -21 deletions) <details> <summary>View changed files</summary> 📝 `src/application/BinaryTargetResolver.ts` (+5 -2) 📝 `src/domain/binaryTarget.fixture.ts` (+4 -2) 📝 `src/domain/binaryTarget.test.ts` (+49 -0) 📝 `src/domain/binaryTarget.ts` (+51 -12) 📝 `tests/boundary/filesystem/binaryTarget.test.ts` (+26 -0) 📝 `tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts` (+2 -4) 📝 `tests/composition/analysis-sessions/appleApplication.test.ts` (+3 -1) </details> ### 📄 Description ## Summary `open_binary` (and every CLI analysis command) admitted a 12-byte prefix of a thin Mach-O as a valid arm64 executable. The first provider query then failed as a generic `execution_failure` with "retry or run doctor" advice: Ghidra's import failed, and on macOS `inspect_macho` failed because `lipo` couldn't read the file. `parseThinMachO` required only 8 bytes (magic and CPU type) before returning executable metadata. Fixes #1189 ## Adjacent representations The same structure appears in the other header parsers: | Format | Before | Now | |---|---|---| | Thin Mach-O | 8 bytes | full 28/32-byte header, and `sizeofcmds` must fit in the file | | ELF | 20 bytes | full 52-byte (ELF32) or 64-byte (ELF64) header | | FAT Mach-O | architecture table only | the selected host slice's `offset + size` must lie within the file | | PE / DOS MZ | already validated their headers and declared tables | unchanged | Each refusal names the failed constraint, for example `truncated Mach-O header: a 64-bit header needs 32 bytes; the file has 12`, or `truncated FAT slice: the arm64 slice ends at byte 154208; the file has 20000`. It surfaces through the existing `BinaryTargetError` path, the same path that already refuses a 7-byte file, so no provider starts. Only the slice REA would analyze must be present; a missing non-host slice does not block admission. `BinaryTargetResolver` now passes the real file size for Mach-O, ELF, and FAT headers, not just DOS images. The resolver reads only a 4 KiB prefix, and header commitments such as load commands and slices extend past it. A boundary test covers load commands that end beyond the prefix. ## Tests - `src/domain/binaryTarget.test.ts` (`executable header completeness`): headers shorter than their fixed size (Mach-O 32/64-bit, ELF32/64), load commands past the file, and a host FAT slice past the file are refused with their constraint. The same bytes are admitted when the file is long enough, and a missing non-host slice is ignored. - `tests/boundary/filesystem/binaryTarget.test.ts`: `parseBinaryTarget` refuses the issue's 12-byte prefix with the named reason, and admits a Mach-O whose load commands end beyond the 4 KiB probe. - 7 of the new cases fail on `main`. - `src/domain/binaryTarget.fixture.ts`: `elf()` and `thinMach()` now build complete headers. `artifactProvider.directoryErrors.test.ts` used a hand-written 8-byte Mach-O and now uses `thinMach()`. ## Verification - `npm run check:fast`, `npm run jscpd` - `npm run test:focused --` 57 test files that use these fixtures, `parseBinaryTarget`, `parseExecutableHeader`, `open_binary`, or the Mach-O image builders, plus 22 files that write raw ELF/Mach-O/FAT bytes. All pass except the `ibtool`-backed NIB tests, which need full Xcode and fail identically on `main` on this machine (Command Line Tools only). - Built CLI, `inspect-macho`: the issue's 12-byte prefix → `target_unavailable` with the header constraint (on `main`: `execution_failure` from `lipo`); `/bin/ls` cut to 20,000 bytes → the FAT slice constraint; `/bin/ls`, a thin `arm64e` slice, and `/usr/lib/dyld` still open. No real Ghidra verification was run (Ghidra is not installed here). The change happens before any provider starts, and the issue's Ghidra reproduction fails at the same admission step this now refuses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:09:19 +00:00
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#2709
No description provided.