buildid.go raw

   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