1 package builder
2 3 import (
4 "bytes"
5 "debug/elf"
6 "debug/macho"
7 "encoding/binary"
8 "fmt"
9 "io"
10 "os"
11 "runtime"
12 )
13 14 // ReadBuildID reads the build ID from the currently running executable.
15 func ReadBuildID() ([]byte, error) {
16 executable, err := os.Executable()
17 if err != nil {
18 return nil, err
19 }
20 f, err := os.Open(executable)
21 if err != nil {
22 return nil, err
23 }
24 defer f.Close()
25 26 switch runtime.GOOS {
27 case "linux", "freebsd", "android":
28 // Read the GNU build id section. (Not sure about FreeBSD though...)
29 file, err := elf.NewFile(f)
30 if err != nil {
31 return nil, err
32 }
33 var gnuID, goID []byte
34 for _, section := range file.Sections {
35 if section.Type != elf.SHT_NOTE ||
36 (section.Name != ".note.gnu.build-id" && section.Name != ".note.go.buildid") {
37 continue
38 }
39 buf := make([]byte, section.Size)
40 n, err := section.ReadAt(buf, 0)
41 if uint64(n) != section.Size || err != nil {
42 return nil, fmt.Errorf("could not read build id: %w", err)
43 }
44 if section.Name == ".note.gnu.build-id" {
45 gnuID = buf
46 } else {
47 goID = buf
48 }
49 }
50 if gnuID != nil {
51 return gnuID, nil
52 } else if goID != nil {
53 return goID, nil
54 }
55 case "darwin":
56 // Read the LC_UUID load command, which contains the equivalent of a
57 // build ID.
58 file, err := macho.NewFile(f)
59 if err != nil {
60 return nil, err
61 }
62 for _, load := range file.Loads {
63 // Unfortunately, the debug/macho package doesn't support the
64 // LC_UUID command directly. So we have to read it from
65 // macho.LoadBytes.
66 load, ok := load.(macho.LoadBytes)
67 if !ok {
68 continue
69 }
70 raw := load.Raw()
71 command := binary.LittleEndian.Uint32(raw)
72 if command != 0x1b {
73 // Looking for the LC_UUID load command.
74 // LC_UUID is defined here as 0x1b:
75 // https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
76 continue
77 }
78 return raw[4:], nil
79 }
80 81 // Normally we would have found a build ID by now. But not on Nix,
82 // unfortunately, because Nix adds -no_uuid for some reason:
83 // https://github.com/NixOS/nixpkgs/issues/178366
84 // Fall back to the same implementation that we use for Windows.
85 id, err := readRawGoBuildID(f, 32*1024)
86 if len(id) != 0 || err != nil {
87 return id, err
88 }
89 default:
90 // On other platforms (such as Windows) there isn't such a convenient
91 // build ID. Luckily, Go does have an equivalent of the build ID, which
92 // is stored as a special symbol named go.buildid. You can read it
93 // using `go tool buildid`, but the code below extracts it directly
94 // from the binary.
95 // Unfortunately, because of stripping with the -w flag, no symbol
96 // table might be available. Therefore, we have to scan the binary
97 // directly. Luckily the build ID is always at the start of the file.
98 // For details, see:
99 // https://github.com/golang/go/blob/master/src/cmd/internal/buildid/buildid.go
100 id, err := readRawGoBuildID(f, 4096)
101 if len(id) != 0 || err != nil {
102 return id, err
103 }
104 }
105 return nil, fmt.Errorf("could not find build ID in %v", executable)
106 }
107 108 // The Go toolchain stores a build ID in the binary that we can use, as a
109 // fallback if binary file specific build IDs can't be obtained.
110 // This function reads that build ID from the binary.
111 func readRawGoBuildID(f *os.File, prefixSize int) ([]byte, error) {
112 fileStart := make([]byte, prefixSize)
113 _, err := io.ReadFull(f, fileStart)
114 if err != nil {
115 return nil, fmt.Errorf("could not read build id from %s: %v", f.Name(), err)
116 }
117 index := bytes.Index(fileStart, []byte("\xff Go build ID: \""))
118 if index < 0 || index > len(fileStart)-103 {
119 return nil, fmt.Errorf("could not find build id in %s", f.Name())
120 }
121 buf := fileStart[index : index+103]
122 if bytes.HasPrefix(buf, []byte("\xff Go build ID: \"")) && bytes.HasSuffix(buf, []byte("\"\n \xff")) {
123 return buf[len("\xff Go build ID: \"") : len(buf)-1], nil
124 }
125 126 return nil, nil
127 }
128