1// Copyright 2022 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
5// Package cfile implements management of coverage files.
6// It provides functionality exported in runtime/coverage as well as
7// additional functionality used directly by package testing
8// through testing/internal/testdeps.
9package cfile
10
11import (
12	"crypto/md5"
13	"fmt"
14	"internal/coverage"
15	"internal/coverage/encodecounter"
16	"internal/coverage/encodemeta"
17	"internal/coverage/rtcov"
18	"io"
19	"os"
20	"path/filepath"
21	"runtime"
22	"strconv"
23	"sync/atomic"
24	"time"
25	"unsafe"
26)
27
28// This file contains functions that support the writing of data files
29// emitted at the end of code coverage testing runs, from instrumented
30// executables.
31
32// getCovCounterList returns a list of counter-data blobs registered
33// for the currently executing instrumented program. It is defined in the
34// runtime.
35//
36//go:linkname getCovCounterList
37func getCovCounterList() []rtcov.CovCounterBlob
38
39// emitState holds useful state information during the emit process.
40//
41// When an instrumented program finishes execution and starts the
42// process of writing out coverage data, it's possible that an
43// existing meta-data file already exists in the output directory. In
44// this case openOutputFiles() below will leave the 'mf' field below
45// as nil. If a new meta-data file is needed, field 'mfname' will be
46// the final desired path of the meta file, 'mftmp' will be a
47// temporary file, and 'mf' will be an open os.File pointer for
48// 'mftmp'. The meta-data file payload will be written to 'mf', the
49// temp file will be then closed and renamed (from 'mftmp' to
50// 'mfname'), so as to insure that the meta-data file is created
51// atomically; we want this so that things work smoothly in cases
52// where there are several instances of a given instrumented program
53// all terminating at the same time and trying to create meta-data
54// files simultaneously.
55//
56// For counter data files there is less chance of a collision, hence
57// the openOutputFiles() stores the counter data file in 'cfname' and
58// then places the *io.File into 'cf'.
59type emitState struct {
60	mfname string   // path of final meta-data output file
61	mftmp  string   // path to meta-data temp file (if needed)
62	mf     *os.File // open os.File for meta-data temp file
63	cfname string   // path of final counter data file
64	cftmp  string   // path to counter data temp file
65	cf     *os.File // open os.File for counter data file
66	outdir string   // output directory
67
68	// List of meta-data symbols obtained from the runtime
69	metalist []rtcov.CovMetaBlob
70
71	// List of counter-data symbols obtained from the runtime
72	counterlist []rtcov.CovCounterBlob
73
74	// Table to use for remapping hard-coded pkg ids.
75	pkgmap map[int]int
76
77	// emit debug trace output
78	debug bool
79}
80
81var (
82	// finalHash is computed at init time from the list of meta-data
83	// symbols registered during init. It is used both for writing the
84	// meta-data file and counter-data files.
85	finalHash [16]byte
86	// Set to true when we've computed finalHash + finalMetaLen.
87	finalHashComputed bool
88	// Total meta-data length.
89	finalMetaLen uint64
90	// Records whether we've already attempted to write meta-data.
91	metaDataEmitAttempted bool
92	// Counter mode for this instrumented program run.
93	cmode coverage.CounterMode
94	// Counter granularity for this instrumented program run.
95	cgran coverage.CounterGranularity
96	// Cached value of GOCOVERDIR environment variable.
97	goCoverDir string
98	// Copy of os.Args made at init time, converted into map format.
99	capturedOsArgs map[string]string
100	// Flag used in tests to signal that coverage data already written.
101	covProfileAlreadyEmitted bool
102)
103
104// fileType is used to select between counter-data files and
105// meta-data files.
106type fileType int
107
108const (
109	noFile = 1 << iota
110	metaDataFile
111	counterDataFile
112)
113
114// emitMetaData emits the meta-data output file for this coverage run.
115// This entry point is intended to be invoked by the compiler from
116// an instrumented program's main package init func.
117func emitMetaData() {
118	if covProfileAlreadyEmitted {
119		return
120	}
121	ml, err := prepareForMetaEmit()
122	if err != nil {
123		fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err)
124		if os.Getenv("GOCOVERDEBUG") != "" {
125			panic("meta-data write failure")
126		}
127	}
128	if len(ml) == 0 {
129		fmt.Fprintf(os.Stderr, "program not built with -cover\n")
130		return
131	}
132
133	goCoverDir = os.Getenv("GOCOVERDIR")
134	if goCoverDir == "" {
135		fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n")
136		return
137	}
138
139	if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil {
140		fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err)
141		if os.Getenv("GOCOVERDEBUG") != "" {
142			panic("meta-data write failure")
143		}
144	}
145}
146
147func modeClash(m coverage.CounterMode) bool {
148	if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain {
149		return false
150	}
151	if cmode == coverage.CtrModeInvalid {
152		cmode = m
153		return false
154	}
155	return cmode != m
156}
157
158func granClash(g coverage.CounterGranularity) bool {
159	if cgran == coverage.CtrGranularityInvalid {
160		cgran = g
161		return false
162	}
163	return cgran != g
164}
165
166// prepareForMetaEmit performs preparatory steps needed prior to
167// emitting a meta-data file, notably computing a final hash of
168// all meta-data blobs and capturing os args.
169func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) {
170	// Ask the runtime for the list of coverage meta-data symbols.
171	ml := rtcov.Meta.List
172
173	// In the normal case (go build -o prog.exe ... ; ./prog.exe)
174	// len(ml) will always be non-zero, but we check here since at
175	// some point this function will be reachable via user-callable
176	// APIs (for example, to write out coverage data from a server
177	// program that doesn't ever call os.Exit).
178	if len(ml) == 0 {
179		return nil, nil
180	}
181
182	s := &emitState{
183		metalist: ml,
184		debug:    os.Getenv("GOCOVERDEBUG") != "",
185	}
186
187	// Capture os.Args() now so as to avoid issues if args
188	// are rewritten during program execution.
189	capturedOsArgs = captureOsArgs()
190
191	if s.debug {
192		fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR"))
193		fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n")
194		for k, b := range ml {
195			fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath)
196			if b.PkgID != -1 {
197				fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID)
198			}
199			fmt.Fprintf(os.Stderr, "\n")
200		}
201		pm := rtcov.Meta.PkgMap
202		fmt.Fprintf(os.Stderr, "=+= remap table:\n")
203		for from, to := range pm {
204			fmt.Fprintf(os.Stderr, "=+= from %d to %d\n",
205				uint32(from), uint32(to))
206		}
207	}
208
209	h := md5.New()
210	tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))
211	for _, entry := range ml {
212		if _, err := h.Write(entry.Hash[:]); err != nil {
213			return nil, err
214		}
215		tlen += uint64(entry.Len)
216		ecm := coverage.CounterMode(entry.CounterMode)
217		if modeClash(ecm) {
218			return nil, fmt.Errorf("coverage counter mode clash: package %s uses mode=%d, but package %s uses mode=%s\n", ml[0].PkgPath, cmode, entry.PkgPath, ecm)
219		}
220		ecg := coverage.CounterGranularity(entry.CounterGranularity)
221		if granClash(ecg) {
222			return nil, fmt.Errorf("coverage counter granularity clash: package %s uses gran=%d, but package %s uses gran=%s\n", ml[0].PkgPath, cgran, entry.PkgPath, ecg)
223		}
224	}
225
226	// Hash mode and granularity as well.
227	h.Write([]byte(cmode.String()))
228	h.Write([]byte(cgran.String()))
229
230	// Compute final digest.
231	fh := h.Sum(nil)
232	copy(finalHash[:], fh)
233	finalHashComputed = true
234	finalMetaLen = tlen
235
236	return ml, nil
237}
238
239// emitMetaDataToDirectory emits the meta-data output file to the specified
240// directory, returning an error if something went wrong.
241func emitMetaDataToDirectory(outdir string, ml []rtcov.CovMetaBlob) error {
242	ml, err := prepareForMetaEmit()
243	if err != nil {
244		return err
245	}
246	if len(ml) == 0 {
247		return nil
248	}
249
250	metaDataEmitAttempted = true
251
252	s := &emitState{
253		metalist: ml,
254		debug:    os.Getenv("GOCOVERDEBUG") != "",
255		outdir:   outdir,
256	}
257
258	// Open output files.
259	if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil {
260		return err
261	}
262
263	// Emit meta-data file only if needed (may already be present).
264	if s.needMetaDataFile() {
265		if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil {
266			return err
267		}
268	}
269	return nil
270}
271
272// emitCounterData emits the counter data output file for this coverage run.
273// This entry point is intended to be invoked by the runtime when an
274// instrumented program is terminating or calling os.Exit().
275func emitCounterData() {
276	if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted {
277		return
278	}
279	if err := emitCounterDataToDirectory(goCoverDir); err != nil {
280		fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err)
281		if os.Getenv("GOCOVERDEBUG") != "" {
282			panic("counter-data write failure")
283		}
284	}
285}
286
287// emitCounterDataToDirectory emits the counter-data output file for this coverage run.
288func emitCounterDataToDirectory(outdir string) error {
289	// Ask the runtime for the list of coverage counter symbols.
290	cl := getCovCounterList()
291	if len(cl) == 0 {
292		// no work to do here.
293		return nil
294	}
295
296	if !finalHashComputed {
297		return fmt.Errorf("error: meta-data not available (binary not built with -cover?)")
298	}
299
300	// Ask the runtime for the list of coverage counter symbols.
301	pm := rtcov.Meta.PkgMap
302	s := &emitState{
303		counterlist: cl,
304		pkgmap:      pm,
305		outdir:      outdir,
306		debug:       os.Getenv("GOCOVERDEBUG") != "",
307	}
308
309	// Open output file.
310	if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil {
311		return err
312	}
313	if s.cf == nil {
314		return fmt.Errorf("counter data output file open failed (no additional info")
315	}
316
317	// Emit counter data file.
318	if err := s.emitCounterDataFile(finalHash, s.cf); err != nil {
319		return err
320	}
321	if err := s.cf.Close(); err != nil {
322		return fmt.Errorf("closing counter data file: %v", err)
323	}
324
325	// Counter file has now been closed. Rename the temp to the
326	// final desired path.
327	if err := os.Rename(s.cftmp, s.cfname); err != nil {
328		return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err)
329	}
330
331	return nil
332}
333
334// emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.
335func (s *emitState) emitCounterDataToWriter(w io.Writer) error {
336	if err := s.emitCounterDataFile(finalHash, w); err != nil {
337		return err
338	}
339	return nil
340}
341
342// openMetaFile determines whether we need to emit a meta-data output
343// file, or whether we can reuse the existing file in the coverage out
344// dir. It updates mfname/mftmp/mf fields in 's', returning an error
345// if something went wrong. See the comment on the emitState type
346// definition above for more on how file opening is managed.
347func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error {
348
349	// Open meta-outfile for reading to see if it exists.
350	fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash)
351	s.mfname = filepath.Join(s.outdir, fn)
352	fi, err := os.Stat(s.mfname)
353	if err != nil || fi.Size() != int64(metaLen) {
354		// We need a new meta-file.
355		tname := "tmp." + fn + strconv.FormatInt(time.Now().UnixNano(), 10)
356		s.mftmp = filepath.Join(s.outdir, tname)
357		s.mf, err = os.Create(s.mftmp)
358		if err != nil {
359			return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err)
360		}
361	}
362	return nil
363}
364
365// openCounterFile opens an output file for the counter data portion
366// of a test coverage run. If updates the 'cfname' and 'cf' fields in
367// 's', returning an error if something went wrong.
368func (s *emitState) openCounterFile(metaHash [16]byte) error {
369	processID := os.Getpid()
370	fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano())
371	s.cfname = filepath.Join(s.outdir, fn)
372	s.cftmp = filepath.Join(s.outdir, "tmp."+fn)
373	var err error
374	s.cf, err = os.Create(s.cftmp)
375	if err != nil {
376		return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err)
377	}
378	return nil
379}
380
381// openOutputFiles opens output files in preparation for emitting
382// coverage data. In the case of the meta-data file, openOutputFiles
383// may determine that we can reuse an existing meta-data file in the
384// outdir, in which case it will leave the 'mf' field in the state
385// struct as nil. If a new meta-file is needed, the field 'mfname'
386// will be the final desired path of the meta file, 'mftmp' will be a
387// temporary file, and 'mf' will be an open os.File pointer for
388// 'mftmp'. The idea is that the client/caller will write content into
389// 'mf', close it, and then rename 'mftmp' to 'mfname'. This function
390// also opens the counter data output file, setting 'cf' and 'cfname'
391// in the state struct.
392func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error {
393	fi, err := os.Stat(s.outdir)
394	if err != nil {
395		return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err)
396	}
397	if !fi.IsDir() {
398		return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir)
399	}
400
401	if (which & metaDataFile) != 0 {
402		if err := s.openMetaFile(metaHash, metaLen); err != nil {
403			return err
404		}
405	}
406	if (which & counterDataFile) != 0 {
407		if err := s.openCounterFile(metaHash); err != nil {
408			return err
409		}
410	}
411	return nil
412}
413
414// emitMetaDataFile emits coverage meta-data to a previously opened
415// temporary file (s.mftmp), then renames the generated file to the
416// final path (s.mfname).
417func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error {
418	if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil {
419		return fmt.Errorf("writing %s: %v\n", s.mftmp, err)
420	}
421	if err := s.mf.Close(); err != nil {
422		return fmt.Errorf("closing meta data temp file: %v", err)
423	}
424
425	// Temp file has now been flushed and closed. Rename the temp to the
426	// final desired path.
427	if err := os.Rename(s.mftmp, s.mfname); err != nil {
428		return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err)
429	}
430
431	return nil
432}
433
434// needMetaDataFile returns TRUE if we need to emit a meta-data file
435// for this program run. It should be used only after
436// openOutputFiles() has been invoked.
437func (s *emitState) needMetaDataFile() bool {
438	return s.mf != nil
439}
440
441func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error {
442	mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w)
443
444	var blobs [][]byte
445	for _, e := range metalist {
446		sd := unsafe.Slice(e.P, int(e.Len))
447		blobs = append(blobs, sd)
448	}
449	return mfw.Write(finalHash, blobs, cmode, gran)
450}
451
452func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
453	var tcounters []uint32
454
455	rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 {
456		ctrs = ctrs[:0]
457		for i := range actrs {
458			ctrs = append(ctrs, actrs[i].Load())
459		}
460		return ctrs
461	}
462
463	dpkg := uint32(0)
464	for _, c := range s.counterlist {
465		sd := unsafe.Slice((*atomic.Uint32)(unsafe.Pointer(c.Counters)), int(c.Len))
466		for i := 0; i < len(sd); i++ {
467			// Skip ahead until the next non-zero value.
468			sdi := sd[i].Load()
469			if sdi == 0 {
470				continue
471			}
472
473			// We found a function that was executed.
474			nCtrs := sd[i+coverage.NumCtrsOffset].Load()
475			pkgId := sd[i+coverage.PkgIdOffset].Load()
476			funcId := sd[i+coverage.FuncIdOffset].Load()
477			cst := i + coverage.FirstCtrOffset
478			counters := sd[cst : cst+int(nCtrs)]
479
480			// Check to make sure that we have at least one live
481			// counter. See the implementation note in ClearCoverageCounters
482			// for a description of why this is needed.
483			isLive := false
484			for i := 0; i < len(counters); i++ {
485				if counters[i].Load() != 0 {
486					isLive = true
487					break
488				}
489			}
490			if !isLive {
491				// Skip this function.
492				i += coverage.FirstCtrOffset + int(nCtrs) - 1
493				continue
494			}
495
496			if s.debug {
497				if pkgId != dpkg {
498					dpkg = pkgId
499					fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn",
500						i, pkgId)
501				}
502				fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs)
503			}
504
505			// Vet and/or fix up package ID. A package ID of zero
506			// indicates that there is some new package X that is a
507			// runtime dependency, and this package has code that
508			// executes before its corresponding init package runs.
509			// This is a fatal error that we should only see during
510			// Go development (e.g. tip).
511			ipk := int32(pkgId)
512			if ipk == 0 {
513				fmt.Fprintf(os.Stderr, "\n")
514				reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
515			} else if ipk < 0 {
516				if newId, ok := s.pkgmap[int(ipk)]; ok {
517					pkgId = uint32(newId)
518				} else {
519					fmt.Fprintf(os.Stderr, "\n")
520					reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
521				}
522			} else {
523				// The package ID value stored in the counter array
524				// has 1 added to it (so as to preclude the
525				// possibility of a zero value ; see
526				// runtime.addCovMeta), so subtract off 1 here to form
527				// the real package ID.
528				pkgId--
529			}
530
531			tcounters = rdCounters(counters, tcounters)
532			if err := f(pkgId, funcId, tcounters); err != nil {
533				return err
534			}
535
536			// Skip over this function.
537			i += coverage.FirstCtrOffset + int(nCtrs) - 1
538		}
539		if s.debug {
540			fmt.Fprintf(os.Stderr, "\n")
541		}
542	}
543	return nil
544}
545
546// captureOsArgs converts os.Args() into the format we use to store
547// this info in the counter data file (counter data file "args"
548// section is a generic key-value collection). See the 'args' section
549// in internal/coverage/defs.go for more info. The args map
550// is also used to capture GOOS + GOARCH values as well.
551func captureOsArgs() map[string]string {
552	m := make(map[string]string)
553	m["argc"] = strconv.Itoa(len(os.Args))
554	for k, a := range os.Args {
555		m[fmt.Sprintf("argv%d", k)] = a
556	}
557	m["GOOS"] = runtime.GOOS
558	m["GOARCH"] = runtime.GOARCH
559	return m
560}
561
562// emitCounterDataFile emits the counter data portion of a
563// coverage output file (to the file 's.cf').
564func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error {
565	cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128)
566	if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil {
567		return err
568	}
569	return nil
570}
571
572// MarkProfileEmitted signals the coverage machinery that
573// coverage data output files have already been written out, and there
574// is no need to take any additional action at exit time. This
575// function is called from the coverage-related boilerplate code in _testmain.go
576// emitted for go unit tests.
577func MarkProfileEmitted(val bool) {
578	covProfileAlreadyEmitted = val
579}
580
581func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) {
582	metaList := rtcov.Meta.List
583	pkgMap := rtcov.Meta.PkgMap
584
585	println("internal error in coverage meta-data tracking:")
586	println("encountered bad pkgID:", pkgID, " at slot:", slot,
587		" fnID:", fnID, " numCtrs:", nCtrs)
588	println("list of hard-coded runtime package IDs needs revising.")
589	println("[see the comment on the 'rtPkgs' var in ")
590	println(" <goroot>/src/internal/coverage/pkid.go]")
591	println("registered list:")
592	for k, b := range metaList {
593		print("slot: ", k, " path='", b.PkgPath, "' ")
594		if b.PkgID != -1 {
595			print(" hard-coded id: ", b.PkgID)
596		}
597		println("")
598	}
599	println("remap table:")
600	for from, to := range pkgMap {
601		println("from ", from, " to ", to)
602	}
603}
604