src/cmd/compile/internal/base/hashdebug.go GO 422 lines View on github.com → Search inside
1// Copyright 2022 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package base67import (8	"bytes"9	"cmd/internal/obj"10	"cmd/internal/src"11	"fmt"12	"internal/bisect"13	"io"14	"os"15	"path/filepath"16	"strconv"17	"strings"18	"sync"19)2021type hashAndMask struct {22	// a hash h matches if (h^hash)&mask == 023	hash uint6424	mask uint6425	name string // base name, or base name + "0", "1", etc.26}2728type HashDebug struct {29	mu   sync.Mutex // for logfile, posTmp, bytesTmp30	name string     // base name of the flag/variable.31	// what file (if any) receives the yes/no logging?32	// default is os.Stdout33	logfile          io.Writer34	posTmp           []src.Pos35	bytesTmp         bytes.Buffer36	matches          []hashAndMask // A hash matches if one of these matches.37	excludes         []hashAndMask // explicitly excluded hash suffixes38	bisect           *bisect.Matcher39	fileSuffixOnly   bool // for Pos hashes, remove the directory prefix.40	inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position.41}4243// SetInlineSuffixOnly controls whether hashing and reporting use the entire44// inline position, or just the most-inline suffix.  Compiler debugging tends45// to want the whole inlining, debugging user problems (loopvarhash, e.g.)46// typically does not need to see the entire inline tree, there is just one47// copy of the source code.48func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug {49	d.inlineSuffixOnly = b50	return d51}5253// The default compiler-debugging HashDebug, for "-d=gossahash=..."54var hashDebug *HashDebug5556var ConvertHash *HashDebug      // for debugging float-to-[u]int conversion changes57var FmaHash *HashDebug          // for debugging fused-multiply-add floating point changes58var LoopVarHash *HashDebug      // for debugging shared/private loop variable changes59var PGOHash *HashDebug          // for debugging PGO optimization decisions60var LiteralAllocHash *HashDebug // for debugging literal allocation optimizations61var MergeLocalsHash *HashDebug  // for debugging local stack slot merging changes62var VariableMakeHash *HashDebug // for debugging variable-sized make optimizations6364// DebugHashMatchPkgFunc reports whether debug variable Gossahash65//66//  1. is empty (returns true; this is a special more-quickly implemented case of 4 below)67//68//  2. is "y" or "Y" (returns true)69//70//  3. is "n" or "N" (returns false)71//72//  4. does not explicitly exclude the sha1 hash of pkgAndName (see step 6)73//74//  5. is a suffix of the sha1 hash of pkgAndName (returns true)75//76//  6. OR77//     if the (non-empty) value is in the regular language78//     "(-[01]+/)+?([01]+(/[01]+)+?"79//     (exclude..)(....include...)80//     test the [01]+ exclude substrings, if any suffix-match, return false (4 above)81//     test the [01]+ include substrings, if any suffix-match, return true82//     The include substrings AFTER the first slash are numbered 0,1, etc and83//     are named fmt.Sprintf("%s%d", varname, number)84//     As an extra-special case for multiple failure search,85//     an excludes-only string ending in a slash (terminated, not separated)86//     implicitly specifies the include string "0/1", that is, match everything.87//     (Exclude strings are used for automated search for multiple failures.)88//     Clause 6 is not really intended for human use and only89//     matters for failures that require multiple triggers.90//91// Otherwise it returns false.92//93// Unless Flags.Gossahash is empty, when DebugHashMatchPkgFunc returns true the message94//95//	"%s triggered %s\n", varname, pkgAndName96//97// is printed on the file named in environment variable GSHS_LOGFILE,98// or standard out if that is empty.  "Varname" is either the name of99// the variable or the name of the substring, depending on which matched.100//101// Typical use:102//103//  1. you make a change to the compiler, say, adding a new phase104//105//  2. it is broken in some mystifying way, for example, make.bash builds a broken106//     compiler that almost works, but crashes compiling a test in run.bash.107//108//  3. add this guard to the code, which by default leaves it broken, but does not109//     run the broken new code if Flags.Gossahash is non-empty and non-matching:110//111//     if !base.DebugHashMatch(ir.PkgFuncName(fn)) {112//     return nil // early exit, do nothing113//     }114//115//  4. rebuild w/o the bad code,116//     GOCOMPILEDEBUG=gossahash=n ./all.bash117//     to verify that you put the guard in the right place with the right sense of the test.118//119//  5. use github.com/dr2chase/gossahash to search for the error:120//121//     go install github.com/dr2chase/gossahash@latest122//123//     gossahash -- <the thing that fails>124//125//     for example: GOMAXPROCS=1 gossahash -- ./all.bash126//127//  6. gossahash should return a single function whose miscompilation128//     causes the problem, and you can focus on that.129func DebugHashMatchPkgFunc(pkg, fn string) bool {130	return hashDebug.MatchPkgFunc(pkg, fn, nil)131}132133func DebugHashMatchPos(pos src.XPos) bool {134	return hashDebug.MatchPos(pos, nil)135}136137// HasDebugHash returns true if Flags.Gossahash is non-empty, which138// results in hashDebug being not-nil.  I.e., if !HasDebugHash(),139// there is no need to create the string for hashing and testing.140func HasDebugHash() bool {141	return hashDebug != nil142}143144// TODO: Delete when we switch to bisect-only.145func toHashAndMask(s, varname string) hashAndMask {146	l := len(s)147	if l > 64 {148		s = s[l-64:]149		l = 64150	}151	m := ^(^uint64(0) << l)152	h, err := strconv.ParseUint(s, 2, 64)153	if err != nil {154		Fatalf("Could not parse %s (=%s) as a binary number", varname, s)155	}156157	return hashAndMask{name: varname, hash: h, mask: m}158}159160// NewHashDebug returns a new hash-debug tester for the161// environment variable ev.  If ev is not set, it returns162// nil, allowing a lightweight check for normal-case behavior.163func NewHashDebug(ev, s string, file io.Writer) *HashDebug {164	if s == "" {165		return nil166	}167168	hd := &HashDebug{name: ev, logfile: file}169	if !strings.Contains(s, "/") {170		m, err := bisect.New(s)171		if err != nil {172			Fatalf("%s: %v", ev, err)173		}174		hd.bisect = m175		return hd176	}177178	// TODO: Delete remainder of function when we switch to bisect-only.179	ss := strings.Split(s, "/")180	// first remove any leading exclusions; these are preceded with "-"181	i := 0182	for len(ss) > 0 {183		s := ss[0]184		if len(s) == 0 || len(s) > 0 && s[0] != '-' {185			break186		}187		ss = ss[1:]188		hd.excludes = append(hd.excludes, toHashAndMask(s[1:], fmt.Sprintf("%s%d", "HASH_EXCLUDE", i)))189		i++190	}191	// hash searches may use additional EVs with 0, 1, 2, ... suffixes.192	i = 0193	for _, s := range ss {194		if s == "" {195			if i != 0 || len(ss) > 1 && ss[1] != "" || len(ss) > 2 {196				Fatalf("Empty hash match string for %s should be first (and only) one", ev)197			}198			// Special case of should match everything.199			hd.matches = append(hd.matches, toHashAndMask("0", fmt.Sprintf("%s0", ev)))200			hd.matches = append(hd.matches, toHashAndMask("1", fmt.Sprintf("%s1", ev)))201			break202		}203		if i == 0 {204			hd.matches = append(hd.matches, toHashAndMask(s, ev))205		} else {206			hd.matches = append(hd.matches, toHashAndMask(s, fmt.Sprintf("%s%d", ev, i-1)))207		}208		i++209	}210	return hd211}212213// TODO: Delete when we switch to bisect-only.214func (d *HashDebug) excluded(hash uint64) bool {215	for _, m := range d.excludes {216		if (m.hash^hash)&m.mask == 0 {217			return true218		}219	}220	return false221}222223// TODO: Delete when we switch to bisect-only.224func hashString(hash uint64) string {225	hstr := ""226	if hash == 0 {227		hstr = "0"228	} else {229		for ; hash != 0; hash = hash >> 1 {230			hstr = string('0'+byte(hash&1)) + hstr231		}232	}233	if len(hstr) > 24 {234		hstr = hstr[len(hstr)-24:]235	}236	return hstr237}238239// TODO: Delete when we switch to bisect-only.240func (d *HashDebug) match(hash uint64) *hashAndMask {241	for i, m := range d.matches {242		if (m.hash^hash)&m.mask == 0 {243			return &d.matches[i]244		}245	}246	return nil247}248249// MatchPkgFunc returns true if either the variable used to create d is250// unset, or if its value is y, or if it is a suffix of the base-two251// representation of the hash of pkg and fn.  If the variable is not nil,252// then a true result is accompanied by stylized output to d.logfile, which253// is used for automated bug search.254func (d *HashDebug) MatchPkgFunc(pkg, fn string, note func() string) bool {255	if d == nil {256		return true257	}258	// Written this way to make inlining likely.259	return d.matchPkgFunc(pkg, fn, note)260}261262func (d *HashDebug) matchPkgFunc(pkg, fn string, note func() string) bool {263	hash := bisect.Hash(pkg, fn)264	return d.matchAndLog(hash, func() string { return pkg + "." + fn }, note)265}266267// MatchPos is similar to MatchPkgFunc, but for hash computation268// it uses the source position including all inlining information instead of269// package name and path.270// Note that the default answer for no environment variable (d == nil)271// is "yes", do the thing.272func (d *HashDebug) MatchPos(pos src.XPos, desc func() string) bool {273	if d == nil {274		return true275	}276	// Written this way to make inlining likely.277	return d.matchPos(Ctxt, pos, desc)278}279280func (d *HashDebug) matchPos(ctxt *obj.Link, pos src.XPos, note func() string) bool {281	return d.matchPosWithInfo(ctxt, pos, nil, note)282}283284func (d *HashDebug) matchPosWithInfo(ctxt *obj.Link, pos src.XPos, info any, note func() string) bool {285	hash := d.hashPos(ctxt, pos)286	if info != nil {287		hash = bisect.Hash(hash, info)288	}289	return d.matchAndLog(hash,290		func() string {291			r := d.fmtPos(ctxt, pos)292			if info != nil {293				r += fmt.Sprintf(" (%v)", info)294			}295			return r296		},297		note)298}299300// MatchPosWithInfo is similar to MatchPos, but with additional information301// that is included for hash computation, so it can distinguish multiple302// matches on the same source location.303// Note that the default answer for no environment variable (d == nil)304// is "yes", do the thing.305func (d *HashDebug) MatchPosWithInfo(pos src.XPos, info any, desc func() string) bool {306	if d == nil {307		return true308	}309	// Written this way to make inlining likely.310	return d.matchPosWithInfo(Ctxt, pos, info, desc)311}312313// matchAndLog is the core matcher. It reports whether the hash matches the pattern.314// If a report needs to be printed, match prints that report to the log file.315// The text func must be non-nil and should return a user-readable316// representation of what was hashed. The note func may be nil; if non-nil,317// it should return additional information to display to the user when this318// change is selected.319func (d *HashDebug) matchAndLog(hash uint64, text, note func() string) bool {320	if d.bisect != nil {321		enabled := d.bisect.ShouldEnable(hash)322		if d.bisect.ShouldPrint(hash) {323			disabled := ""324			if !enabled {325				disabled = " [DISABLED]"326			}327			var t string328			if !d.bisect.MarkerOnly() {329				t = text()330				if note != nil {331					if n := note(); n != "" {332						t += ": " + n + disabled333						disabled = ""334					}335				}336			}337			d.log(d.name, hash, strings.TrimSpace(t+disabled))338		}339		return enabled340	}341342	// TODO: Delete rest of function body when we switch to bisect-only.343	if d.excluded(hash) {344		return false345	}346	if m := d.match(hash); m != nil {347		d.log(m.name, hash, text())348		return true349	}350	return false351}352353// short returns the form of file name to use for d.354// The default is the full path, but fileSuffixOnly selects355// just the final path element.356func (d *HashDebug) short(name string) string {357	if d.fileSuffixOnly {358		return filepath.Base(name)359	}360	return name361}362363// hashPos returns a hash of the position pos, including its entire inline stack.364// If d.inlineSuffixOnly is true, hashPos only considers the innermost (leaf) position on the inline stack.365func (d *HashDebug) hashPos(ctxt *obj.Link, pos src.XPos) uint64 {366	if d.inlineSuffixOnly {367		p := ctxt.InnermostPos(pos)368		return bisect.Hash(d.short(p.Filename()), p.Line(), p.Col())369	}370	h := bisect.Hash()371	ctxt.AllPos(pos, func(p src.Pos) {372		h = bisect.Hash(h, d.short(p.Filename()), p.Line(), p.Col())373	})374	return h375}376377// fmtPos returns a textual formatting of the position pos, including its entire inline stack.378// If d.inlineSuffixOnly is true, fmtPos only considers the innermost (leaf) position on the inline stack.379func (d *HashDebug) fmtPos(ctxt *obj.Link, pos src.XPos) string {380	format := func(p src.Pos) string {381		return fmt.Sprintf("%s:%d:%d", d.short(p.Filename()), p.Line(), p.Col())382	}383	if d.inlineSuffixOnly {384		return format(ctxt.InnermostPos(pos))385	}386	var stk []string387	ctxt.AllPos(pos, func(p src.Pos) {388		stk = append(stk, format(p))389	})390	return strings.Join(stk, "; ")391}392393// log prints a match with the given hash and textual formatting.394// TODO: Delete varname parameter when we switch to bisect-only.395func (d *HashDebug) log(varname string, hash uint64, text string) {396	d.mu.Lock()397	defer d.mu.Unlock()398399	file := d.logfile400	if file == nil {401		if tmpfile := os.Getenv("GSHS_LOGFILE"); tmpfile != "" {402			var err error403			file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)404			if err != nil {405				Fatalf("could not open hash-testing logfile %s", tmpfile)406				return407			}408		}409		if file == nil {410			file = os.Stdout411		}412		d.logfile = file413	}414415	// Bisect output.416	fmt.Fprintf(file, "%s %s\n", text, bisect.Marker(hash))417418	// Gossahash output.419	// TODO: Delete rest of function when we switch to bisect-only.420	fmt.Fprintf(file, "%s triggered %s %s\n", varname, text, hashString(hash))421}

Code quality findings 14

Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
hd.excludes = append(hd.excludes, toHashAndMask(s[1:], fmt.Sprintf("%s%d", "HASH_EXCLUDE", i)))
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
hd.matches = append(hd.matches, toHashAndMask("0", fmt.Sprintf("%s0", ev)))
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
hd.matches = append(hd.matches, toHashAndMask("1", fmt.Sprintf("%s1", ev)))
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
hd.matches = append(hd.matches, toHashAndMask(s, ev))
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
hd.matches = append(hd.matches, toHashAndMask(s, fmt.Sprintf("%s%d", ev, i-1)))
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, m := range d.matches {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if d.bisect.ShouldPrint(hash) {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if !enabled {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if !d.bisect.MarkerOnly() {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if note != nil {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
stk = append(stk, format(p))

Security findings 1

Ensure restrictive umask values
security permissive-file-mode
file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.