[PR #2291] [MERGED] fix(ida): stop the private input of a universal Mach-O at the end of the selected slice #5143

Closed
opened 2026-10-31 01:04:18 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2291
Author: @nikolay-dementiev
Created: 10/12/2026
Status: ✅ Merged
Merged: 10/13/2026
Merged by: @morluto

Base: main ← Head: fix/ida-fat-slice-copy-bounds


📝 Commits (2)

  • 8499cce test(ida): expect the private input of a universal Mach-O to end at the selected slice
  • bb7d139 fix(ida): stop the private input of a universal Mach-O at the end of the selected slice

📊 Changes

2 files changed (+96 additions, -3 deletions)

View changed files

📝 src/ida/IdaWorkspace.ts (+4 -2)
📝 src/ida/IdaWorkspaceThinning.test.ts (+92 -1)

📄 Description

Summary

When IDA opens a universal Mach-O, REA copies only the selected slice into the private input. The copy also appended the head of the following slice whenever the selected slice ended before a 64 KiB read boundary and another slice followed it. The copy now ends exactly at the selected slice.

Fixes #2285

Regression of #2201, which closed #2139.

Problem and expected behavior

IdaWorkspace.copyInput() reads the admitted source in 64 KiB chunks and writes bytes.subarray(start - position, end - position) with start = max(position, slice.offset) and end = min(position + bytes.length, slice.offset + slice.size). For the first chunk that starts after the slice end, end - position is negative, and Buffer.subarray counts a negative end from the end of the chunk, so it returns a non-empty prefix. Those bytes were written to the private input and added to its digest.

Real reproduction (macOS arm64, IDA Professional 9.1): clang -O0 -arch x86_64, -arch arm64 and -arch arm64e of one source, combined with lipo -create: x86_64 at 4096 (8480 bytes), arm64 at 16384 (33464 bytes), arm64e at 65536 (33464 bytes), 99000 bytes in total. REA selects the arm64 slice on this host; the private copy was 51240 bytes, not 33464. The first 33464 bytes equal the arm64 slice and the last 17776 bytes are the first 17776 bytes of the arm64e slice. IDA still analysed ARM code here because its loader ignored the trailing bytes. /usr/bin/caffeinate with its first slice selected gave 91296 bytes instead of 37456. The bug does not appear when the selected slice is the last one or ends on a 64 KiB boundary.

Expected: the private input is exactly the selected slice (length slice.size, SHA-256 equal to the slice's).

Change and scope

  • src/ida/IdaWorkspace.ts: write and hash a chunk's bytes only when end > start. The whole-source digest check and the copy of non-universal targets are unchanged.

Intentionally not included: no change to slice selection or admission, and no docs change (no document describes the copy semantics beyond "thinned to the selected slice").

Contract and boundary impact

  • Semantic owner and earliest changed stage: the IDA workspace, at the private input copy.
  • CLI and MCP/tool-catalog contract: none.
  • Provider, bridge, target-format, or platform compatibility: IDA headless profile with universal Mach-O targets; other formats and providers are unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: the private input digest and the bytes IDA receives now equal the selected slice; nothing else in Evidence changes.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata/package/installation impact: none.

Evidence and regression coverage

  • Tests added or updated: src/ida/IdaWorkspaceThinning.test.ts (beside the existing thinning tests, real workspace and real file, no mocks): a 99000-byte universal file with the layout above (synthetic bytes, each slice filled with its own value) and a host that selects the middle slice; and a universal file whose first slice is selected with another slice after it. Each asserts that the private input length equals slice.size, that its bytes equal the slice, and that inputSha256 equals the slice's SHA-256.
  • Base reproduction or other evidence: both new tests fail on ec2d251c0 with the private input at 51240 bytes instead of 33464 and 59224 instead of 37456. Real IDA Professional 9.1 (headless idalib, ida-pro-mcp c133c38, macOS arm64) on the lipo layout above: on ec2d251c0 the private input was 51240 bytes; with this branch it is 33464 bytes and byte-identical to the arm64 slice, IDA still analyses ARM code, rea function /usr/bin/caffeinate start still returns arm64e code at 0x100000740, and npm run verify:ida passes.
  • Remaining proof gaps: only macOS arm64 was exercised; other hosts and IDA versions were not.
  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run check:fast, npm run check, npm run verify:test-discovery (950 files) - green
  • npm run test:focused -- src/ida/IdaWorkspaceThinning.test.ts - 7 passed
  • npm run test:fast - 404 files, 5245 passed, 7 skipped, 0 failed
  • Mutation check, reverted: replacing the new guard with true fails both new tests (51240 and 59224 bytes)

macOS arm64 only; Linux and Windows were not executed. test:boundary was not run because no boundary file is touched.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: real IDA 9.1 on macOS arm64 (see above); Hopper, Ghidra and browser not applicable.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: the private input no longer contains bytes outside the admitted slice.

Review checklist

  • The change addresses a concrete problem, not an unscoped refactor.
  • The PR has one focused outcome, and the title follows type(scope): outcome.
  • Related issue is linked.
  • Tests cover changed behavior and failure paths.
  • Owning docs, contracts, and generated metadata are updated.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 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/2291 **Author:** [@nikolay-dementiev](https://github.com/nikolay-dementiev) **Created:** 10/12/2026 **Status:** ✅ Merged **Merged:** 10/13/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/ida-fat-slice-copy-bounds` --- ### 📝 Commits (2) - [`8499cce`](https://github.com/morluto/rea/commit/8499cce37f6d93b8806144fa5cab11313aafdb3b) test(ida): expect the private input of a universal Mach-O to end at the selected slice - [`bb7d139`](https://github.com/morluto/rea/commit/bb7d139b20e08662b29bad98e3c4abd88b9b80e3) fix(ida): stop the private input of a universal Mach-O at the end of the selected slice ### 📊 Changes **2 files changed** (+96 additions, -3 deletions) <details> <summary>View changed files</summary> 📝 `src/ida/IdaWorkspace.ts` (+4 -2) 📝 `src/ida/IdaWorkspaceThinning.test.ts` (+92 -1) </details> ### 📄 Description ## Summary When IDA opens a universal Mach-O, REA copies only the selected slice into the private input. The copy also appended the head of the following slice whenever the selected slice ended before a 64 KiB read boundary and another slice followed it. The copy now ends exactly at the selected slice. Fixes #2285 Regression of #2201, which closed #2139. ## Problem and expected behavior `IdaWorkspace.copyInput()` reads the admitted source in 64 KiB chunks and writes `bytes.subarray(start - position, end - position)` with `start = max(position, slice.offset)` and `end = min(position + bytes.length, slice.offset + slice.size)`. For the first chunk that starts after the slice end, `end - position` is negative, and `Buffer.subarray` counts a negative end from the end of the chunk, so it returns a non-empty prefix. Those bytes were written to the private input and added to its digest. Real reproduction (macOS arm64, IDA Professional 9.1): `clang -O0 -arch x86_64`, `-arch arm64` and `-arch arm64e` of one source, combined with `lipo -create`: x86_64 at 4096 (8480 bytes), arm64 at 16384 (33464 bytes), arm64e at 65536 (33464 bytes), 99000 bytes in total. REA selects the arm64 slice on this host; the private copy was 51240 bytes, not 33464. The first 33464 bytes equal the arm64 slice and the last 17776 bytes are the first 17776 bytes of the arm64e slice. IDA still analysed ARM code here because its loader ignored the trailing bytes. `/usr/bin/caffeinate` with its first slice selected gave 91296 bytes instead of 37456. The bug does not appear when the selected slice is the last one or ends on a 64 KiB boundary. Expected: the private input is exactly the selected slice (length `slice.size`, SHA-256 equal to the slice's). ## Change and scope - `src/ida/IdaWorkspace.ts`: write and hash a chunk's bytes only when `end > start`. The whole-source digest check and the copy of non-universal targets are unchanged. Intentionally not included: no change to slice selection or admission, and no docs change (no document describes the copy semantics beyond "thinned to the selected slice"). ## Contract and boundary impact - Semantic owner and earliest changed stage: the IDA workspace, at the private input copy. - CLI and MCP/tool-catalog contract: none. - Provider, bridge, target-format, or platform compatibility: IDA headless profile with universal Mach-O targets; other formats and providers are unchanged. - Evidence, artifact, provenance, or reconstruction contract: the private input digest and the bytes IDA receives now equal the selected slice; nothing else in Evidence changes. - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata/package/installation impact: none. ## Evidence and regression coverage - Tests added or updated: `src/ida/IdaWorkspaceThinning.test.ts` (beside the existing thinning tests, real workspace and real file, no mocks): a 99000-byte universal file with the layout above (synthetic bytes, each slice filled with its own value) and a host that selects the middle slice; and a universal file whose first slice is selected with another slice after it. Each asserts that the private input length equals `slice.size`, that its bytes equal the slice, and that `inputSha256` equals the slice's SHA-256. - Base reproduction or other evidence: both new tests fail on `ec2d251c0` with the private input at 51240 bytes instead of 33464 and 59224 instead of 37456. Real IDA Professional 9.1 (headless idalib, ida-pro-mcp c133c38, macOS arm64) on the `lipo` layout above: on `ec2d251c0` the private input was 51240 bytes; with this branch it is 33464 bytes and byte-identical to the arm64 slice, IDA still analyses ARM code, `rea function /usr/bin/caffeinate start` still returns arm64e code at `0x100000740`, and `npm run verify:ida` passes. - Remaining proof gaps: only macOS arm64 was exercised; other hosts and IDA versions were not. - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npm run check:fast`, `npm run check`, `npm run verify:test-discovery` (950 files) - green - `npm run test:focused -- src/ida/IdaWorkspaceThinning.test.ts` - 7 passed - `npm run test:fast` - 404 files, 5245 passed, 7 skipped, 0 failed - Mutation check, reverted: replacing the new guard with `true` fails both new tests (51240 and 59224 bytes) macOS arm64 only; Linux and Windows were not executed. `test:boundary` was not run because no boundary file is touched. ## Compatibility, safety, and release - Breaking changes or migration steps: none. - Real Hopper/Ghidra, browser, or OS coverage: real IDA 9.1 on macOS arm64 (see above); Hopper, Ghidra and browser not applicable. - Package or release metadata impact: none. - Security, privacy, process, or containment review: the private input no longer contains bytes outside the admitted slice. ## Review checklist - [x] The change addresses a concrete problem, not an unscoped refactor. - [x] The PR has one focused outcome, and the title follows `type(scope): outcome`. - [x] Related issue is linked. - [x] Tests cover changed behavior and failure paths. - [x] Owning docs, contracts, and generated metadata are updated. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:04:18 +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#5143
No description provided.