diff options
Diffstat (limited to 'src/runtime/debug')
-rw-r--r-- | src/runtime/debug/debug.s | 9 | ||||
-rw-r--r-- | src/runtime/debug/garbage.go | 238 | ||||
-rw-r--r-- | src/runtime/debug/garbage_test.go | 238 | ||||
-rw-r--r-- | src/runtime/debug/heapdump_test.go | 95 | ||||
-rw-r--r-- | src/runtime/debug/mod.go | 287 | ||||
-rw-r--r-- | src/runtime/debug/mod_test.go | 75 | ||||
-rw-r--r-- | src/runtime/debug/panic_test.go | 56 | ||||
-rw-r--r-- | src/runtime/debug/stack.go | 30 | ||||
-rw-r--r-- | src/runtime/debug/stack_test.go | 121 | ||||
-rw-r--r-- | src/runtime/debug/stubs.go | 18 | ||||
-rw-r--r-- | src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/5501685e611fa764 | 2 | ||||
-rw-r--r-- | src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/71634114e78567cf | 2 | ||||
-rw-r--r-- | src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/c73dce23c1f2494c | 2 |
13 files changed, 1173 insertions, 0 deletions
diff --git a/src/runtime/debug/debug.s b/src/runtime/debug/debug.s new file mode 100644 index 0000000..6aae33a --- /dev/null +++ b/src/runtime/debug/debug.s @@ -0,0 +1,9 @@ +// Copyright 2013 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. + +// Nothing to see here. +// This file exists so that the go command knows that parts of the +// package are implemented in C, so that it does not instruct the +// Go compiler to complain about extern declarations. +// The actual implementations are in package runtime. diff --git a/src/runtime/debug/garbage.go b/src/runtime/debug/garbage.go new file mode 100644 index 0000000..cb3248d --- /dev/null +++ b/src/runtime/debug/garbage.go @@ -0,0 +1,238 @@ +// Copyright 2013 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 debug + +import ( + "runtime" + "sort" + "time" +) + +// GCStats collect information about recent garbage collections. +type GCStats struct { + LastGC time.Time // time of last collection + NumGC int64 // number of garbage collections + PauseTotal time.Duration // total pause for all collections + Pause []time.Duration // pause history, most recent first + PauseEnd []time.Time // pause end times history, most recent first + PauseQuantiles []time.Duration +} + +// ReadGCStats reads statistics about garbage collection into stats. +// The number of entries in the pause history is system-dependent; +// stats.Pause slice will be reused if large enough, reallocated otherwise. +// ReadGCStats may use the full capacity of the stats.Pause slice. +// If stats.PauseQuantiles is non-empty, ReadGCStats fills it with quantiles +// summarizing the distribution of pause time. For example, if +// len(stats.PauseQuantiles) is 5, it will be filled with the minimum, +// 25%, 50%, 75%, and maximum pause times. +func ReadGCStats(stats *GCStats) { + // Create a buffer with space for at least two copies of the + // pause history tracked by the runtime. One will be returned + // to the caller and the other will be used as transfer buffer + // for end times history and as a temporary buffer for + // computing quantiles. + const maxPause = len(((*runtime.MemStats)(nil)).PauseNs) + if cap(stats.Pause) < 2*maxPause+3 { + stats.Pause = make([]time.Duration, 2*maxPause+3) + } + + // readGCStats fills in the pause and end times histories (up to + // maxPause entries) and then three more: Unix ns time of last GC, + // number of GC, and total pause time in nanoseconds. Here we + // depend on the fact that time.Duration's native unit is + // nanoseconds, so the pauses and the total pause time do not need + // any conversion. + readGCStats(&stats.Pause) + n := len(stats.Pause) - 3 + stats.LastGC = time.Unix(0, int64(stats.Pause[n])) + stats.NumGC = int64(stats.Pause[n+1]) + stats.PauseTotal = stats.Pause[n+2] + n /= 2 // buffer holds pauses and end times + stats.Pause = stats.Pause[:n] + + if cap(stats.PauseEnd) < maxPause { + stats.PauseEnd = make([]time.Time, 0, maxPause) + } + stats.PauseEnd = stats.PauseEnd[:0] + for _, ns := range stats.Pause[n : n+n] { + stats.PauseEnd = append(stats.PauseEnd, time.Unix(0, int64(ns))) + } + + if len(stats.PauseQuantiles) > 0 { + if n == 0 { + for i := range stats.PauseQuantiles { + stats.PauseQuantiles[i] = 0 + } + } else { + // There's room for a second copy of the data in stats.Pause. + // See the allocation at the top of the function. + sorted := stats.Pause[n : n+n] + copy(sorted, stats.Pause) + sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + nq := len(stats.PauseQuantiles) - 1 + for i := 0; i < nq; i++ { + stats.PauseQuantiles[i] = sorted[len(sorted)*i/nq] + } + stats.PauseQuantiles[nq] = sorted[len(sorted)-1] + } + } +} + +// SetGCPercent sets the garbage collection target percentage: +// a collection is triggered when the ratio of freshly allocated data +// to live data remaining after the previous collection reaches this percentage. +// SetGCPercent returns the previous setting. +// The initial setting is the value of the GOGC environment variable +// at startup, or 100 if the variable is not set. +// This setting may be effectively reduced in order to maintain a memory +// limit. +// A negative percentage effectively disables garbage collection, unless +// the memory limit is reached. +// See SetMemoryLimit for more details. +func SetGCPercent(percent int) int { + return int(setGCPercent(int32(percent))) +} + +// FreeOSMemory forces a garbage collection followed by an +// attempt to return as much memory to the operating system +// as possible. (Even if this is not called, the runtime gradually +// returns memory to the operating system in a background task.) +func FreeOSMemory() { + freeOSMemory() +} + +// SetMaxStack sets the maximum amount of memory that +// can be used by a single goroutine stack. +// If any goroutine exceeds this limit while growing its stack, +// the program crashes. +// SetMaxStack returns the previous setting. +// The initial setting is 1 GB on 64-bit systems, 250 MB on 32-bit systems. +// There may be a system-imposed maximum stack limit regardless +// of the value provided to SetMaxStack. +// +// SetMaxStack is useful mainly for limiting the damage done by +// goroutines that enter an infinite recursion. It only limits future +// stack growth. +func SetMaxStack(bytes int) int { + return setMaxStack(bytes) +} + +// SetMaxThreads sets the maximum number of operating system +// threads that the Go program can use. If it attempts to use more than +// this many, the program crashes. +// SetMaxThreads returns the previous setting. +// The initial setting is 10,000 threads. +// +// The limit controls the number of operating system threads, not the number +// of goroutines. A Go program creates a new thread only when a goroutine +// is ready to run but all the existing threads are blocked in system calls, cgo calls, +// or are locked to other goroutines due to use of runtime.LockOSThread. +// +// SetMaxThreads is useful mainly for limiting the damage done by +// programs that create an unbounded number of threads. The idea is +// to take down the program before it takes down the operating system. +func SetMaxThreads(threads int) int { + return setMaxThreads(threads) +} + +// SetPanicOnFault controls the runtime's behavior when a program faults +// at an unexpected (non-nil) address. Such faults are typically caused by +// bugs such as runtime memory corruption, so the default response is to crash +// the program. Programs working with memory-mapped files or unsafe +// manipulation of memory may cause faults at non-nil addresses in less +// dramatic situations; SetPanicOnFault allows such programs to request +// that the runtime trigger only a panic, not a crash. +// The runtime.Error that the runtime panics with may have an additional method: +// +// Addr() uintptr +// +// If that method exists, it returns the memory address which triggered the fault. +// The results of Addr are best-effort and the veracity of the result +// may depend on the platform. +// SetPanicOnFault applies only to the current goroutine. +// It returns the previous setting. +func SetPanicOnFault(enabled bool) bool { + return setPanicOnFault(enabled) +} + +// WriteHeapDump writes a description of the heap and the objects in +// it to the given file descriptor. +// +// WriteHeapDump suspends the execution of all goroutines until the heap +// dump is completely written. Thus, the file descriptor must not be +// connected to a pipe or socket whose other end is in the same Go +// process; instead, use a temporary file or network socket. +// +// The heap dump format is defined at https://golang.org/s/go15heapdump. +func WriteHeapDump(fd uintptr) + +// SetTraceback sets the amount of detail printed by the runtime in +// the traceback it prints before exiting due to an unrecovered panic +// or an internal runtime error. +// The level argument takes the same values as the GOTRACEBACK +// environment variable. For example, SetTraceback("all") ensure +// that the program prints all goroutines when it crashes. +// See the package runtime documentation for details. +// If SetTraceback is called with a level lower than that of the +// environment variable, the call is ignored. +func SetTraceback(level string) + +// SetMemoryLimit provides the runtime with a soft memory limit. +// +// The runtime undertakes several processes to try to respect this +// memory limit, including adjustments to the frequency of garbage +// collections and returning memory to the underlying system more +// aggressively. This limit will be respected even if GOGC=off (or, +// if SetGCPercent(-1) is executed). +// +// The input limit is provided as bytes, and includes all memory +// mapped, managed, and not released by the Go runtime. Notably, it +// does not account for space used by the Go binary and memory +// external to Go, such as memory managed by the underlying system +// on behalf of the process, or memory managed by non-Go code inside +// the same process. Examples of excluded memory sources include: OS +// kernel memory held on behalf of the process, memory allocated by +// C code, and memory mapped by syscall.Mmap (because it is not +// managed by the Go runtime). +// +// More specifically, the following expression accurately reflects +// the value the runtime attempts to maintain as the limit: +// +// runtime.MemStats.Sys - runtime.MemStats.HeapReleased +// +// or in terms of the runtime/metrics package: +// +// /memory/classes/total:bytes - /memory/classes/heap/released:bytes +// +// A zero limit or a limit that's lower than the amount of memory +// used by the Go runtime may cause the garbage collector to run +// nearly continuously. However, the application may still make +// progress. +// +// The memory limit is always respected by the Go runtime, so to +// effectively disable this behavior, set the limit very high. +// [math.MaxInt64] is the canonical value for disabling the limit, +// but values much greater than the available memory on the underlying +// system work just as well. +// +// See https://go.dev/doc/gc-guide for a detailed guide explaining +// the soft memory limit in more detail, as well as a variety of common +// use-cases and scenarios. +// +// The initial setting is math.MaxInt64 unless the GOMEMLIMIT +// environment variable is set, in which case it provides the initial +// setting. GOMEMLIMIT is a numeric value in bytes with an optional +// unit suffix. The supported suffixes include B, KiB, MiB, GiB, and +// TiB. These suffixes represent quantities of bytes as defined by +// the IEC 80000-13 standard. That is, they are based on powers of +// two: KiB means 2^10 bytes, MiB means 2^20 bytes, and so on. +// +// SetMemoryLimit returns the previously set memory limit. +// A negative input does not adjust the limit, and allows for +// retrieval of the currently set memory limit. +func SetMemoryLimit(limit int64) int64 { + return setMemoryLimit(limit) +} diff --git a/src/runtime/debug/garbage_test.go b/src/runtime/debug/garbage_test.go new file mode 100644 index 0000000..cd91782 --- /dev/null +++ b/src/runtime/debug/garbage_test.go @@ -0,0 +1,238 @@ +// Copyright 2013 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 debug_test + +import ( + "internal/testenv" + "os" + "runtime" + . "runtime/debug" + "testing" + "time" +) + +func TestReadGCStats(t *testing.T) { + defer SetGCPercent(SetGCPercent(-1)) + + var stats GCStats + var mstats runtime.MemStats + var min, max time.Duration + + // First ReadGCStats will allocate, second should not, + // especially if we follow up with an explicit garbage collection. + stats.PauseQuantiles = make([]time.Duration, 10) + ReadGCStats(&stats) + runtime.GC() + + // Assume these will return same data: no GC during ReadGCStats. + ReadGCStats(&stats) + runtime.ReadMemStats(&mstats) + + if stats.NumGC != int64(mstats.NumGC) { + t.Errorf("stats.NumGC = %d, but mstats.NumGC = %d", stats.NumGC, mstats.NumGC) + } + if stats.PauseTotal != time.Duration(mstats.PauseTotalNs) { + t.Errorf("stats.PauseTotal = %d, but mstats.PauseTotalNs = %d", stats.PauseTotal, mstats.PauseTotalNs) + } + if stats.LastGC.UnixNano() != int64(mstats.LastGC) { + t.Errorf("stats.LastGC.UnixNano = %d, but mstats.LastGC = %d", stats.LastGC.UnixNano(), mstats.LastGC) + } + n := int(mstats.NumGC) + if n > len(mstats.PauseNs) { + n = len(mstats.PauseNs) + } + if len(stats.Pause) != n { + t.Errorf("len(stats.Pause) = %d, want %d", len(stats.Pause), n) + } else { + off := (int(mstats.NumGC) + len(mstats.PauseNs) - 1) % len(mstats.PauseNs) + for i := 0; i < n; i++ { + dt := stats.Pause[i] + if dt != time.Duration(mstats.PauseNs[off]) { + t.Errorf("stats.Pause[%d] = %d, want %d", i, dt, mstats.PauseNs[off]) + } + if max < dt { + max = dt + } + if min > dt || i == 0 { + min = dt + } + off = (off + len(mstats.PauseNs) - 1) % len(mstats.PauseNs) + } + } + + q := stats.PauseQuantiles + nq := len(q) + if q[0] != min || q[nq-1] != max { + t.Errorf("stats.PauseQuantiles = [%d, ..., %d], want [%d, ..., %d]", q[0], q[nq-1], min, max) + } + + for i := 0; i < nq-1; i++ { + if q[i] > q[i+1] { + t.Errorf("stats.PauseQuantiles[%d]=%d > stats.PauseQuantiles[%d]=%d", i, q[i], i+1, q[i+1]) + } + } + + // compare memory stats with gc stats: + if len(stats.PauseEnd) != n { + t.Fatalf("len(stats.PauseEnd) = %d, want %d", len(stats.PauseEnd), n) + } + off := (int(mstats.NumGC) + len(mstats.PauseEnd) - 1) % len(mstats.PauseEnd) + for i := 0; i < n; i++ { + dt := stats.PauseEnd[i] + if dt.UnixNano() != int64(mstats.PauseEnd[off]) { + t.Errorf("stats.PauseEnd[%d] = %d, want %d", i, dt.UnixNano(), mstats.PauseEnd[off]) + } + off = (off + len(mstats.PauseEnd) - 1) % len(mstats.PauseEnd) + } +} + +var big []byte + +func TestFreeOSMemory(t *testing.T) { + // Tests FreeOSMemory by making big susceptible to collection + // and checking that at least that much memory is returned to + // the OS after. + + const bigBytes = 32 << 20 + big = make([]byte, bigBytes) + + // Make sure any in-progress GCs are complete. + runtime.GC() + + var before runtime.MemStats + runtime.ReadMemStats(&before) + + // Clear the last reference to the big allocation, making it + // susceptible to collection. + big = nil + + // FreeOSMemory runs a GC cycle before releasing memory, + // so it's fine to skip a GC here. + // + // It's possible the background scavenger runs concurrently + // with this function and does most of the work for it. + // If that happens, it's OK. What we want is a test that fails + // often if FreeOSMemory does not work correctly, and a test + // that passes every time if it does. + FreeOSMemory() + + var after runtime.MemStats + runtime.ReadMemStats(&after) + + // Check to make sure that the big allocation (now freed) + // had its memory shift into HeapReleased as a result of that + // FreeOSMemory. + if after.HeapReleased <= before.HeapReleased { + t.Fatalf("no memory released: %d -> %d", before.HeapReleased, after.HeapReleased) + } + + // Check to make sure bigBytes was released, plus some slack. Pages may get + // allocated in between the two measurements above for a variety for reasons, + // most commonly for GC work bufs. Since this can get fairly high, depending + // on scheduling and what GOMAXPROCS is, give a lot of slack up-front. + // + // Add a little more slack too if the page size is bigger than the runtime page size. + // "big" could end up unaligned on its ends, forcing the scavenger to skip at worst + // 2x pages. + slack := uint64(bigBytes / 2) + pageSize := uint64(os.Getpagesize()) + if pageSize > 8<<10 { + slack += pageSize * 2 + } + if slack > bigBytes { + // We basically already checked this. + return + } + if after.HeapReleased-before.HeapReleased < bigBytes-slack { + t.Fatalf("less than %d released: %d -> %d", bigBytes-slack, before.HeapReleased, after.HeapReleased) + } +} + +var ( + setGCPercentBallast any + setGCPercentSink any +) + +func TestSetGCPercent(t *testing.T) { + testenv.SkipFlaky(t, 20076) + + // Test that the variable is being set and returned correctly. + old := SetGCPercent(123) + new := SetGCPercent(old) + if new != 123 { + t.Errorf("SetGCPercent(123); SetGCPercent(x) = %d, want 123", new) + } + + // Test that the percentage is implemented correctly. + defer func() { + SetGCPercent(old) + setGCPercentBallast, setGCPercentSink = nil, nil + }() + SetGCPercent(100) + runtime.GC() + // Create 100 MB of live heap as a baseline. + const baseline = 100 << 20 + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + setGCPercentBallast = make([]byte, baseline-ms.Alloc) + runtime.GC() + runtime.ReadMemStats(&ms) + if abs64(baseline-int64(ms.Alloc)) > 10<<20 { + t.Fatalf("failed to set up baseline live heap; got %d MB, want %d MB", ms.Alloc>>20, baseline>>20) + } + // NextGC should be ~200 MB. + const thresh = 20 << 20 // TODO: Figure out why this is so noisy on some builders + if want := int64(2 * baseline); abs64(want-int64(ms.NextGC)) > thresh { + t.Errorf("NextGC = %d MB, want %d±%d MB", ms.NextGC>>20, want>>20, thresh>>20) + } + // Create some garbage, but not enough to trigger another GC. + for i := 0; i < int(1.2*baseline); i += 1 << 10 { + setGCPercentSink = make([]byte, 1<<10) + } + setGCPercentSink = nil + // Adjust GOGC to 50. NextGC should be ~150 MB. + SetGCPercent(50) + runtime.ReadMemStats(&ms) + if want := int64(1.5 * baseline); abs64(want-int64(ms.NextGC)) > thresh { + t.Errorf("NextGC = %d MB, want %d±%d MB", ms.NextGC>>20, want>>20, thresh>>20) + } + + // Trigger a GC and get back to 100 MB live with GOGC=100. + SetGCPercent(100) + runtime.GC() + // Raise live to 120 MB. + setGCPercentSink = make([]byte, int(0.2*baseline)) + // Lower GOGC to 10. This must force a GC. + runtime.ReadMemStats(&ms) + ngc1 := ms.NumGC + SetGCPercent(10) + // It may require an allocation to actually force the GC. + setGCPercentSink = make([]byte, 1<<20) + runtime.ReadMemStats(&ms) + ngc2 := ms.NumGC + if ngc1 == ngc2 { + t.Errorf("expected GC to run but it did not") + } +} + +func abs64(a int64) int64 { + if a < 0 { + return -a + } + return a +} + +func TestSetMaxThreadsOvf(t *testing.T) { + // Verify that a big threads count will not overflow the int32 + // maxmcount variable, causing a panic (see Issue 16076). + // + // This can only happen when ints are 64 bits, since on platforms + // with 32 bit ints SetMaxThreads (which takes an int parameter) + // cannot be given anything that will overflow an int32. + // + // Call SetMaxThreads with 1<<31, but only on 64 bit systems. + nt := SetMaxThreads(1 << (30 + ^uint(0)>>63)) + SetMaxThreads(nt) // restore previous value +} diff --git a/src/runtime/debug/heapdump_test.go b/src/runtime/debug/heapdump_test.go new file mode 100644 index 0000000..ee6b054 --- /dev/null +++ b/src/runtime/debug/heapdump_test.go @@ -0,0 +1,95 @@ +// Copyright 2014 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 debug_test + +import ( + "os" + "runtime" + . "runtime/debug" + "testing" +) + +func TestWriteHeapDumpNonempty(t *testing.T) { + if runtime.GOOS == "js" { + t.Skipf("WriteHeapDump is not available on %s.", runtime.GOOS) + } + f, err := os.CreateTemp("", "heapdumptest") + if err != nil { + t.Fatalf("TempFile failed: %v", err) + } + defer os.Remove(f.Name()) + defer f.Close() + WriteHeapDump(f.Fd()) + fi, err := f.Stat() + if err != nil { + t.Fatalf("Stat failed: %v", err) + } + const minSize = 1 + if size := fi.Size(); size < minSize { + t.Fatalf("Heap dump size %d bytes, expected at least %d bytes", size, minSize) + } +} + +type Obj struct { + x, y int +} + +func objfin(x *Obj) { + //println("finalized", x) +} + +func TestWriteHeapDumpFinalizers(t *testing.T) { + if runtime.GOOS == "js" { + t.Skipf("WriteHeapDump is not available on %s.", runtime.GOOS) + } + f, err := os.CreateTemp("", "heapdumptest") + if err != nil { + t.Fatalf("TempFile failed: %v", err) + } + defer os.Remove(f.Name()) + defer f.Close() + + // bug 9172: WriteHeapDump couldn't handle more than one finalizer + println("allocating objects") + x := &Obj{} + runtime.SetFinalizer(x, objfin) + y := &Obj{} + runtime.SetFinalizer(y, objfin) + + // Trigger collection of x and y, queueing of their finalizers. + println("starting gc") + runtime.GC() + + // Make sure WriteHeapDump doesn't fail with multiple queued finalizers. + println("starting dump") + WriteHeapDump(f.Fd()) + println("done dump") +} + +type G[T any] struct{} +type I interface { + M() +} + +//go:noinline +func (g G[T]) M() {} + +var dummy I = G[int]{} +var dummy2 I = G[G[int]]{} + +func TestWriteHeapDumpTypeName(t *testing.T) { + if runtime.GOOS == "js" { + t.Skipf("WriteHeapDump is not available on %s.", runtime.GOOS) + } + f, err := os.CreateTemp("", "heapdumptest") + if err != nil { + t.Fatalf("TempFile failed: %v", err) + } + defer os.Remove(f.Name()) + defer f.Close() + WriteHeapDump(f.Fd()) + dummy.M() + dummy2.M() +} diff --git a/src/runtime/debug/mod.go b/src/runtime/debug/mod.go new file mode 100644 index 0000000..a470560 --- /dev/null +++ b/src/runtime/debug/mod.go @@ -0,0 +1,287 @@ +// Copyright 2018 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 debug + +import ( + "fmt" + "runtime" + "strconv" + "strings" +) + +// exported from runtime. +func modinfo() string + +// ReadBuildInfo returns the build information embedded +// in the running binary. The information is available only +// in binaries built with module support. +func ReadBuildInfo() (info *BuildInfo, ok bool) { + data := modinfo() + if len(data) < 32 { + return nil, false + } + data = data[16 : len(data)-16] + bi, err := ParseBuildInfo(data) + if err != nil { + return nil, false + } + + // The go version is stored separately from other build info, mostly for + // historical reasons. It is not part of the modinfo() string, and + // ParseBuildInfo does not recognize it. We inject it here to hide this + // awkwardness from the user. + bi.GoVersion = runtime.Version() + + return bi, true +} + +// BuildInfo represents the build information read from a Go binary. +type BuildInfo struct { + // GoVersion is the version of the Go toolchain that built the binary + // (for example, "go1.19.2"). + GoVersion string + + // Path is the package path of the main package for the binary + // (for example, "golang.org/x/tools/cmd/stringer"). + Path string + + // Main describes the module that contains the main package for the binary. + Main Module + + // Deps describes all the dependency modules, both direct and indirect, + // that contributed packages to the build of this binary. + Deps []*Module + + // Settings describes the build settings used to build the binary. + Settings []BuildSetting +} + +// A Module describes a single module included in a build. +type Module struct { + Path string // module path + Version string // module version + Sum string // checksum + Replace *Module // replaced by this module +} + +// A BuildSetting is a key-value pair describing one setting that influenced a build. +// +// Defined keys include: +// +// - -buildmode: the buildmode flag used (typically "exe") +// - -compiler: the compiler toolchain flag used (typically "gc") +// - CGO_ENABLED: the effective CGO_ENABLED environment variable +// - CGO_CFLAGS: the effective CGO_CFLAGS environment variable +// - CGO_CPPFLAGS: the effective CGO_CPPFLAGS environment variable +// - CGO_CXXFLAGS: the effective CGO_CXXFLAGS environment variable +// - CGO_LDFLAGS: the effective CGO_LDFLAGS environment variable +// - GOARCH: the architecture target +// - GOAMD64/GOARM/GO386/etc: the architecture feature level for GOARCH +// - GOOS: the operating system target +// - vcs: the version control system for the source tree where the build ran +// - vcs.revision: the revision identifier for the current commit or checkout +// - vcs.time: the modification time associated with vcs.revision, in RFC3339 format +// - vcs.modified: true or false indicating whether the source tree had local modifications +type BuildSetting struct { + // Key and Value describe the build setting. + // Key must not contain an equals sign, space, tab, or newline. + // Value must not contain newlines ('\n'). + Key, Value string +} + +// quoteKey reports whether key is required to be quoted. +func quoteKey(key string) bool { + return len(key) == 0 || strings.ContainsAny(key, "= \t\r\n\"`") +} + +// quoteValue reports whether value is required to be quoted. +func quoteValue(value string) bool { + return strings.ContainsAny(value, " \t\r\n\"`") +} + +func (bi *BuildInfo) String() string { + buf := new(strings.Builder) + if bi.GoVersion != "" { + fmt.Fprintf(buf, "go\t%s\n", bi.GoVersion) + } + if bi.Path != "" { + fmt.Fprintf(buf, "path\t%s\n", bi.Path) + } + var formatMod func(string, Module) + formatMod = func(word string, m Module) { + buf.WriteString(word) + buf.WriteByte('\t') + buf.WriteString(m.Path) + buf.WriteByte('\t') + buf.WriteString(m.Version) + if m.Replace == nil { + buf.WriteByte('\t') + buf.WriteString(m.Sum) + } else { + buf.WriteByte('\n') + formatMod("=>", *m.Replace) + } + buf.WriteByte('\n') + } + if bi.Main != (Module{}) { + formatMod("mod", bi.Main) + } + for _, dep := range bi.Deps { + formatMod("dep", *dep) + } + for _, s := range bi.Settings { + key := s.Key + if quoteKey(key) { + key = strconv.Quote(key) + } + value := s.Value + if quoteValue(value) { + value = strconv.Quote(value) + } + fmt.Fprintf(buf, "build\t%s=%s\n", key, value) + } + + return buf.String() +} + +func ParseBuildInfo(data string) (bi *BuildInfo, err error) { + lineNum := 1 + defer func() { + if err != nil { + err = fmt.Errorf("could not parse Go build info: line %d: %w", lineNum, err) + } + }() + + var ( + pathLine = "path\t" + modLine = "mod\t" + depLine = "dep\t" + repLine = "=>\t" + buildLine = "build\t" + newline = "\n" + tab = "\t" + ) + + readModuleLine := func(elem []string) (Module, error) { + if len(elem) != 2 && len(elem) != 3 { + return Module{}, fmt.Errorf("expected 2 or 3 columns; got %d", len(elem)) + } + version := elem[1] + sum := "" + if len(elem) == 3 { + sum = elem[2] + } + return Module{ + Path: elem[0], + Version: version, + Sum: sum, + }, nil + } + + bi = new(BuildInfo) + var ( + last *Module + line string + ok bool + ) + // Reverse of BuildInfo.String(), except for go version. + for len(data) > 0 { + line, data, ok = strings.Cut(data, newline) + if !ok { + break + } + switch { + case strings.HasPrefix(line, pathLine): + elem := line[len(pathLine):] + bi.Path = string(elem) + case strings.HasPrefix(line, modLine): + elem := strings.Split(line[len(modLine):], tab) + last = &bi.Main + *last, err = readModuleLine(elem) + if err != nil { + return nil, err + } + case strings.HasPrefix(line, depLine): + elem := strings.Split(line[len(depLine):], tab) + last = new(Module) + bi.Deps = append(bi.Deps, last) + *last, err = readModuleLine(elem) + if err != nil { + return nil, err + } + case strings.HasPrefix(line, repLine): + elem := strings.Split(line[len(repLine):], tab) + if len(elem) != 3 { + return nil, fmt.Errorf("expected 3 columns for replacement; got %d", len(elem)) + } + if last == nil { + return nil, fmt.Errorf("replacement with no module on previous line") + } + last.Replace = &Module{ + Path: string(elem[0]), + Version: string(elem[1]), + Sum: string(elem[2]), + } + last = nil + case strings.HasPrefix(line, buildLine): + kv := line[len(buildLine):] + if len(kv) < 1 { + return nil, fmt.Errorf("build line missing '='") + } + + var key, rawValue string + switch kv[0] { + case '=': + return nil, fmt.Errorf("build line with missing key") + + case '`', '"': + rawKey, err := strconv.QuotedPrefix(kv) + if err != nil { + return nil, fmt.Errorf("invalid quoted key in build line") + } + if len(kv) == len(rawKey) { + return nil, fmt.Errorf("build line missing '=' after quoted key") + } + if c := kv[len(rawKey)]; c != '=' { + return nil, fmt.Errorf("unexpected character after quoted key: %q", c) + } + key, _ = strconv.Unquote(rawKey) + rawValue = kv[len(rawKey)+1:] + + default: + var ok bool + key, rawValue, ok = strings.Cut(kv, "=") + if !ok { + return nil, fmt.Errorf("build line missing '=' after key") + } + if quoteKey(key) { + return nil, fmt.Errorf("unquoted key %q must be quoted", key) + } + } + + var value string + if len(rawValue) > 0 { + switch rawValue[0] { + case '`', '"': + var err error + value, err = strconv.Unquote(rawValue) + if err != nil { + return nil, fmt.Errorf("invalid quoted value in build line") + } + + default: + value = rawValue + if quoteValue(value) { + return nil, fmt.Errorf("unquoted value %q must be quoted", value) + } + } + } + + bi.Settings = append(bi.Settings, BuildSetting{Key: key, Value: value}) + } + lineNum++ + } + return bi, nil +} diff --git a/src/runtime/debug/mod_test.go b/src/runtime/debug/mod_test.go new file mode 100644 index 0000000..b291769 --- /dev/null +++ b/src/runtime/debug/mod_test.go @@ -0,0 +1,75 @@ +// Copyright 2022 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 debug_test + +import ( + "reflect" + "runtime/debug" + "strings" + "testing" +) + +// strip removes two leading tabs after each newline of s. +func strip(s string) string { + replaced := strings.ReplaceAll(s, "\n\t\t", "\n") + if len(replaced) > 0 && replaced[0] == '\n' { + replaced = replaced[1:] + } + return replaced +} + +func FuzzParseBuildInfoRoundTrip(f *testing.F) { + // Package built from outside a module, missing some fields.. + f.Add(strip(` + path rsc.io/fortune + mod rsc.io/fortune v1.0.0 + `)) + + // Package built from the standard library, missing some fields.. + f.Add(`path cmd/test2json`) + + // Package built from inside a module. + f.Add(strip(` + go 1.18 + path example.com/m + mod example.com/m (devel) + build -compiler=gc + `)) + + // Package built in GOPATH mode. + f.Add(strip(` + go 1.18 + path example.com/m + build -compiler=gc + `)) + + // Escaped build info. + f.Add(strip(` + go 1.18 + path example.com/m + build CRAZY_ENV="requires\nescaping" + `)) + + f.Fuzz(func(t *testing.T, s string) { + bi, err := debug.ParseBuildInfo(s) + if err != nil { + // Not a round-trippable BuildInfo string. + t.Log(err) + return + } + + // s2 could have different escaping from s. + // However, it should parse to exactly the same contents. + s2 := bi.String() + bi2, err := debug.ParseBuildInfo(s2) + if err != nil { + t.Fatalf("%v:\n%s", err, s2) + } + + if !reflect.DeepEqual(bi2, bi) { + t.Fatalf("Parsed representation differs.\ninput:\n%s\noutput:\n%s", s, s2) + } + }) +} diff --git a/src/runtime/debug/panic_test.go b/src/runtime/debug/panic_test.go new file mode 100644 index 0000000..ec5294c --- /dev/null +++ b/src/runtime/debug/panic_test.go @@ -0,0 +1,56 @@ +// Copyright 2020 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. + +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd + +// TODO: test on Windows? + +package debug_test + +import ( + "runtime" + "runtime/debug" + "syscall" + "testing" + "unsafe" +) + +func TestPanicOnFault(t *testing.T) { + if runtime.GOARCH == "s390x" { + t.Skip("s390x fault addresses are missing the low order bits") + } + if runtime.GOOS == "ios" { + t.Skip("iOS doesn't provide fault addresses") + } + if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm" { + t.Skip("netbsd-arm doesn't provide fault address (golang.org/issue/45026)") + } + m, err := syscall.Mmap(-1, 0, 0x1000, syscall.PROT_READ /* Note: no PROT_WRITE */, syscall.MAP_SHARED|syscall.MAP_ANON) + if err != nil { + t.Fatalf("can't map anonymous memory: %s", err) + } + defer syscall.Munmap(m) + old := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(old) + const lowBits = 0x3e7 + defer func() { + r := recover() + if r == nil { + t.Fatalf("write did not fault") + } + type addressable interface { + Addr() uintptr + } + a, ok := r.(addressable) + if !ok { + t.Fatalf("fault does not contain address") + } + want := uintptr(unsafe.Pointer(&m[lowBits])) + got := a.Addr() + if got != want { + t.Fatalf("fault address %x, want %x", got, want) + } + }() + m[lowBits] = 1 // will fault +} diff --git a/src/runtime/debug/stack.go b/src/runtime/debug/stack.go new file mode 100644 index 0000000..3999840 --- /dev/null +++ b/src/runtime/debug/stack.go @@ -0,0 +1,30 @@ +// Copyright 2011 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 debug contains facilities for programs to debug themselves while +// they are running. +package debug + +import ( + "os" + "runtime" +) + +// PrintStack prints to standard error the stack trace returned by runtime.Stack. +func PrintStack() { + os.Stderr.Write(Stack()) +} + +// Stack returns a formatted stack trace of the goroutine that calls it. +// It calls [runtime.Stack] with a large enough buffer to capture the entire trace. +func Stack() []byte { + buf := make([]byte, 1024) + for { + n := runtime.Stack(buf, false) + if n < len(buf) { + return buf[:n] + } + buf = make([]byte, 2*len(buf)) + } +} diff --git a/src/runtime/debug/stack_test.go b/src/runtime/debug/stack_test.go new file mode 100644 index 0000000..671057c --- /dev/null +++ b/src/runtime/debug/stack_test.go @@ -0,0 +1,121 @@ +// Copyright 2011 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 debug_test + +import ( + "bytes" + "fmt" + "internal/testenv" + "os" + "os/exec" + "path/filepath" + "runtime" + . "runtime/debug" + "strings" + "testing" +) + +func TestMain(m *testing.M) { + if os.Getenv("GO_RUNTIME_DEBUG_TEST_DUMP_GOROOT") != "" { + fmt.Println(runtime.GOROOT()) + os.Exit(0) + } + os.Exit(m.Run()) +} + +type T int + +func (t *T) ptrmethod() []byte { + return Stack() +} +func (t T) method() []byte { + return t.ptrmethod() +} + +/* +The traceback should look something like this, modulo line numbers and hex constants. +Don't worry much about the base levels, but check the ones in our own package. + + goroutine 10 [running]: + runtime/debug.Stack(0x0, 0x0, 0x0) + /Users/r/go/src/runtime/debug/stack.go:28 +0x80 + runtime/debug.(*T).ptrmethod(0xc82005ee70, 0x0, 0x0, 0x0) + /Users/r/go/src/runtime/debug/stack_test.go:15 +0x29 + runtime/debug.T.method(0x0, 0x0, 0x0, 0x0) + /Users/r/go/src/runtime/debug/stack_test.go:18 +0x32 + runtime/debug.TestStack(0xc8201ce000) + /Users/r/go/src/runtime/debug/stack_test.go:37 +0x38 + testing.tRunner(0xc8201ce000, 0x664b58) + /Users/r/go/src/testing/testing.go:456 +0x98 + created by testing.RunTests + /Users/r/go/src/testing/testing.go:561 +0x86d +*/ +func TestStack(t *testing.T) { + b := T(0).method() + lines := strings.Split(string(b), "\n") + if len(lines) < 6 { + t.Fatal("too few lines") + } + + // If built with -trimpath, file locations should start with package paths. + // Otherwise, file locations should start with a GOROOT/src prefix + // (for whatever value of GOROOT is baked into the binary, not the one + // that may be set in the environment). + fileGoroot := "" + if envGoroot := os.Getenv("GOROOT"); envGoroot != "" { + // Since GOROOT is set explicitly in the environment, we can't be certain + // that it is the same GOROOT value baked into the binary, and we can't + // change the value in-process because runtime.GOROOT uses the value from + // initial (not current) environment. Spawn a subprocess to determine the + // real baked-in GOROOT. + t.Logf("found GOROOT %q from environment; checking embedded GOROOT value", envGoroot) + testenv.MustHaveExec(t) + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + cmd := exec.Command(exe) + cmd.Env = append(os.Environ(), "GOROOT=", "GO_RUNTIME_DEBUG_TEST_DUMP_GOROOT=1") + out, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + fileGoroot = string(bytes.TrimSpace(out)) + } else { + // Since GOROOT is not set in the environment, its value (if any) must come + // from the path embedded in the binary. + fileGoroot = runtime.GOROOT() + } + filePrefix := "" + if fileGoroot != "" { + filePrefix = filepath.ToSlash(fileGoroot) + "/src/" + } + + n := 0 + frame := func(file, code string) { + t.Helper() + + line := lines[n] + if !strings.Contains(line, code) { + t.Errorf("expected %q in %q", code, line) + } + n++ + + line = lines[n] + + wantPrefix := "\t" + filePrefix + file + if !strings.HasPrefix(line, wantPrefix) { + t.Errorf("in line %q, expected prefix %q", line, wantPrefix) + } + n++ + } + n++ + + frame("runtime/debug/stack.go", "runtime/debug.Stack") + frame("runtime/debug/stack_test.go", "runtime/debug_test.(*T).ptrmethod") + frame("runtime/debug/stack_test.go", "runtime/debug_test.T.method") + frame("runtime/debug/stack_test.go", "runtime/debug_test.TestStack") + frame("testing/testing.go", "") +} diff --git a/src/runtime/debug/stubs.go b/src/runtime/debug/stubs.go new file mode 100644 index 0000000..913d4b9 --- /dev/null +++ b/src/runtime/debug/stubs.go @@ -0,0 +1,18 @@ +// Copyright 2014 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 debug + +import ( + "time" +) + +// Implemented in package runtime. +func readGCStats(*[]time.Duration) +func freeOSMemory() +func setMaxStack(int) int +func setGCPercent(int32) int32 +func setPanicOnFault(bool) bool +func setMaxThreads(int) int +func setMemoryLimit(int64) int64 diff --git a/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/5501685e611fa764 b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/5501685e611fa764 new file mode 100644 index 0000000..4ab5d92 --- /dev/null +++ b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/5501685e611fa764 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("mod\t\t0\n") diff --git a/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/71634114e78567cf b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/71634114e78567cf new file mode 100644 index 0000000..741c4df --- /dev/null +++ b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/71634114e78567cf @@ -0,0 +1,2 @@ +go test fuzz v1 +string("mod\t0\t\n") diff --git a/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/c73dce23c1f2494c b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/c73dce23c1f2494c new file mode 100644 index 0000000..60f9338 --- /dev/null +++ b/src/runtime/debug/testdata/fuzz/FuzzParseBuildInfoRoundTrip/c73dce23c1f2494c @@ -0,0 +1,2 @@ +go test fuzz v1 +string("build\t0=\" 0\"\n") |