Skip to content

Commit e3fd4ba

Browse files
cherrymuigopherbot
authored andcommitted
[release-branch.go1.22] cmd/link: generate Mach-O UUID when -B flag is specified
Currently, on Mach-O, the Go linker doesn't generate LC_UUID in internal linking mode. This causes some macOS system tools unable to track the binary, as well as in some cases the binary unable to access local network on macOS 15. This CL makes the linker start generate LC_UUID. Currently, the UUID is generated if the -B flag is specified. And we'll make it generate UUID by default in a later CL. The -B flag is currently for generating GNU build ID on ELF, which is a similar concept to Mach-O's UUID. Instead of introducing another flag, we just use the same flag and the same setting. Specifically, "-B gobuildid" will generate a UUID based on the Go build ID. Updates #68678. Fixes #69991. Cq-Include-Trybots: luci.golang.try:go1.22-darwin-amd64_14,go1.22-darwin-arm64_13 Change-Id: I90089a78ba144110bf06c1c6836daf2d737ff10a Reviewed-on: https://go-review.googlesource.com/c/go/+/618595 Reviewed-by: Michael Knyszek <mknyszek@google.com> Reviewed-by: Ingo Oeser <nightlyone@googlemail.com> Reviewed-by: Than McIntosh <thanm@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> (cherry picked from commit 20ed603) Reviewed-on: https://go-review.googlesource.com/c/go/+/622596 Auto-Submit: Michael Pratt <mpratt@google.com> Reviewed-by: Michael Pratt <mpratt@google.com> TryBot-Bypass: Michael Pratt <mpratt@google.com>
1 parent 29252e4 commit e3fd4ba

File tree

5 files changed

+93
-11
lines changed

5 files changed

+93
-11
lines changed

‎src/cmd/link/internal/ld/elf.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -805,13 +805,19 @@ func elfwritefreebsdsig(out *OutBuf) int {
805805
return int(sh.Size)
806806
}
807807

