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
5package 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
20import (
21	"cmd/internal/notsha256"
22	"debug/macho"
23	"io"
24	"os"
25	"unsafe"
26)
27
28// uuidFromGoBuildId hashes the Go build ID and returns a slice of 16
29// bytes suitable for use as the payload in a Macho LC_UUID load
30// command.
31func uuidFromGoBuildId(buildID string) []byte {
32	if buildID == "" {
33		return make([]byte, 16)
34	}
35	hashedBuildID := notsha256.Sum256([]byte(buildID))
36	rv := hashedBuildID[:16]
37
38	// RFC 4122 conformance (see RFC 4122 Sections 4.2.2, 4.1.3). We
39	// want the "version" of this UUID to appear as 'hashed' as opposed
40	// to random or time-based.  This is something of a fiction since
41	// we're not actually hashing using MD5 or SHA1, but it seems better
42	// to use this UUID flavor than any of the others. This is similar
43	// to how other linkers handle this (for example this code in lld:
44	// https://github.com/llvm/llvm-project/blob/2a3a79ce4c2149d7787d56f9841b66cacc9061d0/lld/MachO/Writer.cpp#L524).
45	rv[6] &= 0xcf
46	rv[6] |= 0x30
47	rv[8] &= 0x3f
48	rv[8] |= 0xc0
49
50	return rv
51}
52
53// machoRewriteUuid copies over the contents of the Macho executable
54// exef into the output file outexe, and in the process updates the
55// LC_UUID command to a new value recomputed from the Go build id.
56func machoRewriteUuid(ctxt *Link, exef *os.File, exem *macho.File, outexe string) error {
57	outf, err := os.OpenFile(outexe, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
58	if err != nil {
59		return err
60	}
61	defer outf.Close()
62
63	// Copy over the file.
64	if _, err := io.Copy(outf, exef); err != nil {
65		return err
66	}
67
68	// Locate the portion of the binary containing the load commands.
69	cmdOffset := unsafe.Sizeof(exem.FileHeader)
70	if is64bit := exem.Magic == macho.Magic64; is64bit {
71		// mach_header_64 has one extra uint32.
72		cmdOffset += unsafe.Sizeof(exem.Magic)
73	}
74	if _, err := outf.Seek(int64(cmdOffset), 0); err != nil {
75		return err
76	}
77
78	// Read the load commands, looking for the LC_UUID cmd. If/when we
79	// locate it, overwrite it with a new value produced by
80	// uuidFromGoBuildId.
81	reader := loadCmdReader{next: int64(cmdOffset),
82		f: outf, order: exem.ByteOrder}
83	for i := uint32(0); i < exem.Ncmd; i++ {
84		cmd, err := reader.Next()
85		if err != nil {
86			return err
87		}
88		if cmd.Cmd == LC_UUID {
89			var u uuidCmd
90			if err := reader.ReadAt(0, &u); err != nil {
91				return err
92			}
93			copy(u.Uuid[:], uuidFromGoBuildId(*flagBuildid))
94			if err := reader.WriteAt(0, &u); err != nil {
95				return err
96			}
97			break
98		}
99	}
100
101	// We're done
102	return nil
103}
104