1// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package buildid
6
7import (
8	"bytes"
9	"crypto/sha256"
10	"debug/elf"
11	"encoding/binary"
12	"internal/obscuretestdata"
13	"os"
14	"reflect"
15	"strings"
16	"testing"
17)
18
19const (
20	expectedID = "abcdefghijklmnopqrstuvwxyz.1234567890123456789012345678901234567890123456789012345678901234"
21	newID      = "bcdefghijklmnopqrstuvwxyza.2345678901234567890123456789012345678901234567890123456789012341"
22)
23
24func TestReadFile(t *testing.T) {
25	f, err := os.CreateTemp("", "buildid-test-")
26	if err != nil {
27		t.Fatal(err)
28	}
29	tmp := f.Name()
30	defer os.Remove(tmp)
31	f.Close()
32
33	// Use obscured files to prevent Apple’s notarization service from
34	// mistaking them as candidates for notarization and rejecting the entire
35	// toolchain.
36	// See golang.org/issue/34986
37	var files = []string{
38		"p.a.base64",
39		"a.elf.base64",
40		"a.macho.base64",
41		"a.pe.base64",
42	}
43
44	for _, name := range files {
45		f, err := obscuretestdata.DecodeToTempFile("testdata/" + name)
46		if err != nil {
47			t.Errorf("obscuretestdata.DecodeToTempFile(testdata/%s): %v", name, err)
48			continue
49		}
50		defer os.Remove(f)
51		id, err := ReadFile(f)
52		if id != expectedID || err != nil {
53			t.Errorf("ReadFile(testdata/%s) = %q, %v, want %q, nil", f, id, err, expectedID)
54		}
55		old := readSize
56		readSize = 2048
57		id, err = ReadFile(f)
58		readSize = old
59		if id != expectedID || err != nil {
60			t.Errorf("ReadFile(%s) [readSize=2k] = %q, %v, want %q, nil", f, id, err, expectedID)
61		}
62
63		data, err := os.ReadFile(f)
64		if err != nil {
65			t.Fatal(err)
66		}
67		m, _, err := FindAndHash(bytes.NewReader(data), expectedID, 1024)
68		if err != nil {
69			t.Errorf("FindAndHash(%s): %v", f, err)
70			continue
71		}
72		if err := os.WriteFile(tmp, data, 0666); err != nil {
73			t.Error(err)
74			continue
75		}
76		tf, err := os.OpenFile(tmp, os.O_WRONLY, 0)
77		if err != nil {
78			t.Error(err)
79			continue
80		}
81		err = Rewrite(tf, m, newID)
82		err2 := tf.Close()
83		if err != nil {
84			t.Errorf("Rewrite(%s): %v", f, err)
85			continue
86		}
87		if err2 != nil {
88			t.Fatal(err2)
89		}
90
91		id, err = ReadFile(tmp)
92		if id != newID || err != nil {
93			t.Errorf("ReadFile(%s after Rewrite) = %q, %v, want %q, nil", f, id, err, newID)
94		}
95
96		// Test an ELF PT_NOTE segment with an Align field of 0.
97		// Do this by rewriting the file data.
98		if strings.Contains(name, "elf") {
99			// We only expect a 64-bit ELF file.
100			if elf.Class(data[elf.EI_CLASS]) != elf.ELFCLASS64 {
101				continue
102			}
103
104			// We only expect a little-endian ELF file.
105			if elf.Data(data[elf.EI_DATA]) != elf.ELFDATA2LSB {
106				continue
107			}
108			order := binary.LittleEndian
109
110			var hdr elf.Header64
111			if err := binary.Read(bytes.NewReader(data), order, &hdr); err != nil {
112				t.Error(err)
113				continue
114			}
115
116			phoff := hdr.Phoff
117			phnum := int(hdr.Phnum)
118			phsize := uint64(hdr.Phentsize)
119
120			for i := 0; i < phnum; i++ {
121				var phdr elf.Prog64
122				if err := binary.Read(bytes.NewReader(data[phoff:]), order, &phdr); err != nil {
123					t.Error(err)
124					continue
125				}
126
127				if elf.ProgType(phdr.Type) == elf.PT_NOTE {
128					// Increase the size so we keep
129					// reading notes.
130					order.PutUint64(data[phoff+4*8:], phdr.Filesz+1)
131
132					// Clobber the Align field to zero.
133					order.PutUint64(data[phoff+6*8:], 0)
134
135					// Clobber the note type so we
136					// keep reading notes.
137					order.PutUint32(data[phdr.Off+12:], 0)
138				}
139
140				phoff += phsize
141			}
142
143			if err := os.WriteFile(tmp, data, 0666); err != nil {
144				t.Error(err)
145				continue
146			}
147
148			id, err := ReadFile(tmp)
149			// Because we clobbered the note type above,
150			// we don't expect to see a Go build ID.
151			// The issue we are testing for was a crash
152			// in Readfile; see issue #62097.
153			if id != "" || err != nil {
154				t.Errorf("ReadFile with zero ELF Align = %q, %v, want %q, nil", id, err, "")
155				continue
156			}
157		}
158	}
159}
160
161func TestFindAndHash(t *testing.T) {
162	buf := make([]byte, 64)
163	buf2 := make([]byte, 64)
164	id := make([]byte, 8)
165	zero := make([]byte, 8)
166	for i := range id {
167		id[i] = byte(i)
168	}
169	numError := 0
170	errorf := func(msg string, args ...any) {
171		t.Errorf(msg, args...)
172		if numError++; numError > 20 {
173			t.Logf("stopping after too many errors")
174			t.FailNow()
175		}
176	}
177	for bufSize := len(id); bufSize <= len(buf); bufSize++ {
178		for j := range buf {
179			for k := 0; k < 2*len(id) && j+k < len(buf); k++ {
180				for i := range buf {
181					buf[i] = 1
182				}
183				copy(buf[j:], id)
184				copy(buf[j+k:], id)
185				var m []int64
186				if j+len(id) <= j+k {
187					m = append(m, int64(j))
188				}
189				if j+k+len(id) <= len(buf) {
190					m = append(m, int64(j+k))
191				}
192				copy(buf2, buf)
193				for _, p := range m {
194					copy(buf2[p:], zero)
195				}
196				h := sha256.Sum256(buf2)
197
198				matches, hash, err := FindAndHash(bytes.NewReader(buf), string(id), bufSize)
199				if err != nil {
200					errorf("bufSize=%d j=%d k=%d: findAndHash: %v", bufSize, j, k, err)
201					continue
202				}
203				if !reflect.DeepEqual(matches, m) {
204					errorf("bufSize=%d j=%d k=%d: findAndHash: matches=%v, want %v", bufSize, j, k, matches, m)
205					continue
206				}
207				if hash != h {
208					errorf("bufSize=%d j=%d k=%d: findAndHash: matches correct, but hash=%x, want %x", bufSize, j, k, hash, h)
209				}
210			}
211		}
212	}
213}
214
215func TestExcludedReader(t *testing.T) {
216	const s = "0123456789abcdefghijklmn"
217	tests := []struct {
218		start, end int64    // excluded range
219		results    []string // expected results of reads
220	}{
221		{12, 15, []string{"0123456789", "ab\x00\x00\x00fghij", "klmn"}},                              // within one read
222		{8, 21, []string{"01234567\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", "\x00lmn"}}, // across multiple reads
223		{10, 20, []string{"0123456789", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", "klmn"}},         // a whole read
224		{0, 5, []string{"\x00\x00\x00\x00\x0056789", "abcdefghij", "klmn"}},                          // start
225		{12, 24, []string{"0123456789", "ab\x00\x00\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00"}},   // end
226	}
227	p := make([]byte, 10)
228	for _, test := range tests {
229		r := &excludedReader{strings.NewReader(s), 0, test.start, test.end}
230		for _, res := range test.results {
231			n, err := r.Read(p)
232			if err != nil {
233				t.Errorf("read failed: %v", err)
234			}
235			if n != len(res) {
236				t.Errorf("unexpected number of bytes read: want %d, got %d", len(res), n)
237			}
238			if string(p[:n]) != res {
239				t.Errorf("unexpected bytes: want %q, got %q", res, p[:n])
240			}
241		}
242	}
243}
244
245func TestEmptyID(t *testing.T) {
246	r := strings.NewReader("aha!")
247	matches, hash, err := FindAndHash(r, "", 1000)
248	if matches != nil || hash != ([32]byte{}) || err == nil || !strings.Contains(err.Error(), "no id") {
249		t.Errorf("FindAndHash: want nil, [32]byte{}, no id specified, got %v, %v, %v", matches, hash, err)
250	}
251}
252