[PR #2089] [CLOSED] fix(go): read build info from unstripped Mach-O binaries #5016

Closed
opened 2026-10-31 00:58:06 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

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

Base: main ← Head: fix/go-macho-unmapped-dwarf


📝 Commits (3)

  • 7955a85 fix(go): read build info from unstripped Mach-O binaries
  • 33eb746 fix(go): still bound-check an unmapped Mach-O segment's file range
  • bfbf99c fix(go): exempt only unprotected zero-vmsize Mach-O segments

📊 Changes

2 files changed (+94 additions, -6 deletions)

View changed files

📝 src/go/GoMachoImage.ts (+13 -4)
📝 tests/boundary/go/goBinaryImage.test.ts (+81 -2)

📄 Description

Summary

inspect-go-binary refuses every Go binary built for macOS unless debug info was stripped with -w. A plain GOOS=darwin go build or go install gives invalid_request, "Mach-O segment addresses or sizes are invalid", where go version -m reads the same file. No issue is linked. I found this by comparing REA with go version -m on cross-compiled binaries, and the fix is small enough to explain here.

Problem and expected behavior

Go's linker writes DWARF into a __DWARF segment that has file bytes but vmsize 0, because the segment is never mapped. Both link modes do this: internal linking in cmd/link/internal/ld/macho.go (ms.vsize = 0), and external linking in macho_combine_dwarf.go (Memsz = 0). readGoMachoImage in src/go/GoMachoImage.ts refuses any segment whose filesize exceeds its vmsize, so it refuses the whole file.

The segment table of a Go 1.27.1 darwin/arm64 hello world:

Segment vmaddr vmsize fileoff filesize
__DATA 0x100114000 0x31710 0x114000 0x6f40
__DWARF 0x100148000 0x0 0x11c000 0x85968
__LINKEDIT 0x100148000 0x1b3b2 0x1a4000 0x1b3b2
$ GOOS=darwin GOARCH=arm64 go build -o hello .
$ go version -m hello
hello: go1.27.1
	path	example.com/hello
$ rea inspect-go-binary hello --format json
{"code":"invalid_request","message":"Analysis input is invalid. Check the arguments and try again.", ...}

Expected: the build info go version -m reports. The repository's own check, scripts/verify-go-binary.mjs, builds every target with -ldflags=-s -w, which drops this segment, so it never meets one.

Change and scope

  • A segment with vmsize 0 and initial protection 0 is treated as unmapped. It skips the filesize > vmsize check and gets no virtual mapping, the same as a segment with no file bytes. Its file range is still bound-checked, so a range past the end of the file is still refused as malformed.
  • The protection condition matches what Go's linker writes. It clears __DWARF's initial protection because dyld accepts the size mismatch only on an unprotected segment (the comment above seg.Prot = 0 in macho_combine_dwarf.go). A zero-vmsize segment that keeps a protection is still refused.
  • No file mapping matters because __DWARF shares its address with __LINKEDIT. If its file bytes were mapped at that address, the two would alias different file offsets.
  • The address-overflow check still applies. A __go_buildinfo section inside an unmapped segment still fails, because no mapping covers its address.

Not changed, and yours to decide:

  • src/native/AppleMachoSelection.ts builds segment address ranges from vmaddr + filesize and ignores vmsize, so it would also see __DWARF overlapping __LINKEDIT. It is a separate reader, and I did not find a path that runs it on Go binaries, so I left it alone.
  • scripts/verify-go-binary.mjs still strips its builds. An unstripped darwin target there would have caught this, but it adds a build to that script, so I left it for you.

Contract and boundary impact

  • Semantic owner and earliest changed stage: Mach-O segment admission in readGoMachoImage (src/go/GoMachoImage.ts).
  • CLI and MCP/tool-catalog contract: none. No schemas change.
  • Provider, bridge, target-format, or platform compatibility: Mach-O Go binaries with an unmapped __DWARF segment are now read. ELF and PE are unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: none.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata, package, or installation impact: none.

Evidence and regression coverage

  • Tests added in tests/boundary/go/goBinaryImage.test.ts:
    • Build info is read beside a vmsize 0 segment, for 32- and 64-bit, little- and big-endian Mach-O.
    • An unmapped segment placed at the address of the segment holding the build info does not alias it.
    • An unmapped segment whose file range runs past the end of the file is refused (added after the Codex review).
    • A zero-vmsize segment with a nonzero initial protection is refused (added after the second Codex review).
  • Base reproduction: on the parent commit the 5 read and alias cases fail with "Mach-O segment addresses or sizes are invalid". Each part of the fix is needed. Restoring the size check fails the four read cases. Mapping the unmapped segment's file bytes fails the alias case with "Virtual address has ambiguous file mappings". Skipping the bound check fails the past-the-end case. Dropping the protection condition fails the protected-segment case.
  • User-visible CLI output, with the fix, on the binary above: normalized_result.build_info.go_version is go1.27.1, and module_text is byte-identical to go version -m.

For evidence-bearing changes:

  • 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

  • vitest over the 8 test files that cover src/go and inspect-go-binary: 144 tests passed.
  • npm run check:fast: passed.
  • npm run verify:test-discovery: "Discovered all 914 test files exactly once."
  • Real binaries, built CLI, compared with go version -m on version and full module text. I used 48 binaries cross-compiled with Go 1.27.1 and Go 1.17.13. The 6 unstripped darwin binaries (amd64 and arm64, both Go versions) fail on main and match with the fix. Of the other 42, 40 match before and after: ELF and PE across 11 architectures, stripped darwin, PIE, external linking and GOPATH builds. The last 2 (AIX and Plan 9) are refused as unsupported targets before and after.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: Linux x64 (Fedora 44) only. The darwin binaries were cross-compiled, not run on macOS. No provider runs.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: parsing only. An unmapped segment's file range is validated but never mapped, so it cannot widen what the reader reads.

Review checklist

  • This PR addresses a concrete problem or an agreed enhancement.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • 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/2089 **Author:** [@JayOfTheKeyboard](https://github.com/JayOfTheKeyboard) **Created:** 10/12/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/go-macho-unmapped-dwarf` --- ### 📝 Commits (3) - [`7955a85`](https://github.com/morluto/rea/commit/7955a856e72e03a38c0c94c93fab66a4c05a9ea9) fix(go): read build info from unstripped Mach-O binaries - [`33eb746`](https://github.com/morluto/rea/commit/33eb7469f0ce7c8fe83bc9f06a591431aa08ee5f) fix(go): still bound-check an unmapped Mach-O segment's file range - [`bfbf99c`](https://github.com/morluto/rea/commit/bfbf99c7495c3171e5389b24a0ef6e2739fa6f8e) fix(go): exempt only unprotected zero-vmsize Mach-O segments ### 📊 Changes **2 files changed** (+94 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `src/go/GoMachoImage.ts` (+13 -4) 📝 `tests/boundary/go/goBinaryImage.test.ts` (+81 -2) </details> ### 📄 Description ## Summary `inspect-go-binary` refuses every Go binary built for macOS unless debug info was stripped with `-w`. A plain `GOOS=darwin go build` or `go install` gives `invalid_request`, "Mach-O segment addresses or sizes are invalid", where `go version -m` reads the same file. No issue is linked. I found this by comparing REA with `go version -m` on cross-compiled binaries, and the fix is small enough to explain here. ## Problem and expected behavior Go's linker writes DWARF into a `__DWARF` segment that has file bytes but `vmsize 0`, because the segment is never mapped. Both link modes do this: internal linking in `cmd/link/internal/ld/macho.go` (`ms.vsize = 0`), and external linking in `macho_combine_dwarf.go` (`Memsz = 0`). `readGoMachoImage` in `src/go/GoMachoImage.ts` refuses any segment whose `filesize` exceeds its `vmsize`, so it refuses the whole file. The segment table of a Go 1.27.1 `darwin/arm64` hello world: | Segment | vmaddr | vmsize | fileoff | filesize | | --- | --- | --- | --- | --- | | `__DATA` | `0x100114000` | `0x31710` | `0x114000` | `0x6f40` | | `__DWARF` | `0x100148000` | `0x0` | `0x11c000` | `0x85968` | | `__LINKEDIT` | `0x100148000` | `0x1b3b2` | `0x1a4000` | `0x1b3b2` | ```text $ GOOS=darwin GOARCH=arm64 go build -o hello . $ go version -m hello hello: go1.27.1 path example.com/hello $ rea inspect-go-binary hello --format json {"code":"invalid_request","message":"Analysis input is invalid. Check the arguments and try again.", ...} ``` Expected: the build info `go version -m` reports. The repository's own check, `scripts/verify-go-binary.mjs`, builds every target with `-ldflags=-s -w`, which drops this segment, so it never meets one. ## Change and scope - A segment with `vmsize 0` and initial protection 0 is treated as unmapped. It skips the `filesize > vmsize` check and gets no virtual mapping, the same as a segment with no file bytes. Its file range is still bound-checked, so a range past the end of the file is still refused as malformed. - The protection condition matches what Go's linker writes. It clears `__DWARF`'s initial protection because dyld accepts the size mismatch only on an unprotected segment (the comment above `seg.Prot = 0` in `macho_combine_dwarf.go`). A zero-`vmsize` segment that keeps a protection is still refused. - No file mapping matters because `__DWARF` shares its address with `__LINKEDIT`. If its file bytes were mapped at that address, the two would alias different file offsets. - The address-overflow check still applies. A `__go_buildinfo` section inside an unmapped segment still fails, because no mapping covers its address. Not changed, and yours to decide: - `src/native/AppleMachoSelection.ts` builds segment address ranges from `vmaddr + filesize` and ignores `vmsize`, so it would also see `__DWARF` overlapping `__LINKEDIT`. It is a separate reader, and I did not find a path that runs it on Go binaries, so I left it alone. - `scripts/verify-go-binary.mjs` still strips its builds. An unstripped `darwin` target there would have caught this, but it adds a build to that script, so I left it for you. ## Contract and boundary impact - Semantic owner and earliest changed stage: Mach-O segment admission in `readGoMachoImage` (`src/go/GoMachoImage.ts`). - CLI and MCP/tool-catalog contract: none. No schemas change. - Provider, bridge, target-format, or platform compatibility: Mach-O Go binaries with an unmapped `__DWARF` segment are now read. ELF and PE are unchanged. - Evidence, artifact, provenance, or reconstruction contract: none. - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata, package, or installation impact: none. ## Evidence and regression coverage - Tests added in `tests/boundary/go/goBinaryImage.test.ts`: - Build info is read beside a `vmsize 0` segment, for 32- and 64-bit, little- and big-endian Mach-O. - An unmapped segment placed at the address of the segment holding the build info does not alias it. - An unmapped segment whose file range runs past the end of the file is refused (added after the Codex review). - A zero-`vmsize` segment with a nonzero initial protection is refused (added after the second Codex review). - Base reproduction: on the parent commit the 5 read and alias cases fail with "Mach-O segment addresses or sizes are invalid". Each part of the fix is needed. Restoring the size check fails the four read cases. Mapping the unmapped segment's file bytes fails the alias case with "Virtual address has ambiguous file mappings". Skipping the bound check fails the past-the-end case. Dropping the protection condition fails the protected-segment case. - User-visible CLI output, with the fix, on the binary above: `normalized_result.build_info.go_version` is `go1.27.1`, and `module_text` is byte-identical to `go version -m`. For evidence-bearing changes: - [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 - `vitest` over the 8 test files that cover `src/go` and `inspect-go-binary`: 144 tests passed. - `npm run check:fast`: passed. - `npm run verify:test-discovery`: "Discovered all 914 test files exactly once." - Real binaries, built CLI, compared with `go version -m` on version and full module text. I used 48 binaries cross-compiled with Go 1.27.1 and Go 1.17.13. The 6 unstripped `darwin` binaries (amd64 and arm64, both Go versions) fail on `main` and match with the fix. Of the other 42, 40 match before and after: ELF and PE across 11 architectures, stripped `darwin`, PIE, external linking and GOPATH builds. The last 2 (AIX and Plan 9) are refused as unsupported targets before and after. ## Compatibility, safety, and release - Breaking changes or migration steps: none. - Real Hopper/Ghidra, browser, or OS coverage: Linux x64 (Fedora 44) only. The `darwin` binaries were cross-compiled, not run on macOS. No provider runs. - Package or release metadata impact: none. - Security, privacy, process, or containment review: parsing only. An unmapped segment's file range is validated but never mapped, so it cannot widen what the reader reads. ## Review checklist - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [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 00:58:07 +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#5016
No description provided.