cmd/link: add -macos and -macsdk flags to set LC_BUILD_VERSION

These are for debugging problems with the build versions
in the load commands. We still want to set them correctly by
default for most users, provided we can determine what that means.

Fixes #58722.

Change-Id: Iccab7044ac7b0c58e7e01258a5e374c4155528fc
Reviewed-on: https://go-review.googlesource.com/c/go/+/751260
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Russ Cox
2026-03-03 17:06:03 -05:00
parent 2433a3f2d6
commit 23fde5c48c
4 changed files with 86 additions and 48 deletions

View File

@@ -878,7 +878,7 @@ func addbuildinfo(ctxt *Link) {
}
if ctxt.IsDarwin() {
buildinfo = uuidFromGoBuildId(buildID)
buildinfo = uuidFromHash(hash.Sum32([]byte(buildID)))
return
}

View File

@@ -7,6 +7,7 @@ package ld
import (
"bytes"
"cmd/internal/codesign"
"cmd/internal/hash"
imacho "cmd/internal/macho"
"cmd/internal/objabi"
"cmd/internal/sys"
@@ -19,6 +20,7 @@ import (
"io"
"os"
"sort"
"strconv"
"strings"
"unsafe"
)
@@ -404,6 +406,50 @@ func machowrite(ctxt *Link, arch *sys.Arch, out *OutBuf, linkmode LinkMode) int
return int(out.Offset() - o1)
}
type macVersionFlag [3]byte
func (f *macVersionFlag) String() string {
return fmt.Sprintf("%d.%d.%d", f[0], f[1], f[2])
}
func (f *macVersionFlag) Set(s string) error {
var parsed macVersionFlag
nums := strings.Split(s, ".")
if len(nums) > 3 {
goto Error
}
for i, num := range nums {
n, err := strconv.Atoi(num)
if err != nil || n < 0 || n > 0xFF {
goto Error
}
parsed[i] = byte(n)
}
// success, now modify f
*f = parsed
return nil
Error:
return fmt.Errorf("invalid version %q", s)
}
func (f *macVersionFlag) version() uint32 {
return uint32(f[0])<<16 | uint32(f[1])<<8 | uint32(f[2])
}
var (
// On advice from Apple engineers, we keep macOS set to the
// oldest supported macOS version but keep macSDK to the newest
// tested OS/SDK version. If these defaults are not good enough,
// the -macos and -macsdk linker flags can override them.
// For past problems involving these values, see
// go.dev/issue/30488
// go.dev/issue/56784
// go.dev/issue/77917
macOS = macVersionFlag{12, 0, 0}
macSDK = macVersionFlag{26, 2, 0}
)
func (ctxt *Link) domacho() {
if *FlagD {
return
@@ -419,6 +465,10 @@ func (ctxt *Link) domacho() {
machoPlatform = load.platform
ml := newMachoLoad(ctxt.Arch, load.cmd.type_, uint32(len(load.cmd.data)))
copy(ml.data, load.cmd.data)
if machoPlatform == PLATFORM_MACOS {
ml.data[1] = macOS.version()
ml.data[2] = macSDK.version()
}
break
}
}
@@ -428,22 +478,11 @@ func (ctxt *Link) domacho() {
machoPlatform = PLATFORM_IOS
}
if ctxt.LinkMode == LinkInternal && machoPlatform == PLATFORM_MACOS {
var version uint32
switch ctxt.Arch.Family {
case sys.ARM64, sys.AMD64:
// This must be fairly recent for Apple signing (go.dev/issue/30488).
// Having too old a version here was also implicated in some problems
// calling into macOS libraries (go.dev/issue/56784).
// CL 460476 noted that in general this can be the most recent supported
// macOS version, but we haven't tested if going higher than Go's oldest
// supported macOS version could cause new problems.
version = 12<<16 | 0<<8 | 0<<0 // 12.0.0
}
ml := newMachoLoad(ctxt.Arch, imacho.LC_BUILD_VERSION, 4)
ml.data[0] = uint32(machoPlatform)
ml.data[1] = version // OS version
ml.data[2] = version // SDK version
ml.data[3] = 0 // ntools
ml.data[1] = macOS.version()
ml.data[2] = macSDK.version()
ml.data[3] = 0 // ntools
}
}
@@ -806,18 +845,24 @@ func asmbMacho(ctxt *Link) {
}
}
if ctxt.IsInternal() && len(buildinfo) > 0 {
if ctxt.IsInternal() && *flagHostBuildid != "none" {
ml := newMachoLoad(ctxt.Arch, imacho.LC_UUID, 4)
// Mach-O UUID is 16 bytes
if len(buildinfo) < 16 {
buildinfo = append(buildinfo, make([]byte, 16)...)
var uuid [16]byte
if len(buildinfo) >= 16 {
copy(uuid[:], buildinfo)
} else {
// Note: When setting macSDK to 26.2, dyld refuses to run any
// binary without an LC_UUID, which makes bootstrap fail.
// To work around that situation, if buildinfo is missing we
// construct a hash of the binary written so far and use that.
// Using -B none will bypass this if desired,
// but the resulting binary may not be runnable.
copy(uuid[:], uuidFromHash(hash.Sum32(ctxt.Out.Data())))
}
// By default, buildinfo is already in UUIDv3 format
// (see uuidFromGoBuildId).
ml.data[0] = ctxt.Arch.ByteOrder.Uint32(buildinfo)
ml.data[1] = ctxt.Arch.ByteOrder.Uint32(buildinfo[4:])
ml.data[2] = ctxt.Arch.ByteOrder.Uint32(buildinfo[8:])
ml.data[3] = ctxt.Arch.ByteOrder.Uint32(buildinfo[12:])
ml.data[0] = ctxt.Arch.ByteOrder.Uint32(uuid[0:])
ml.data[1] = ctxt.Arch.ByteOrder.Uint32(uuid[4:])
ml.data[2] = ctxt.Arch.ByteOrder.Uint32(uuid[8:])
ml.data[3] = ctxt.Arch.ByteOrder.Uint32(uuid[12:])
}
if ctxt.IsInternal() && ctxt.NeedCodeSign() {

View File

@@ -18,7 +18,6 @@ package ld
// final executable generated by the external linker.
import (
"cmd/internal/hash"
imacho "cmd/internal/macho"
"debug/macho"
@@ -26,27 +25,19 @@ import (
"os"
)
// uuidFromGoBuildId hashes the Go build ID and returns a slice of 16
// bytes suitable for use as the payload in a Macho LC_UUID load
// command.
func uuidFromGoBuildId(buildID string) []byte {
if buildID == "" {
return make([]byte, 16)
}
hashedBuildID := hash.Sum32([]byte(buildID))
rv := hashedBuildID[:16]
// uuidFromHash converts a hash to a UUID
// suitable for use in the Macho LC_UUID load command.
func uuidFromHash(hashed [32]byte) []byte {
rv := make([]byte, 16)
copy(rv, hashed[:16])
// RFC 4122 conformance (see RFC 4122 Sections 4.2.2, 4.1.3). We
// want the "version" of this UUID to appear as 'hashed' as opposed
// to random or time-based. This is something of a fiction since
// we're not actually hashing using MD5 or SHA1, but it seems better
// to use this UUID flavor than any of the others. This is similar
// to how other linkers handle this (for example this code in lld:
// https://github.com/llvm/llvm-project/blob/2a3a79ce4c2149d7787d56f9841b66cacc9061d0/lld/MachO/Writer.cpp#L524).
rv[6] &= 0x0f
rv[6] |= 0x30
rv[8] &= 0x3f
rv[8] |= 0xc0
// Mark the UUID as version 3, variant 1, which is an MD5-hash-based UUID.
// We are not actually using MD5, but we're also not using SHA1 (version 5),
// and LLVM also uses version 3 [1] so this seems good enough.
//
// [1] https://github.com/llvm/llvm-project/blob/2a3a79ce4c2149d7787d56f9841b66cacc9061d0/lld/MachO/Writer.cpp#L524
rv[6] = (rv[6] &^ 0xF0) | 0x30 // version 3
rv[8] = (rv[8] &^ 0xC0) | 0x80 // variant 1
return rv
}
@@ -74,7 +65,7 @@ func machoRewriteUuid(ctxt *Link, exef *os.File, exem *macho.File, outexe string
// Read the load commands, looking for the LC_UUID cmd. If/when we
// locate it, overwrite it with a new value produced by
// uuidFromGoBuildId.
// uuidFromHash.
reader := imacho.NewLoadCmdUpdater(outf, exem.ByteOrder, cmdOffset)
for i := uint32(0); i < exem.Ncmd; i++ {
cmd, err := reader.Next()

View File

@@ -55,9 +55,11 @@ var (
)
func init() {
flag.Var(&rpath, "r", "set the ELF dynamic linker search `path` to dir1:dir2:...")
flag.Var(&flagExtld, "extld", "use `linker` when linking in external mode")
flag.Var(&flagExtldflags, "extldflags", "pass `flags` to external linker")
flag.Var(&macOS, "macos", "mac OS version to write in build info (only used in internal linking)")
flag.Var(&macSDK, "macsdk", "mac SDK version to write in build info (only used in internal linking)")
flag.Var(&rpath, "r", "set the ELF dynamic linker search `path` to dir1:dir2:...")
flag.Var(&flagW, "w", "disable DWARF generation")
}