cmd/link: support Mach-O UNSIGNED relocations for dynamic imports on darwin

When internally linking darwin binaries, the linker rejected Mach-O
UNSIGNED (pointer) relocations targeting dynamic import symbols,
producing errors like:

    unexpected reloc for dynamic symbol _swift_FORCE_LOAD_$_swiftIOKit

These relocations are legitimate and appear in data sections (e.g.
__DATA/__const) of object files that reference external symbols such as
Swift force-load symbols. The dynamic linker (dyld) needs to bind these
pointers at load time.

Cq-Include-Trybots: luci.golang.try:gotip-darwin-arm64_15,gotip-darwin-amd64_14
Change-Id: I1cc759dec28b8aa076602a45062f403d0d9f45fe
Reviewed-on: https://go-review.googlesource.com/c/go/+/745220
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
This commit is contained in:
George Adams
2026-02-13 11:26:12 +00:00
committed by Gopher Robot
parent 76ebf63307
commit f4afab14d0
6 changed files with 73 additions and 18 deletions

View File

@@ -8,4 +8,5 @@ package cgotest
import "testing"
func TestIssue76023(t *testing.T) { issue76023(t) }
func TestIssue76023(t *testing.T) { issue76023(t) }
func TestGlobalDataDynimport(t *testing.T) { unsignedRelocDynimport(t) }

View File

@@ -0,0 +1,41 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin
package cgotest
/*
#include <stdio.h>
// Global function pointer to a dynamically-linked libc function.
// When compiled to a Mach-O object, this produces a RELOC_UNSIGNED
// relocation targeting the external symbol _puts.
int (*_cgo_test_dynref_puts)(const char *) = puts;
static int cgo_call_dynref(void) {
if (_cgo_test_dynref_puts == 0) {
return -1;
}
// Call the resolved function pointer. puts returns a non-negative
// value on success.
return _cgo_test_dynref_puts("cgo unsigned reloc test");
}
*/
import "C"
import "testing"
// unsignedRelocDynimport verifies that the Go internal linker correctly
// handles Mach-O UNSIGNED relocations targeting dynamic import symbols.
// The C preamble above contains a global function pointer initialized
// to puts, which produces a RELOC_UNSIGNED relocation to the external
// symbol _puts. If the linker can't handle this, the test binary
// won't link at all.
func unsignedRelocDynimport(t *testing.T) {
got := C.cgo_call_dynref()
if got < 0 {
t.Fatal("C function pointer to puts not resolved")
}
}

View File

@@ -164,9 +164,6 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
su := ldr.MakeSymbolUpdater(s)
su.SetRelocType(rIdx, objabi.R_ADDR)
if targType == sym.SDYNIMPORT {
ldr.Errorf(s, "unexpected reloc for dynamic symbol %s", ldr.SymName(targ))
}
if target.IsPIE() && target.IsInternal() {
// For internal linking PIE, this R_ADDR relocation cannot
// be resolved statically. We need to generate a dynamic
@@ -179,6 +176,9 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
ldr.Errorf(s, "unsupported relocation for PIE: %v", rt)
}
}
if targType == sym.SDYNIMPORT {
ldr.Errorf(s, "unexpected reloc for dynamic symbol %s", ldr.SymName(targ))
}
return true
case objabi.MachoRelocOffset + ld.MACHO_X86_64_RELOC_SUBTRACTOR*2 + 0:
@@ -419,7 +419,13 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
// Mach-O relocations are a royal pain to lay out.
// They use a compact stateful bytecode representation.
// Here we record what are needed and encode them later.
ld.MachoAddRebase(s, int64(r.Off()))
if targType == sym.SDYNIMPORT {
// Dynamic import: the pointer must be bound by
// the dynamic linker at load time.
ld.MachoAddBind(s, int64(r.Off()), targ)
} else {
ld.MachoAddRebase(s, int64(r.Off()))
}
// Not mark r done here. So we still apply it statically,
// so in the file content we'll also have the right offset
// to the relocation target. So it can be examined statically

View File

