[GH-ISSUE #2285] [Bug] Headless IDA private input for a universal Mach-O includes bytes after the selected slice #754

Closed
opened 2026-10-15 18:28:00 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @nikolay-dementiev on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2285

I'm working on this and will open a PR.

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

Since #2201, the headless IDA provider copies only the selected slice of a universal Mach-O into its private input. When another slice follows the selected one and the file continues past the next 64 KiB read boundary, the private input also receives bytes after the selected slice. The private-input digest is computed over those extra bytes as well.

Reproduction

macOS arm64, IDA Professional 9.1 (9.1.250226) headless, ida-pro-mcp c133c38, REA built from main at 11fe5a13f.

// hello.c
#include <stdio.h>
int square(int x) { return x * x; }
int main(void) { printf("%d\n", square(7)); return 0; }
for a in x86_64 arm64 arm64e; do clang -O0 -arch $a -o h_$a hello.c; done
lipo -create h_x86_64 h_arm64 h_arm64e -output fat3
lipo -detailed_info fat3   # x86_64 @4096 (8480), arm64 @16384 (33464), arm64e @65536 (33464); 99000 bytes

Open fat3 with provider_id: "ida" and run list_procedures. REA selects the arm64 slice. While the session is open, the private input <tmpdir>/rea-ida-*/fat3 is 51240 bytes. Its first 33464 bytes are the arm64 slice, and its last 17776 bytes are the first 17776 bytes of the arm64e slice. IDA still analyses ARM code here, so the trailing bytes are not visible in the results.

The same arithmetic gives 91296 bytes instead of 37456 for /usr/bin/caffeinate when its first slice (x86_64 at 16384) is selected, for example on an x86_64 host.

Expected behavior

The private input is exactly the selected slice: its length is the slice size and its digest is the slice digest.

Impact and scope

Universal Mach-O targets analysed with headless IDA where the selected slice is not the last one and does not end on a 64 KiB boundary. Not in a release yet.

REA version

main at 11fe5a13f.

Runtime and platform

macOS 26.5.2 (arm64), Node 26.3.0; IDA Professional 9.1 (9.1.250226) with idalib; ida-pro-mcp 2.0.0 at c133c3853.

Target and artifact context

The 3-line hello.c above, built for three architectures and combined with lipo. No private target data.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Analysis or suspected cause

IdaWorkspace.copyInput() (src/ida/IdaWorkspace.ts) reads the source in 64 KiB chunks and writes bytes.subarray(start - position, end - position) with end = Math.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 treats a negative end as an offset from the chunk end, so it returns a non-empty prefix of that chunk. The tests in src/ida/IdaWorkspaceThinning.test.ts use 32-byte slices inside the first chunk, so they never reach a second chunk.

Originally created by @nikolay-dementiev on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2285 > I'm working on this and will open a PR. ### Before submitting - [x] I searched the open and closed issues and pull requests for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior Since #2201, the headless IDA provider copies only the selected slice of a universal Mach-O into its private input. When another slice follows the selected one and the file continues past the next 64 KiB read boundary, the private input also receives bytes after the selected slice. The private-input digest is computed over those extra bytes as well. ### Reproduction macOS arm64, IDA Professional 9.1 (9.1.250226) headless, ida-pro-mcp c133c38, REA built from `main` at 11fe5a13f. ```c // hello.c #include <stdio.h> int square(int x) { return x * x; } int main(void) { printf("%d\n", square(7)); return 0; } ``` ```sh for a in x86_64 arm64 arm64e; do clang -O0 -arch $a -o h_$a hello.c; done lipo -create h_x86_64 h_arm64 h_arm64e -output fat3 lipo -detailed_info fat3 # x86_64 @4096 (8480), arm64 @16384 (33464), arm64e @65536 (33464); 99000 bytes ``` Open `fat3` with `provider_id: "ida"` and run `list_procedures`. REA selects the `arm64` slice. While the session is open, the private input `<tmpdir>/rea-ida-*/fat3` is 51240 bytes. Its first 33464 bytes are the `arm64` slice, and its last 17776 bytes are the first 17776 bytes of the `arm64e` slice. IDA still analyses ARM code here, so the trailing bytes are not visible in the results. The same arithmetic gives 91296 bytes instead of 37456 for `/usr/bin/caffeinate` when its first slice (`x86_64` at 16384) is selected, for example on an x86_64 host. ### Expected behavior The private input is exactly the selected slice: its length is the slice size and its digest is the slice digest. ### Impact and scope Universal Mach-O targets analysed with headless IDA where the selected slice is not the last one and does not end on a 64 KiB boundary. Not in a release yet. ### REA version `main` at 11fe5a13f. ### Runtime and platform macOS 26.5.2 (arm64), Node 26.3.0; IDA Professional 9.1 (9.1.250226) with idalib; ida-pro-mcp 2.0.0 at c133c3853. ### Target and artifact context The 3-line `hello.c` above, built for three architectures and combined with `lipo`. No private target data. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Analysis or suspected cause `IdaWorkspace.copyInput()` (`src/ida/IdaWorkspace.ts`) reads the source in 64 KiB chunks and writes `bytes.subarray(start - position, end - position)` with `end = Math.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` treats a negative end as an offset from the chunk end, so it returns a non-empty prefix of that chunk. The tests in `src/ida/IdaWorkspaceThinning.test.ts` use 32-byte slices inside the first chunk, so they never reach a second chunk.
bsakaguchi 2026-10-15 18:28:00 +00:00
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Root Cause & Slicing Calculation Review

The analysis in the report precisely locates the negative offset bug in IdaWorkspace.copyInput():

  • When calculating end = Math.min(position + bytes.length, slice.offset + slice.size), chunks processed after slice.offset + slice.size yield end - position < 0.
  • In Node.js Buffer.subarray(start, end), a negative end parameter indexes backwards from the end of the buffer, causing unintentional trailing chunk leakage into the sliced Mach-O image.
  • Fix: Terminate chunk copying when position >= slice.offset + slice.size, and clamp Math.max(0, ...) before calling subarray. Looking forward to the PR.
<!-- gh-comment-id:6134315610 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Root Cause & Slicing Calculation Review The analysis in the report precisely locates the negative offset bug in `IdaWorkspace.copyInput()`: - When calculating `end = Math.min(position + bytes.length, slice.offset + slice.size)`, chunks processed after `slice.offset + slice.size` yield `end - position < 0`. - In Node.js `Buffer.subarray(start, end)`, a negative end parameter indexes backwards from the end of the buffer, causing unintentional trailing chunk leakage into the sliced Mach-O image. - **Fix:** Terminate chunk copying when `position >= slice.offset + slice.size`, and clamp `Math.max(0, ...)` before calling `subarray`. Looking forward to the PR.
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#754
No description provided.