// fields.
var buildInfoMagic = []byte("\xff Go buildinf:")
+const (
+ buildInfoAlign = 16
+ buildInfoHeaderSize = 32
+)
+
// ReadFile returns build information embedded in a Go binary
// file at the given path. Most information is only available for binaries built
// with module support.
if dataSize == 0 {
return "", "", errNotGoExe
}
- data, err := x.ReadData(dataAddr, dataSize)
+
+ addr, err := searchMagic(x, dataAddr, dataSize)
+ if err != nil {
+ return "", "", err
+ }
+
+ // Read in the full header first.
+ header, err := x.ReadData(addr, buildInfoHeaderSize)
if err != nil {
return "", "", err
}
- const (
- buildInfoAlign = 16
- buildInfoHeaderSize = 32
+ const (
ptrSizeOffset = 14
flagsOffset = 15
versPtrOffset = 16
flagsVersionPtr = 0x0
flagsVersionInl = 0x2
)
- for {
- i := bytes.Index(data, buildInfoMagic)
- if i < 0 || len(data)-i < buildInfoHeaderSize {
- return "", "", errNotGoExe
- }
- if i%buildInfoAlign == 0 && len(data)-i >= buildInfoHeaderSize {
- data = data[i:]
- break
- }
- data = data[(i+buildInfoAlign-1)&^(buildInfoAlign-1):]
- }
// Decode the blob. The blob is a 32-byte header, optionally followed
// by 2 varint-prefixed string contents.
// the header is followed by the string contents inline as
// length-prefixed (as varint) string contents. First is the version
// string, followed immediately by the modinfo string.
- flags := data[flagsOffset]
+ flags := header[flagsOffset]
if flags&flagsVersionMask == flagsVersionInl {
- vers, data = decodeString(data[buildInfoHeaderSize:])
- mod, data = decodeString(data)
+ vers, addr, err = decodeString(x, addr+buildInfoHeaderSize)
+ if err != nil {
+ return "", "", err
+ }
+ mod, _, err = decodeString(x, addr)
+ if err != nil {
+ return "", "", err
+ }
} else {
// flagsVersionPtr (<1.18)
- ptrSize := int(data[ptrSizeOffset])
+ ptrSize := int(header[ptrSizeOffset])
bigEndian := flags&flagsEndianMask == flagsEndianBig
var bo binary.ByteOrder
if bigEndian {
} else {
return "", "", errNotGoExe
}
- vers = readString(x, ptrSize, readPtr, readPtr(data[versPtrOffset:]))
- mod = readString(x, ptrSize, readPtr, readPtr(data[versPtrOffset+ptrSize:]))
+ vers = readString(x, ptrSize, readPtr, readPtr(header[versPtrOffset:]))
+ mod = readString(x, ptrSize, readPtr, readPtr(header[versPtrOffset+ptrSize:]))
}
if vers == "" {
return "", "", errNotGoExe
return false
}
-func decodeString(data []byte) (s string, rest []byte) {
- u, n := binary.Uvarint(data)
- if n <= 0 || u > uint64(len(data)-n) {
- return "", nil
+func decodeString(x exe, addr uint64) (string, uint64, error) {
+ // varint length followed by length bytes of data.
+
+ // N.B. ReadData reads _up to_ size bytes from the section containing
+ // addr. So we don't need to check that size doesn't overflow the
+ // section.
+ b, err := x.ReadData(addr, binary.MaxVarintLen64)
+ if err != nil {
+ return "", 0, err
+ }
+
+ length, n := binary.Uvarint(b)
+ if n <= 0 {
+ return "", 0, errNotGoExe
+ }
+ addr += uint64(n)
+
+ b, err = x.ReadData(addr, length)
+ if err != nil {
+ return "", 0, err
+ }
+ if uint64(len(b)) < length {
+ // Section ended before we could read the full string.
+ return "", 0, errNotGoExe
}
- return string(data[n : uint64(n)+u]), data[uint64(n)+u:]
+
+ return string(b), addr + length, nil
}
// readString returns the string at address addr in the executable x.
return string(data)
}
+const searchChunkSize = 1 << 20 // 1 MB
+
+// searchMagic returns the aligned first instance of buildInfoMagic in the data
+// range [addr, addr+size). Returns false if not found.
+func searchMagic(x exe, start, size uint64) (uint64, error) {
+ end := start + size
+ if end < start {
+ // Overflow.
+ return 0, errUnrecognizedFormat
+ }
+
+ // Round up start; magic can't occur in the initial unaligned portion.
+ start = (start + buildInfoAlign - 1) &^ (buildInfoAlign - 1)
+ if start >= end {
+ return 0, errNotGoExe
+ }
+
+ for start < end {
+ // Read in chunks to avoid consuming too much memory if data is large.
+ //
+ // Normally it would be somewhat painful to handle the magic crossing a
+ // chunk boundary, but since it must be 16-byte aligned we know it will
+ // fall within a single chunk.
+ remaining := end - start
+ chunkSize := uint64(searchChunkSize)
+ if chunkSize > remaining {
+ chunkSize = remaining
+ }
+
+ data, err := x.ReadData(start, chunkSize)
+ if err != nil {
+ return 0, err
+ }
+
+ for len(data) > 0 {
+ i := bytes.Index(data, buildInfoMagic)
+ if i < 0 {
+ break
+ }
+ if remaining-uint64(i) < buildInfoHeaderSize {
+ // Found magic, but not enough space left for the full header.
+ return 0, errNotGoExe
+ }
+ if i%buildInfoAlign != 0 {
+ // Found magic, but misaligned. Keep searching.
+ data = data[(i+buildInfoAlign-1)&^(buildInfoAlign-1):]
+ continue
+ }
+ // Good match!
+ return start + uint64(i), nil
+ }
+
+ start += chunkSize
+ }
+
+ return 0, errNotGoExe
+}
+
// elfExe is the ELF implementation of the exe interface.
type elfExe struct {
f *elf.File
--- /dev/null
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package buildinfo
+
+import (
+ "fmt"
+ "testing"
+)
+
+type byteExe struct {
+ b []byte
+}
+
+func (x *byteExe) ReadData(addr, size uint64) ([]byte, error) {
+ end := addr + size
+ if end < addr {
+ return nil, fmt.Errorf("ReadData(%d, %d) overflow", addr, size)
+ }
+ if addr >= uint64(len(x.b)) || end-1 >= uint64(len(x.b)) {
+ return nil, fmt.Errorf("ReadData(%d, %d) out of bounds of %d-byte slice", addr, size, len(x.b))
+ }
+ return x.b[addr:end], nil
+}
+
+func (x *byteExe) DataStart() (uint64, uint64) {
+ return 0, uint64(len(x.b))
+}
+
+func TestSearchMagic(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ want uint64
+ wantErr error
+ }{
+ {
+ name: "beginning",
+ data: func() []byte {
+ b := make([]byte, buildInfoHeaderSize)
+ copy(b, buildInfoMagic)
+ return b
+ }(),
+ want: 0,
+ },
+ {
+ name: "offset",
+ data: func() []byte {
+ b := make([]byte, 512)
+ copy(b[4*buildInfoAlign:], buildInfoMagic)
+ return b
+ }(),
+ want: 4 * buildInfoAlign,
+ },
+ {
+ name: "second_chunk",
+ data: func() []byte {
+ b := make([]byte, 4*searchChunkSize)
+ copy(b[searchChunkSize+4*buildInfoAlign:], buildInfoMagic)
+ return b
+ }(),
+ want: searchChunkSize + 4*buildInfoAlign,
+ },
+ {
+ name: "second_chunk_short",
+ data: func() []byte {
+ // Magic is 64-bytes into the second chunk,
+ // which is short; only exactly long enough to
+ // hold the header.
+ b := make([]byte, searchChunkSize+4*buildInfoAlign+buildInfoHeaderSize)
+ copy(b[searchChunkSize+4*buildInfoAlign:], buildInfoMagic)
+ return b
+ }(),
+ want: searchChunkSize + 4*buildInfoAlign,
+ },
+ {
+ name: "missing",
+ data: func() []byte {
+ b := make([]byte, buildInfoHeaderSize)
+ return b
+ }(),
+ wantErr: errNotGoExe,
+ },
+ {
+ name: "too_short",
+ data: func() []byte {
+ // There needs to be space for the entire
+ // header, not just the magic.
+ b := make([]byte, len(buildInfoMagic))
+ copy(b, buildInfoMagic)
+ return b
+ }(),
+ wantErr: errNotGoExe,
+ },
+ {
+ name: "misaligned",
+ data: func() []byte {
+ b := make([]byte, 512)
+ copy(b[7:], buildInfoMagic)
+ return b
+ }(),
+ wantErr: errNotGoExe,
+ },
+ {
+ name: "misaligned_across_chunk",
+ data: func() []byte {
+ // Magic crosses chunk boundary. By definition,
+ // it has to be misaligned.
+ b := make([]byte, 2*searchChunkSize)
+ copy(b[searchChunkSize-8:], buildInfoMagic)
+ return b
+ }(),
+ wantErr: errNotGoExe,
+ },
+ {
+ name: "header_across_chunk",
+ data: func() []byte {
+ // The magic is aligned within the first chunk,
+ // but the rest of the 32-byte header crosses
+ // the chunk boundary.
+ b := make([]byte, 2*searchChunkSize)
+ copy(b[searchChunkSize-buildInfoAlign:], buildInfoMagic)
+ return b
+ }(),
+ want: searchChunkSize - buildInfoAlign,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ x := &byteExe{tc.data}
+ dataAddr, dataSize := x.DataStart()
+ addr, err := searchMagic(x, dataAddr, dataSize)
+ if tc.wantErr == nil {
+ if err != nil {
+ t.Errorf("searchMagic got err %v want nil", err)
+ }
+ if addr != tc.want {
+ t.Errorf("searchMagic got addr %d want %d", addr, tc.want)
+ }
+ } else {
+ if err != tc.wantErr {
+ t.Errorf("searchMagic got err %v want %v", err, tc.wantErr)
+ }
+ }
+ })
+ }
+}