github.com/grailbio/base@v0.0.11/fatbin/fatbin.go (about)

     1  // Copyright 2019 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package fatbin implements a simple fat binary format, and provides
     6  // facilities for creating fat binaries and accessing its variants.
     7  //
     8  // A fatbin binary is a base binary with a zip archive appended,
     9  // containing copies of the same binary targeted to different
    10  // GOOS/GOARCH combinations. The zip archive contains one entry for
    11  // each supported architecture and operating system combination.
    12  // At the end of a fatbin image is a footer, storing the offset of the
    13  // zip archive as well as a magic constant used to identify fatbin
    14  // images:
    15  //
    16  //	[8]offset[4]magic[8]checksum
    17  //
    18  // The checksum is a 64-bit xxhash checksum of the offset and
    19  // magic fields. The magic value is 0x5758ba2c.
    20  package fatbin
    21  
    22  import (
    23  	"archive/zip"
    24  	"debug/elf"
    25  	"debug/macho"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"io/ioutil"
    30  	"os"
    31  	"runtime"
    32  	"strings"
    33  	"sync"
    34  
    35  	"github.com/grailbio/base/log"
    36  )
    37  
    38  var (
    39  	selfOnce sync.Once
    40  	self     *Reader
    41  	selfErr  error
    42  )
    43  
    44  var (
    45  	// ErrNoSuchImage is returned when the fatbin does not contain an
    46  	// image for the requested GOOS/GOARCH combination.
    47  	ErrNoSuchImage = errors.New("image does not exist")
    48  	// ErrCorruptedImage is returned when the fatbin image has been
    49  	// corrupted.
    50  	ErrCorruptedImage = errors.New("corrupted fatbin image")
    51  )
    52  
    53  // Info provides information for an embedded binary.
    54  type Info struct {
    55  	Goos, Goarch string
    56  	Size         int64
    57  }
    58  
    59  func (info Info) String() string {
    60  	return fmt.Sprintf("%s/%s: %d", info.Goos, info.Goarch, info.Size)
    61  }
    62  
    63  // Reader reads images from a fatbin.
    64  type Reader struct {
    65  	self         io.ReaderAt
    66  	goos, goarch string
    67  	offset       int64
    68  
    69  	z *zip.Reader
    70  }
    71  
    72  // Self reads the currently executing binary image as a fatbin and
    73  // returns a reader to it.
    74  func Self() (*Reader, error) {
    75  	selfOnce.Do(func() {
    76  		filename, err := os.Executable()
    77  		if err != nil {
    78  			selfErr = err
    79  			return
    80  		}
    81  		f, err := os.Open(filename)
    82  		if err != nil {
    83  			selfErr = err
    84  			return
    85  		}
    86  		info, err := f.Stat()
    87  		if err != nil {
    88  			selfErr = err
    89  			return
    90  		}
    91  		_, _, offset, err := Sniff(f, info.Size())
    92  		if err != nil {
    93  			selfErr = err
    94  			return
    95  		}
    96  		self, selfErr = NewReader(f, offset, info.Size(), runtime.GOOS, runtime.GOARCH)
    97  	})
    98  	return self, selfErr
    99  }
   100  
   101  // OpenFile parses the provided ReaderAt with the provided size. The
   102  // file's contents is parsed to determine the offset of the fatbin's
   103  // archive. OpenFile returns an error if the file is not a fatbin.
   104  func OpenFile(r io.ReaderAt, size int64) (*Reader, error) {
   105  	goos, goarch, offset, err := Sniff(r, size)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	return NewReader(r, offset, size, goos, goarch)
   110  }
   111  
   112  // NewReader returns a new fatbin reader from the provided reader.
   113  // The offset should be the offset of the fatbin archive; size is the
   114  // total file size. The provided goos and goarch are that of the base
   115  // binary.
   116  func NewReader(r io.ReaderAt, offset, size int64, goos, goarch string) (*Reader, error) {
   117  	rd := &Reader{
   118  		self:   io.NewSectionReader(r, 0, offset),
   119  		goos:   goos,
   120  		goarch: goarch,
   121  		offset: offset,
   122  	}
   123  	if offset == size {
   124  		return rd, nil
   125  	}
   126  	var err error
   127  	rd.z, err = zip.NewReader(io.NewSectionReader(r, offset, size-offset), size-offset)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return rd, nil
   132  }
   133  
   134  // GOOS returns the base binary GOOS.
   135  func (r *Reader) GOOS() string { return r.goos }
   136  
   137  // GOARCH returns the base binary GOARCH.
   138  func (r *Reader) GOARCH() string { return r.goarch }
   139  
   140  // List returns information about embedded binary images.
   141  func (r *Reader) List() []Info {
   142  	infos := make([]Info, len(r.z.File))
   143  	for i, f := range r.z.File {
   144  		elems := strings.SplitN(f.Name, "/", 2)
   145  		if len(elems) != 2 {
   146  			log.Error.Printf("invalid fatbin: found name %s", f.Name)
   147  			continue
   148  		}
   149  		infos[i] = Info{
   150  			Goos:   elems[0],
   151  			Goarch: elems[1],
   152  			Size:   int64(f.UncompressedSize64),
   153  		}
   154  	}
   155  	return infos
   156  }
   157  
   158  // Open returns a ReadCloser from which the binary with the provided
   159  // goos and goarch can be read. Open returns ErrNoSuchImage if the
   160  // fatbin does not contain an image for the requested goos and
   161  // goarch.
   162  func (r *Reader) Open(goos, goarch string) (io.ReadCloser, error) {
   163  	if goos == r.goos && goarch == r.goarch {
   164  		sr := io.NewSectionReader(r.self, 0, 1<<63-1)
   165  		return ioutil.NopCloser(sr), nil
   166  	}
   167  
   168  	if r.z == nil {
   169  		return nil, ErrNoSuchImage
   170  	}
   171  
   172  	look := goos + "/" + goarch
   173  	for _, f := range r.z.File {
   174  		if f.Name == look {
   175  			return f.Open()
   176  		}
   177  	}
   178  	return nil, ErrNoSuchImage
   179  }
   180  
   181  // Stat returns the information for the image identified by the
   182  // provided GOOS and GOARCH. It returns a boolean indicating
   183  // whether the requested image was found.
   184  func (r *Reader) Stat(goos, goarch string) (info Info, ok bool) {
   185  	info.Goos = goos
   186  	info.Goarch = goarch
   187  	if goos == r.goos && goarch == r.goarch {
   188  		info.Size = r.offset
   189  		ok = true
   190  		return
   191  	}
   192  	look := goos + "/" + goarch
   193  	for _, f := range r.z.File {
   194  		if f.Name == look {
   195  			info.Size = int64(f.UncompressedSize64)
   196  			ok = true
   197  			return
   198  		}
   199  	}
   200  	return
   201  }
   202  
   203  func sectionEndAligned(s *elf.Section) int64 {
   204  	return int64(((s.Offset + s.FileSize) + (s.Addralign - 1)) & -s.Addralign)
   205  }
   206  
   207  // Sniff sniffs a binary's goos, goarch, and fatbin offset. Sniff returns errors
   208  // returned by the provided reader, or ErrCorruptedImage if the binary is identified
   209  // as a fatbin image with a checksum mismatch.
   210  func Sniff(r io.ReaderAt, size int64) (goos, goarch string, offset int64, err error) {
   211  	for _, s := range sniffers {
   212  		var ok bool
   213  		goos, goarch, ok = s(r)
   214  		if ok {
   215  			break
   216  		}
   217  	}
   218  	if goos == "" {
   219  		goos = "unknown"
   220  	}
   221  	if goarch == "" {
   222  		goarch = "unknown"
   223  	}
   224  	offset, err = readFooter(r, size)
   225  	if err == errNoFooter {
   226  		err = nil
   227  		offset = size
   228  	}
   229  	return
   230  }
   231  
   232  type sniffer func(r io.ReaderAt) (goos, goarch string, ok bool)
   233  
   234  var sniffers = []sniffer{sniffElf, sniffMacho}
   235  
   236  func sniffElf(r io.ReaderAt) (goos, goarch string, ok bool) {
   237  	file, err := elf.NewFile(r)
   238  	if err != nil {
   239  		return
   240  	}
   241  	ok = true
   242  	switch file.OSABI {
   243  	default:
   244  		goos = "unknown"
   245  	case elf.ELFOSABI_NONE, elf.ELFOSABI_LINUX:
   246  		goos = "linux"
   247  	case elf.ELFOSABI_NETBSD:
   248  		goos = "netbsd"
   249  	case elf.ELFOSABI_OPENBSD:
   250  		goos = "openbsd"
   251  	}
   252  	switch file.Machine {
   253  	default:
   254  		goarch = "unknown"
   255  	case elf.EM_386:
   256  		goarch = "386"
   257  	case elf.EM_X86_64:
   258  		goarch = "amd64"
   259  	case elf.EM_ARM:
   260  		goarch = "arm"
   261  	case elf.EM_AARCH64:
   262  		goarch = "arm64"
   263  	}
   264  	return
   265  }
   266  
   267  func sniffMacho(r io.ReaderAt) (goos, goarch string, ok bool) {
   268  	file, err := macho.NewFile(r)
   269  	if err != nil {
   270  		return
   271  	}
   272  	ok = true
   273  	// We assume mach-o is only used in Darwin. This is not exposed
   274  	// by the mach-o files.
   275  	goos = "darwin"
   276  	switch file.Cpu {
   277  	default:
   278  		goarch = "unknown"
   279  	case macho.Cpu386:
   280  		goarch = "386"
   281  	case macho.CpuAmd64:
   282  		goarch = "amd64"
   283  	case macho.CpuArm:
   284  		goarch = "arm"
   285  	case macho.CpuArm64:
   286  		goarch = "arm64"
   287  	case macho.CpuPpc:
   288  		goarch = "ppc"
   289  	case macho.CpuPpc64:
   290  		goarch = "ppc64"
   291  	}
   292  	return
   293  }