[PR #2246] [CLOSED] fix(go): read Mach-O images with unmapped debug segments #5112

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

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2246
Author: @XiaoBinGan
Created: 10/12/2026
Status: ❌ Closed

Base: main ← Head: fix/go-macho-unmapped-debug-segment


📝 Commits (1)

  • 914cc40 fix(go): read Mach-O images with unmapped debug segments

📊 Changes

3 files changed (+104 additions, -16 deletions)

View changed files

📝 src/go/GoMachoImage.ts (+10 -2)
📝 tests/boundary/go/goBinaryImage.test.ts (+46 -0)
📝 tests/fixtures/go/image.ts (+48 -14)

📄 Description

Fixes #2167.

Summary

inspect-go-binary and MCP inspect_go_binary rejected a compiler-produced thin Mach-O executable whose __DWARF segment declares no virtual memory and no initial protection. Both interfaces returned an input-format error instead of the selected artifact's compiler, module and build-setting Evidence.

The rejection came from a single size check in the Mach-O container reader: a segment whose file bytes exceed its virtual size was treated as malformed. Apple's loader admits exactly that shape for a segment that occupies no address space and declares no initial protection, and the Go compiler emits its __DWARF segment that way. The check therefore rejected a valid, unmodified executable.

Problem and expected behavior

A Darwin arm64 or amd64 build produced by the default go build carries this segment layout (from a real Go 1.26.2 darwin/arm64 build):

__PAGEZERO       vmaddr=0x100000000 vmsize=4294967296 fileoff=0       filesize=0      initprot=0
__TEXT           vmaddr=0x100000000 vmsize=999424     fileoff=0       filesize=999424 initprot=5
__DATA_CONST     vmaddr=0x100018000 vmsize=89664      fileoff=999424  filesize=89664  initprot=3
__DATA           vmaddr=0x10001E000 vmsize=201640     fileoff=1097728 filesize=27680  initprot=3
__DWARF          vmaddr=0x100021000 vmsize=0          fileoff=1130496 filesize=506171 initprot=0   <-- filesize > vmsize
__LINKEDIT       vmaddr=0x100021000 vmsize=110818     fileoff=1638400 filesize=110818 initprot=1

__DWARF has vmsize = 0, initprot = 0, and filesize = 506171. It also shares the following __LINKEDIT segment's virtual address, and its file range overlaps __LINKEDIT's.

The affected implementation rejected the image as soon as filesize > vmsize, before the reader could locate the __go_buildinfo section. That discards a supported artifact for a property the platform itself allows: the file bytes of a non-mapped segment are legitimately allowed to exceed its empty virtual size, while the file range must still be checked.

Expected behavior is that the documented built-in reader returns the selected artifact's SHA-256, byte length, format: "macho", and its original build-information observations for this unmodified file, exactly as it already does for a -ldflags=-w build of the same module. The two interfaces share the service, so the CLI and MCP results move together.

Change and scope

The correction lives in the Mach-O segment loop of readGoMachoImage, which is the single owner that parses Mach-O segments:

  1. Derive whether the segment occupies address space at all: memorySize === 0n && protection === 0.
  2. Admit fileSize > memorySize only for such an unmapped segment. A mapped segment with an oversized file range is still rejected exactly as before.
  3. Keep an unmapped segment out of the container's address mappings (mappings.push). This is required, not cosmetic: the segment shares a virtual address with the following mapped segment, so admitting it would create two contradicting mappings for the same address, and mappedFileOffset correctly refuses such a pair as ambiguous. The file range itself is still validated by reader.fileRange, so an unmapped segment pointing outside the file remains malformed.

The segment's bytes stay reachable where they are legitimately needed: the local mapping value is still built and still used for the __go_buildinfo section consistency check, and the fallback search mapping is unaffected because it requires initial protection 3 and protection 3.

Contract and boundary impact

  • Semantic owner and earliest changed stage: the Go artifact's native-container admission boundary, inside the Mach-O reader. CLI, MCP tool and shared service layers are unchanged and continue to consume the same GoBinaryContainer facts.
  • CLI and MCP/tool-catalog contract: tool name, CLI command, input fields, output schema and error codes are unchanged. The affected artifact moves from invalid_request / invalid_input / invalid_format to a successful observed result with its build information.
  • Provider, target-format or platform compatibility: Mach-O containers only. ELF and PE readers are untouched. A mapped segment whose file bytes exceed its virtual size is still refused, so the relaxation cannot admit a genuinely inconsistent image.
  • Evidence, artifact, provenance or reconstruction contract: the artifact is read, not modified; its digest and byte count are unchanged. Result fields, confidence: "observed", authority, limitations and locations are the ones already produced for an accepted Mach-O image.
  • Process execution, authorization, cleanup or containment impact: none. This is static inspection of caller-supplied bytes; no target is opened, executed or mutated, and no provider engine is started.
  • Generated metadata, package or installation impact: none. No advertised schema, dependency, installation behaviour, authored skill or tracked generated catalog changes.

Evidence and regression coverage

The Go binary fixture now expresses the compiler's segment shape through a new unmappedDebugSegment option, which appends a __DWARF load command with vmsize = 0, initprot = 0, and overlapping virtual and file ranges, and reports the appended command's offset to the test.

I confirmed the regression is not vacuous by reverting the production change: the four positive cases fail with Mach-O segment addresses or sizes are invalid, which is the error the report describes. I also confirmed each half of the fix independently — dropping the !unmapped guard on the mapping push makes the positive cases fail with Virtual address has ambiguous file mappings, and removing the size condition entirely makes the control fail.

Verification performed on macOS 26.5.1 arm64 with Node.js 24.18.0 and npm 11.16.0:

  • npm run test:focused over goBinaryImage, goBinaryProvider, goBinaryMappings, goModuleText, goBinaryMcp and goBinaryCli: 134 passed.
  • npm run check:fast: passed.
  • npm run verify:test-discovery: all 944 test files discovered exactly once.
  • Public CLI end-to-end against a real Go 1.26.2 darwin/arm64 build whose __DWARF has vmsize = 0 and filesize = 506171. At the affected revision it exits 1 with Mach-O segment addresses or sizes are invalid; with this change it exits 0 and reports go_version: "go1.26.2", the module path and the recorded build settings.
  • A discriminating control build with -ldflags=-w (no debug segment) was accepted both before and after the change.

Unavailable coverage: no Windows, Linux, 32-bit host or real-provider lane was run. The __DWARF shape was reproduced with Go 1.26.2 rather than the reporter's Go 1.27.1, and the fixture is authored rather than compiler-produced.


🔄 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/2246 **Author:** [@XiaoBinGan](https://github.com/XiaoBinGan) **Created:** 10/12/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/go-macho-unmapped-debug-segment` --- ### 📝 Commits (1) - [`914cc40`](https://github.com/morluto/rea/commit/914cc4027e63c5610dfc59a0bda65a924a1fd699) fix(go): read Mach-O images with unmapped debug segments ### 📊 Changes **3 files changed** (+104 additions, -16 deletions) <details> <summary>View changed files</summary> 📝 `src/go/GoMachoImage.ts` (+10 -2) 📝 `tests/boundary/go/goBinaryImage.test.ts` (+46 -0) 📝 `tests/fixtures/go/image.ts` (+48 -14) </details> ### 📄 Description Fixes #2167. ## Summary `inspect-go-binary` and MCP `inspect_go_binary` rejected a compiler-produced thin Mach-O executable whose `__DWARF` segment declares no virtual memory and no initial protection. Both interfaces returned an input-format error instead of the selected artifact's compiler, module and build-setting Evidence. The rejection came from a single size check in the Mach-O container reader: a segment whose file bytes exceed its virtual size was treated as malformed. Apple's loader admits exactly that shape for a segment that occupies no address space and declares no initial protection, and the Go compiler emits its `__DWARF` segment that way. The check therefore rejected a valid, unmodified executable. ## Problem and expected behavior A Darwin arm64 or amd64 build produced by the default `go build` carries this segment layout (from a real Go 1.26.2 darwin/arm64 build): ```text __PAGEZERO vmaddr=0x100000000 vmsize=4294967296 fileoff=0 filesize=0 initprot=0 __TEXT vmaddr=0x100000000 vmsize=999424 fileoff=0 filesize=999424 initprot=5 __DATA_CONST vmaddr=0x100018000 vmsize=89664 fileoff=999424 filesize=89664 initprot=3 __DATA vmaddr=0x10001E000 vmsize=201640 fileoff=1097728 filesize=27680 initprot=3 __DWARF vmaddr=0x100021000 vmsize=0 fileoff=1130496 filesize=506171 initprot=0 <-- filesize > vmsize __LINKEDIT vmaddr=0x100021000 vmsize=110818 fileoff=1638400 filesize=110818 initprot=1 ``` `__DWARF` has `vmsize = 0`, `initprot = 0`, and `filesize = 506171`. It also shares the following `__LINKEDIT` segment's virtual address, and its file range overlaps `__LINKEDIT`'s. The [affected implementation](https://github.com/morluto/rea/blob/e4d3e1399d9cd4932653e7015a1bf8589e56029d/src/go/GoMachoImage.ts#L66-L70) rejected the image as soon as `filesize > vmsize`, before the reader could locate the `__go_buildinfo` section. That discards a supported artifact for a property the platform itself allows: the file bytes of a non-mapped segment are legitimately allowed to exceed its empty virtual size, while the file range must still be checked. Expected behavior is that the documented built-in reader returns the selected artifact's SHA-256, byte length, `format: "macho"`, and its original build-information observations for this unmodified file, exactly as it already does for a `-ldflags=-w` build of the same module. The two interfaces share the service, so the CLI and MCP results move together. ## Change and scope The correction lives in the Mach-O segment loop of [`readGoMachoImage`](https://github.com/XiaoBinGan/rea/blob/914cc4027e63c5610dfc59a0bda65a924a1fd699/src/go/GoMachoImage.ts#L66-L77), which is the single owner that parses Mach-O segments: 1. Derive whether the segment occupies address space at all: `memorySize === 0n && protection === 0`. 2. Admit `fileSize > memorySize` only for such an unmapped segment. A mapped segment with an oversized file range is still rejected exactly as before. 3. Keep an unmapped segment out of the container's address mappings ([`mappings.push`](https://github.com/XiaoBinGan/rea/blob/914cc4027e63c5610dfc59a0bda65a924a1fd699/src/go/GoMachoImage.ts#L92-L93)). This is required, not cosmetic: the segment shares a virtual address with the following mapped segment, so admitting it would create two contradicting mappings for the same address, and `mappedFileOffset` correctly refuses such a pair as ambiguous. The file range itself is still validated by `reader.fileRange`, so an unmapped segment pointing outside the file remains malformed. The segment's bytes stay reachable where they are legitimately needed: the local `mapping` value is still built and still used for the `__go_buildinfo` section consistency check, and the fallback search mapping is unaffected because it requires initial protection `3` and protection `3`. ## Contract and boundary impact - **Semantic owner and earliest changed stage:** the Go artifact's native-container admission boundary, inside the Mach-O reader. CLI, MCP tool and shared service layers are unchanged and continue to consume the same `GoBinaryContainer` facts. - **CLI and MCP/tool-catalog contract:** tool name, CLI command, input fields, output schema and error codes are unchanged. The affected artifact moves from `invalid_request` / `invalid_input` / `invalid_format` to a successful `observed` result with its build information. - **Provider, target-format or platform compatibility:** Mach-O containers only. ELF and PE readers are untouched. A mapped segment whose file bytes exceed its virtual size is still refused, so the relaxation cannot admit a genuinely inconsistent image. - **Evidence, artifact, provenance or reconstruction contract:** the artifact is read, not modified; its digest and byte count are unchanged. Result fields, `confidence: "observed"`, authority, limitations and locations are the ones already produced for an accepted Mach-O image. - **Process execution, authorization, cleanup or containment impact:** none. This is static inspection of caller-supplied bytes; no target is opened, executed or mutated, and no provider engine is started. - **Generated metadata, package or installation impact:** none. No advertised schema, dependency, installation behaviour, authored skill or tracked generated catalog changes. ## Evidence and regression coverage The Go binary fixture now expresses the compiler's segment shape through a new `unmappedDebugSegment` option, which appends a `__DWARF` load command with `vmsize = 0`, `initprot = 0`, and overlapping virtual and file ranges, and reports the appended command's offset to the test. - [`reads %d-bit %s Mach-O metadata beside an unmapped debug segment`](https://github.com/XiaoBinGan/rea/blob/914cc4027e63c5610dfc59a0bda65a924a1fd699/tests/boundary/go/goBinaryImage.test.ts#L439-L468) covers all four thin Mach-O layouts (32/64-bit × little/big-endian) and asserts the recovered `go_version`, module text and locations. Because the appended segment shares the mapped segment's virtual address, this assertion also fails if the segment is wrongly admitted as a mapping. - [`still rejects a mapped Mach-O segment whose file bytes exceed its virtual size`](https://github.com/XiaoBinGan/rea/blob/914cc4027e63c5610dfc59a0bda65a924a1fd699/tests/boundary/go/goBinaryImage.test.ts#L470-L483) is the control: it declares initial protection on the same segment and requires the malformed verdict to remain. I confirmed the regression is not vacuous by reverting the production change: the four positive cases fail with `Mach-O segment addresses or sizes are invalid`, which is the error the report describes. I also confirmed each half of the fix independently — dropping the `!unmapped` guard on the mapping push makes the positive cases fail with `Virtual address has ambiguous file mappings`, and removing the size condition entirely makes the control fail. Verification performed on macOS 26.5.1 arm64 with Node.js 24.18.0 and npm 11.16.0: - `npm run test:focused` over `goBinaryImage`, `goBinaryProvider`, `goBinaryMappings`, `goModuleText`, `goBinaryMcp` and `goBinaryCli`: 134 passed. - `npm run check:fast`: passed. - `npm run verify:test-discovery`: all 944 test files discovered exactly once. - Public CLI end-to-end against a real Go 1.26.2 darwin/arm64 build whose `__DWARF` has `vmsize = 0` and `filesize = 506171`. At the affected revision it exits 1 with `Mach-O segment addresses or sizes are invalid`; with this change it exits 0 and reports `go_version: "go1.26.2"`, the module path and the recorded build settings. - A discriminating control build with `-ldflags=-w` (no debug segment) was accepted both before and after the change. Unavailable coverage: no Windows, Linux, 32-bit host or real-provider lane was run. The `__DWARF` shape was reproduced with Go 1.26.2 rather than the reporter's Go 1.27.1, and the fixture is authored rather than compiler-produced. --- <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:03 +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#5112
No description provided.