808-
func addbuildinfo(val string) {
808+
func addbuildinfo(ctxt *Link) {
809+
val := *flagHostBuildid
809810
if val == "gobuildid" {
810811
buildID := *flagBuildid
811812
if buildID == "" {
812813
Exitf("-B gobuildid requires a Go build ID supplied via -buildid")
813814
}
814815

816+
if ctxt.IsDarwin() {
817+
buildinfo = uuidFromGoBuildId(buildID)
818+
return
819+
}
820+
815821
hashedBuildID := notsha256.Sum256([]byte(buildID))
816822
buildinfo = hashedBuildID[:20]
817823

@@ -821,11 +827,13 @@ func addbuildinfo(val string) {
821827
if !strings.HasPrefix(val, "0x") {
822828
Exitf("-B argument must start with 0x: %s", val)
823829
}
824-
825830
ov := val
826831
val = val[2:]
827832

828-
const maxLen = 32
833+
maxLen := 32
834+
if ctxt.IsDarwin() {
835+
maxLen = 16
836+
}
829837
if hex.DecodedLen(len(val)) > maxLen {
830838
Exitf("-B option too long (max %d digits): %s", maxLen, ov)
831839
}

‎src/cmd/link/internal/ld/macho.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ func getMachoHdr() *MachoHdr {
296296
return &machohdr
297297
}
298298

299+
// Create a new Mach-O load command. ndata is the number of 32-bit words for
300+
// the data (not including the load command header).
299301
func newMachoLoad(arch *sys.Arch, type_ uint32, ndata uint32) *MachoLoad {
300302
if arch.PtrSize == 8 && (ndata&1 != 0) {
301303
ndata++
@@ -850,6 +852,20 @@ func asmbMacho(ctxt *Link) {
850852
}
851853
}
852854

855+
if ctxt.IsInternal() && len(buildinfo) > 0 {
856+
ml := newMachoLoad(ctxt.Arch, LC_UUID, 4)
857+
// Mach-O UUID is 16 bytes
858+
if len(buildinfo) < 16 {
859+
buildinfo = append(buildinfo, make([]byte, 16)...)
860+
}
861+
// By default, buildinfo is already in UUIDv3 format
862+
// (see uuidFromGoBuildId).
863+
ml.data[0] = ctxt.Arch.ByteOrder.Uint32(buildinfo)
864+
ml.data[1] = ctxt.Arch.ByteOrder.Uint32(buildinfo[4:])
865+
ml.data[2] = ctxt.Arch.ByteOrder.Uint32(buildinfo[8:])
866+
ml.data[3] = ctxt.Arch.ByteOrder.Uint32(buildinfo[12:])
867+
}
868+
853869
if ctxt.IsInternal() && ctxt.NeedCodeSign() {
854870
ml := newMachoLoad(ctxt.Arch, LC_CODE_SIGNATURE, 2)
855871
ml.data[0] = uint32(codesigOff)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package ld
6+
7+
// This file provides helper functions for updating/rewriting the UUID
8+
// load command within a Go go binary generated on Darwin using
9+
// external linking. Why is it necessary to update the UUID load
10+
// command? See issue #64947 for more detail, but the short answer is
11+
// that newer versions of the Macos toolchain (the newer linker in
12+
// particular) appear to compute the UUID based not just on the
13+
// content of the object files being linked but also on things like
14+
// the timestamps/paths of the objects; this makes it
15+
// difficult/impossible to support reproducible builds. Since we try
16+
// hard to maintain build reproducibility for Go, the APIs here
17+
// compute a new UUID (based on the Go build ID) and write it to the
18+
// final executable generated by the external linker.
19+
20+
import (
21+
"cmd/internal/notsha256"
22+
)
23+
24+
// uuidFromGoBuildId hashes the Go build ID and returns a slice of 16
25+
// bytes suitable for use as the payload in a Macho LC_UUID load
26+
// command.
27+
func uuidFromGoBuildId(buildID string) []byte {
28+
if buildID == "" {
29+
return make([]byte, 16)
30+
}
31+
hashedBuildID := notsha256.Sum256([]byte(buildID))
32+
rv := hashedBuildID[:16]
33+
34+
// RFC 4122 conformance (see RFC 4122 Sections 4.2.2, 4.1.3). We
35+
// want the "version" of this UUID to appear as 'hashed' as opposed
36+
// to random or time-based. This is something of a fiction since
37+
// we're not actually hashing using MD5 or SHA1, but it seems better
38+
// to use this UUID flavor than any of the others. This is similar
39+
// to how other linkers handle this (for example this code in lld:
40+
// https://github.com/llvm/llvm-project/blob/2a3a79ce4c2149d7787d56f9841b66cacc9061d0/lld/MachO/Writer.cpp#L524).
41+
rv[6] &= 0x0f
42+
rv[6] |= 0x30
43+
rv[8] &= 0x3f
44+
rv[8] |= 0xc0
45+
46+
return rv
47+
}

‎src/cmd/link/internal/ld/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ var (
9393
flagN = flag.Bool("n", false, "no-op (deprecated)")
9494
FlagS = flag.Bool("s", false, "disable symbol table")
9595
flag8 bool // use 64-bit addresses in symbol table
96+
flagHostBuildid = flag.String("B", "", "set ELF NT_GNU_BUILD_ID `note` or Mach-O UUID; use \"gobuildid\" to generate it from the Go build ID")
9697
flagInterpreter = flag.String("I", "", "use `linker` as ELF dynamic linker")
9798
FlagDebugTramp = flag.Int("debugtramp", 0, "debug trampolines")
9899
FlagDebugTextSize = flag.Int("debugtextsize", 0, "debug text section max size")
@@ -190,7 +191,6 @@ func Main(arch *sys.Arch, theArch Arch) {
190191
flag.Var(&ctxt.LinkMode, "linkmode", "set link `mode`")
191192
flag.Var(&ctxt.BuildMode, "buildmode", "set build `mode`")
192193
flag.BoolVar(&ctxt.compressDWARF, "compressdwarf", true, "compress DWARF if possible")
193-
objabi.Flagfn1("B", "add an ELF NT_GNU_BUILD_ID `note` when using ELF; use \"gobuildid\" to generate it from the Go build ID", addbuildinfo)
194194
objabi.Flagfn1("L", "add specified `directory` to library path", func(a string) { Lflag(ctxt, a) })
195195
objabi.AddVersionFlag() // -V
196196
objabi.Flagfn1("X", "add string value `definition` of the form importpath.name=value", func(s string) { addstrdata1(ctxt, s) })
@@ -287,6 +287,10 @@ func Main(arch *sys.Arch, theArch Arch) {
287287
*flagBuildid = "go-openbsd"
288288
}
289289

290+
if *flagHostBuildid != "" {
291+
addbuildinfo(ctxt)
292+
}
293+
290294
// enable benchmarking
291295
var bench *benchmark.Metrics
292296
if len(*benchmarkFlag) != 0 {

‎test/fixedbugs/issue14636.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,29 @@ import (
1212
"bytes"
1313
"log"
1414
"os/exec"
15+
"runtime"
1516
"strings"
1617
)
1718

1819
func main() {
19-
checkLinkOutput("", "-B argument must start with 0x")
20+
// The cannot open file error indicates that the parsing of -B flag
21+
// succeeded and it failed at a later step.
2022
checkLinkOutput("0", "-B argument must start with 0x")
21-
checkLinkOutput("0x", "usage")
23+
checkLinkOutput("0x", "cannot open file nonexistent.o")
2224
checkLinkOutput("0x0", "-B argument must have even number of digits")
23-
checkLinkOutput("0x00", "usage")
25+
checkLinkOutput("0x00", "cannot open file nonexistent.o")
2426
checkLinkOutput("0xYZ", "-B argument contains invalid hex digit")
25-
checkLinkOutput("0x"+strings.Repeat("00", 32), "usage")
26-
checkLinkOutput("0x"+strings.Repeat("00", 33), "-B option too long (max 32 digits)")
27+
28+
maxLen := 32
29+
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
30+
maxLen = 16
31+
}
32+
checkLinkOutput("0x"+strings.Repeat("00", maxLen), "cannot open file nonexistent.o")
33+
checkLinkOutput("0x"+strings.Repeat("00", maxLen+1), "-B option too long")
2734
}
2835

2936
func checkLinkOutput(buildid string, message string) {
30-
cmd := exec.Command("go", "tool", "link", "-B", buildid)
37+
cmd := exec.Command("go", "tool", "link", "-B", buildid, "nonexistent.o")
3138
out, err := cmd.CombinedOutput()
3239
if err == nil {
3340
log.Fatalf("expected cmd/link to fail")
@@ -39,6 +46,6 @@ func checkLinkOutput(buildid string, message string) {
3946
}
4047

4148
if !strings.Contains(firstLine, message) {
42-
log.Fatalf("cmd/link output did not include expected message %q: %s", message, firstLine)
49+
log.Fatalf("%s: cmd/link output did not include expected message %q: %s", buildid, message, firstLine)
4350
}
4451
}

0 commit comments

Comments
 (0)