@@ -211,9 +211,6 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
// Handle relocations found in Mach-O object files.
case objabi.MachoRelocOffset + ld.MACHO_ARM64_RELOC_UNSIGNED*2:
if targType == sym.SDYNIMPORT {
ldr.Errorf(s, "unexpected reloc for dynamic symbol %s", ldr.SymName(targ))
}
su := ldr.MakeSymbolUpdater(s)
su.SetRelocType(rIdx, objabi.R_ADDR)
if target.IsPIE() && target.IsInternal() {
@@ -222,6 +219,9 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
// relocation. Let the code below handle it.
break
}
if targType == sym.SDYNIMPORT {
ldr.Errorf(s, "unexpected reloc for dynamic symbol %s", ldr.SymName(targ))
}
return true
case objabi.MachoRelocOffset + ld.MACHO_ARM64_RELOC_SUBTRACTOR*2:
@@ -469,7 +469,13 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade
// Mach-O relocations are a royal pain to lay out.
// They use a compact stateful bytecode representation.
// Here we record what are needed and encode them later.
ld.MachoAddRebase(s, int64(r.Off()))
if targType == sym.SDYNIMPORT {
// Dynamic import: the pointer must be bound by
// the dynamic linker at load time.
ld.MachoAddBind(s, int64(r.Off()), targ)
} else {
ld.MachoAddRebase(s, int64(r.Off()))
}
// Not mark r done here. So we still apply it statically,
// so in the file content we'll also have the right offset
// to the relocation target. So it can be examined statically

View File

@@ -3040,7 +3040,7 @@ func AddGotSym(target *Target, ldr *loader.Loader, syms *ArchSyms, s loader.Sym,
// Mach-O relocations are a royal pain to lay out.
// They use a compact stateful bytecode representation.
// Here we record what are needed and encode them later.
MachoAddBind(int64(ldr.SymGot(s)), s)
MachoAddBind(syms.GOT, int64(ldr.SymGot(s)), s)
}
} else {
ldr.Errorf(s, "addgotsym: unsupported binary format")

View File

@@ -1338,21 +1338,24 @@ func MachoAddRebase(s loader.Sym, off int64) {
machorebase = append(machorebase, machoRebaseRecord{s, off})
}
// A bind entry tells the dynamic linker the data at GOT+off should be bound
// A bind entry tells the dynamic linker the data at sym+off should be bound
// to the address of the target symbol, which is a dynamic import.
// sym is the symbol containing the pointer (e.g. the GOT or a data symbol),
// off is the offset within that symbol, and targ is the dynamic import target.
// For now, the only kind of entry we support is that the data is an absolute
// address, and the source symbol is always the GOT. That seems all we need.
// address. That seems all we need.
// In the binary it uses a compact stateful bytecode encoding. So we record
// entries as we go and build the table at the end.
type machoBindRecord struct {
sym loader.Sym
off int64
targ loader.Sym
}
var machobind []machoBindRecord
func MachoAddBind(off int64, targ loader.Sym) {
machobind = append(machobind, machoBindRecord{off, targ})
func MachoAddBind(sym loader.Sym, off int64, targ loader.Sym) {
machobind = append(machobind, machoBindRecord{sym, off, targ})
}
// Generate data for the dynamic linker, used in LC_DYLD_INFO_ONLY load command.
@@ -1412,12 +1415,10 @@ func machoDyldInfo(ctxt *Link) {
// Bind table.
// TODO: compact encoding, as above.
// TODO: lazy binding?
got := ctxt.GOT
seg := ldr.SymSect(got).Seg
gotAddr := ldr.SymValue(got)
bind.AddUint8(BIND_OPCODE_SET_TYPE_IMM | BIND_TYPE_POINTER)
for _, r := range machobind {
off := uint64(gotAddr+r.off) - seg.Vaddr
seg := ldr.SymSect(r.sym).Seg
off := uint64(ldr.SymValue(r.sym)+r.off) - seg.Vaddr
bind.AddUint8(BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB | segId(seg))
bind.AddUleb(off)