go/src/cmd/internal/buildid/rewrite.go
Cherry Zhang 8cd35e00bd cmd/internal/buildid: update Mach-O code signature when rewriting buildid
As the code signature contains hashes of the entire file (except
the signature itself), rewriting buildid will invalidate the
signature. This CL makes it regenerate the signature when
rewriting the buildid. It only does it when the file already has
a code signature, with proper size (darwin/arm64 binaries
generated by the Go linker should have).

Updates #38485, #42684.

Change-Id: I082d9e5808b0ee6a35f9c362d7262aadd9113c81
Reviewed-on: https://go-review.googlesource.com/c/go/+/272257
Trust: Cherry Zhang <cherryyz@google.com>
Run-TryBot: Cherry Zhang <cherryyz@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
Reviewed-by: Than McIntosh <thanm@google.com>
2020-12-01 23:37:58 +00:00

163 lines
5.0 KiB
Go

// Copyright 2017 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 buildid
import (
"bytes"
"cmd/internal/codesign"
"crypto/sha256"
"debug/macho"
"fmt"
"io"
)
// FindAndHash reads all of r and returns the offsets of occurrences of id.
// While reading, findAndHash also computes and returns
// a hash of the content of r, but with occurrences of id replaced by zeros.
// FindAndHash reads bufSize bytes from r at a time.
// If bufSize == 0, FindAndHash uses a reasonable default.
func FindAndHash(r io.Reader, id string, bufSize int) (matches []int64, hash [32]byte, err error) {
if bufSize == 0 {
bufSize = 31 * 1024 // bufSize+little will likely fit in 32 kB
}
if len(id) > bufSize {
return nil, [32]byte{}, fmt.Errorf("buildid.FindAndHash: buffer too small")
}
zeros := make([]byte, len(id))
idBytes := []byte(id)
// For Mach-O files, we want to exclude the code signature.
// The code signature contains hashes of the whole file (except the signature
// itself), including the buildid. So the buildid cannot contain the signature.
r = excludeMachoCodeSignature(r)
// The strategy is to read the file through buf, looking for id,
// but we need to worry about what happens if id is broken up
// and returned in parts by two different reads.
// We allocate a tiny buffer (at least len(id)) and a big buffer (bufSize bytes)
// next to each other in memory and then copy the tail of
// one read into the tiny buffer before reading new data into the big buffer.
// The search for id is over the entire tiny+big buffer.
tiny := (len(id) + 127) &^ 127 // round up to 128-aligned
buf := make([]byte, tiny+bufSize)
h := sha256.New()
start := tiny
for offset := int64(0); ; {
// The file offset maintained by the loop corresponds to &buf[tiny].
// buf[start:tiny] is left over from previous iteration.
// After reading n bytes into buf[tiny:], we process buf[start:tiny+n].
n, err := io.ReadFull(r, buf[tiny:])
if err != io.ErrUnexpectedEOF && err != io.EOF && err != nil {
return nil, [32]byte{}, err
}
// Process any matches.
for {
i := bytes.Index(buf[start:tiny+n], idBytes)
if i < 0 {
break
}
matches = append(matches, offset+int64(start+i-tiny))
h.Write(buf[start : start+i])
h.Write(zeros)
start += i + len(id)
}
if n < bufSize {
// Did not fill buffer, must be at end of file.
h.Write(buf[start : tiny+n])
break
}
// Process all but final tiny bytes of buf (bufSize = len(buf)-tiny).
// Note that start > len(buf)-tiny is possible, if the search above
// found an id ending in the final tiny fringe. That's OK.
if start < len(buf)-tiny {
h.Write(buf[start : len(buf)-tiny])
start = len(buf) - tiny
}
// Slide ending tiny-sized fringe to beginning of buffer.
copy(buf[0:], buf[bufSize:])
start -= bufSize
offset += int64(bufSize)
}
h.Sum(hash[:0])
return matches, hash, nil
}
func Rewrite(w io.WriterAt, pos []int64, id string) error {
b := []byte(id)
for _, p := range pos {
if _, err := w.WriteAt(b, p); err != nil {
return err
}
}
// Update Mach-O code signature, if any.
if f, cmd, ok := findMachoCodeSignature(w); ok {
if codesign.Size(int64(cmd.Dataoff), "a.out") == int64(cmd.Datasize) {
// Update the signature if the size matches, so we don't need to
// fix up headers. Binaries generated by the Go linker should have
// the expected size. Otherwise skip.
text := f.Segment("__TEXT")
cs := make([]byte, cmd.Datasize)
codesign.Sign(cs, w.(io.Reader), "a.out", int64(cmd.Dataoff), int64(text.Offset), int64(text.Filesz), f.Type == macho.TypeExec)
if _, err := w.WriteAt(cs, int64(cmd.Dataoff)); err != nil {
return err
}
}
}
return nil
}
func excludeMachoCodeSignature(r io.Reader) io.Reader {
_, cmd, ok := findMachoCodeSignature(r)
if !ok {
return r
}
return &excludedReader{r, 0, int64(cmd.Dataoff), int64(cmd.Dataoff + cmd.Datasize)}
}
// excludedReader wraps an io.Reader. Reading from it returns the bytes from
// the underlying reader, except that when the byte offset is within the
// range between start and end, it returns zero bytes.
type excludedReader struct {
r io.Reader
off int64 // current offset
start, end int64 // the range to be excluded (read as zero)
}
func (r *excludedReader) Read(p []byte) (int, error) {
n, err := r.r.Read(p)
if n > 0 && r.off+int64(n) > r.start && r.off < r.end {
cstart := r.start - r.off
if cstart < 0 {
cstart = 0
}
cend := r.end - r.off
if cend > int64(n) {
cend = int64(n)
}
zeros := make([]byte, cend-cstart)
copy(p[cstart:cend], zeros)
}
r.off += int64(n)
return n, err
}
func findMachoCodeSignature(r interface{}) (*macho.File, codesign.CodeSigCmd, bool) {
ra, ok := r.(io.ReaderAt)
if !ok {
return nil, codesign.CodeSigCmd{}, false
}
f, err := macho.NewFile(ra)
if err != nil {
return nil, codesign.CodeSigCmd{}, false
}
cmd, ok := codesign.FindCodeSigCmd(f)
return f, cmd, ok
}