obfuscate unexported names like exported ones (#227)

In 90fa325da7, the obfuscation logic was changed to use hashes for
exported names, but incremental names starting at just one letter for
unexported names. Presumably, this was done for the sake of binary size.

I argue that this is not a good idea for the default mode for a number
of reasons:

1) It makes reversing of stack traces nearly impossible for unexported
   names, since replacing an obfuscated name "c" with "originalName"
   would trigger too many false positives by matching single characters.

2) Exported and unexported names aren't different. We need to know how
   names were obfuscated at a later time in both cases, thanks to use
   cases like -ldflags=-X. Using short names for one but not the other
   doesn't make a lot of sense, and makes the logic inconsistent.

3) Shaving off three bytes for unexported names doesn't seem like a huge
   deal for the default mode, when we already have -tiny to optimize for
   size.

This saves us a bit of work, but most importantly, simplifies the
obfuscation state as we no longer need to carry privateNameMap between
the compile and link stages.

	name     old time/op       new time/op       delta
	Build-8        153ms ± 2%        150ms ± 2%    ~     (p=0.065 n=6+6)

	name     old bin-B         new bin-B         delta
	Build-8        7.09M ± 0%        7.08M ± 0%  -0.24%  (p=0.002 n=6+6)

	name     old sys-time/op   new sys-time/op   delta
	Build-8        296ms ± 5%        277ms ± 6%  -6.50%  (p=0.026 n=6+6)

	name     old user-time/op  new user-time/op  delta
	Build-8        562ms ± 1%        558ms ± 3%    ~     (p=0.329 n=5+6)

Note that I do not oppose using short names for both exported and
unexported names in the future for -tiny, since reversing of stack
traces will by design not work there. The code can be resurrected from
the git history if we want to improve -tiny that way in the future, as
we'd need to store state in header files again.

Another major cleanup we can do here is to no longer use the
garbledImports map. From a look at obfuscateImports, we hash a package's
import path with its action ID, much like exported names, so we can
simply re-do that hashing for the linker's -X flag.

garbledImports does have some logic to handle duplicate package names,
but it's worth noting that should not affect package paths, as they are
always unique. That area of code could probably do with some
simplification in the future, too.

While at it, make hashWith panic if either parameter is empty.
obfuscateImports was hashing the main package path without a salt due to
a bug, so we want to catch those in the future.

Finally, make some tiny spacing and typo tweaks to the README.
pull/229/head
Daniel Martí 3 years ago committed by GitHub
parent d8e8738216
commit 79c775e218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,7 +31,7 @@ order to:
* Strip filenames and shuffle position information
* Strip debugging information and symbol tables
* Obfuscate literals, if the `-literals` flag is given
* Removes [extra information](#tiny-mode) if the `-tiny` flag is given
* Remove [extra information](#tiny-mode) if the `-tiny` flag is given
### Options
@ -59,22 +59,23 @@ to document the current shortcomings of this tool.
Command string
Args string
}
// never obfuscate the Message type
var _ = reflect.TypeOf(Message{})
```
### Tiny Mode
When the `-tiny` flag is passed, extra information is stripped from the resulting
Go binary. This includes line numbers, filenames, and code in the runtime the
prints panics, fatal errors, and trace/debug info. All in all this can make binaries
When the `-tiny` flag is passed, extra information is stripped from the resulting
Go binary. This includes line numbers, filenames, and code in the runtime the
prints panics, fatal errors, and trace/debug info. All in all this can make binaries
6-10% smaller in our testing.
Note: if `-tiny` is passed, no panics, fatal errors will ever be printed, but they can
still be handled internally with `recover` as normal. In addition, the `GODEBUG`
still be handled internally with `recover` as normal. In addition, the `GODEBUG`
environmental variable will be ignored.
### Contributing
We actively seek new contributors, if you would like to contribute to garble use the
We actively seek new contributors, if you would like to contribute to garble use the
[CONTRIBUTING.md](CONTRIBUTING.md) as a starting point.

@ -45,8 +45,8 @@ func BenchmarkBuild(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
cmd := exec.Command(garbleBin, "build", "./testdata/bench")
if err := cmd.Run(); err != nil {
b.Fatal(err)
if out, err := cmd.CombinedOutput(); err != nil {
b.Fatalf("%v: %s", err, out)
}
atomic.AddInt64(&n, 1)
@ -56,4 +56,9 @@ func BenchmarkBuild(b *testing.B) {
})
b.ReportMetric(float64(userTime)/float64(n), "user-ns/op")
b.ReportMetric(float64(systemTime)/float64(n), "sys-ns/op")
info, err := os.Stat(garbleBin)
if err != nil {
b.Fatal(err)
}
b.ReportMetric(float64(info.Size()), "bin-B")
}

@ -13,7 +13,6 @@ import (
"os"
"os/exec"
"strings"
"unicode"
)
const buildIDSeparator = "/"
@ -143,6 +142,12 @@ func buildidOf(path string) (string, error) {
}
func hashWith(salt []byte, name string) string {
if len(salt) == 0 {
panic("hashWith: empty salt")
}
if name == "" {
panic("hashWith: empty name")
}
const length = 4
d := sha256.New()
@ -156,37 +161,3 @@ func hashWith(salt []byte, name string) string {
}
return "z" + sum[:length]
}
func buildNameCharset() []rune {
var charset []rune
for _, r := range unicode.Letter.R16 {
for c := r.Lo; c <= r.Hi; c += r.Stride {
charset = append(charset, rune(c))
}
}
for _, r := range unicode.Digit.R16 {
for c := r.Lo; c <= r.Hi; c += r.Stride {
charset = append(charset, rune(c))
}
}
return charset
}
var privateNameCharset = buildNameCharset()
func encodeIntToName(i int) string {
builder := strings.Builder{}
for i > 0 {
charIdx := i % len(privateNameCharset)
i -= charIdx + 1
c := privateNameCharset[charIdx]
if builder.Len() == 0 && !unicode.IsLetter(c) {
builder.WriteByte('_')
}
builder.WriteRune(c)
}
return builder.String()
}

@ -8,7 +8,6 @@ import (
"bufio"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -59,22 +58,6 @@ type privateName struct {
seed []byte
}
func appendPrivateNameMap(pkg *goobj2.Package, nameMap map[string]string) error {
for _, member := range pkg.ArchiveMembers {
if member.ArchiveHeader.Name != headerPrivateNameMap {
continue
}
serializedMap := member.ArchiveHeader.Data
serializedMap = serializedMap[:bytes.IndexByte(serializedMap, 0x00)]
if err := json.Unmarshal(serializedMap, &nameMap); err != nil {
return err
}
return nil
}
return nil
}
// extractDebugObfSrc extracts obfuscated sources from object files if -debugdir flag is enabled.
func extractDebugObfSrc(pkgPath string, pkg *goobj2.Package) error {
if opts.DebugDir == "" {
@ -153,21 +136,20 @@ func extractDebugObfSrc(pkgPath string, pkg *goobj2.Package) error {
// It returns the path to the modified main object file, to be used for linking.
// We also return a map of how the imports were garbled, as well as the private
// name map recovered from the archive files, so that we can amend -X flags.
func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj string, garbledImports, privateNameMap map[string]string, _ error) {
func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj string, _ error) {
mainPkg, err := goobj2.Parse(objPath, "main", importMap)
if err != nil {
return "", nil, nil, fmt.Errorf("error parsing main objfile: %v", err)
return "", fmt.Errorf("error parsing main objfile: %v", err)
}
if err := extractDebugObfSrc("main", mainPkg); err != nil {
return "", nil, nil, err
return "", err
}
pkgs := []pkgInfo{{mainPkg, objPath, true}}
privateNameMap = make(map[string]string)
// build list of imported packages that are private
importCfg, err := goobj2.ParseImportCfg(curImportCfg)
if err != nil {
return "", nil, nil, err
return "", err
}
for pkgPath, info := range importCfg.Packages {
// if the '-tiny' flag is passed, we will strip filename
@ -175,18 +157,15 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st
if private := isPrivate(pkgPath); opts.Tiny || private {
pkg, err := goobj2.Parse(info.Path, pkgPath, importMap)
if err != nil {
return "", nil, nil, fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err)
return "", fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err)
}
pkgs = append(pkgs, pkgInfo{pkg, info.Path, private})
if err := appendPrivateNameMap(pkg, privateNameMap); err != nil {
return "", nil, nil, fmt.Errorf("error parsing name map %s at %s: %v", pkgPath, info.Path, err)
}
// Avoiding double extraction from main object file
if objPath != info.Path {
if err := extractDebugObfSrc(pkgPath, pkg); err != nil {
return "", nil, nil, err
return "", err
}
}
}
@ -195,7 +174,6 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st
var sb strings.Builder
var buf bytes.Buffer
garbledImports = make(map[string]string)
replacedFiles := make(map[string]string)
for _, p := range pkgs {
// log.Printf("++ Obfuscating object file for %s ++", p.pkg.ImportPath)
@ -233,7 +211,7 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st
privImports.privatePaths = append(privImports.privatePaths, privPaths...)
privImports.privateNames = append(privImports.privateNames, privNames...)
return hashImport(imp, nil, garbledImports)
return hashImport(imp, nil)
}
for i := range am.Imports {
@ -268,7 +246,7 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st
// log.Printf("\t== Private imports: %v ==\n", privImports)
// garble all private import paths in all symbol names
garbleSymbols(&am, privImports, garbledImports, &buf, &sb)
garbleSymbols(&am, privImports, &buf, &sb)
}
// An archive under the temporary file. Note that
@ -276,22 +254,22 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st
// simply use its name after closing the file.
tempObjFile, err := ioutil.TempFile(sharedTempDir, "pkg.*.a")
if err != nil {
return "", nil, nil, fmt.Errorf("creating temp file: %v", err)
return "", fmt.Errorf("creating temp file: %v", err)
}
tempObj := tempObjFile.Name()
tempObjFile.Close()
if err := p.pkg.Write(tempObj); err != nil {
return "", nil, nil, fmt.Errorf("error writing objfile %s at %s: %v", p.pkg.ImportPath, p.path, err)
return "", fmt.Errorf("error writing objfile %s at %s: %v", p.pkg.ImportPath, p.path, err)
}
replacedFiles[p.path] = tempObj
}
// garble importcfg so the linker knows where to find garbled imports
if err := garbleImportCfg(curImportCfg, importCfg, garbledImports, replacedFiles); err != nil {
return "", nil, nil, err
if err := garbleImportCfg(curImportCfg, importCfg, replacedFiles); err != nil {
return "", err
}
return replacedFiles[objPath], garbledImports, privateNameMap, nil
return replacedFiles[objPath], nil
}
// stripPCLinesAndNames removes all filename and position info
@ -446,23 +424,23 @@ func dedupPrivateNames(names []privateName) []privateName {
return names[:j]
}
func hashImport(pkg string, seed []byte, garbledImports map[string]string) string {
if seed == nil {
if garbledPkg, ok := garbledImports[pkg]; ok {
return garbledPkg
func hashImport(pkg string, seed []byte) string {
if len(seed) == 0 {
pkgPath := pkg
if pkgPath == "main" {
// The main package is known under its import path in
// the import config map.
pkgPath = buildInfo.firstImport
}
seed = buildInfo.imports[pkg].actionID
seed = buildInfo.imports[pkgPath].actionID
}
garbledPkg := hashWith(seed, pkg)
garbledImports[pkg] = garbledPkg
return garbledPkg
return hashWith(seed, pkg)
}
// garbleSymbols replaces all private import paths/package names in symbol names
// and data of an archive member.
func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbledImports map[string]string, buf *bytes.Buffer, sb *strings.Builder) {
func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, buf *bytes.Buffer, sb *strings.Builder) {
lists := [][]*goobj2.Sym{am.SymDefs, am.NonPkgSymDefs, am.NonPkgSymRefs}
for _, list := range lists {
for _, s := range list {
@ -493,26 +471,26 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled
} else if strings.HasPrefix(s.Name, "type..namedata.") {
dataTyp = namedata
}
s.Data = garbleSymData(s.Data, privImports, garbledImports, dataTyp, buf)
s.Data = garbleSymData(s.Data, privImports, dataTyp, buf)
if s.Size != 0 {
s.Size = uint32(len(s.Data))
}
}
s.Name = garbleSymbolName(s.Name, privImports, garbledImports, sb)
s.Name = garbleSymbolName(s.Name, privImports, sb)
for i := range s.Reloc {
s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, garbledImports, sb)
s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, sb)
}
if s.Type != nil {
s.Type.Name = garbleSymbolName(s.Type.Name, privImports, garbledImports, sb)
s.Type.Name = garbleSymbolName(s.Type.Name, privImports, sb)
}
if s.Func != nil {
for i := range s.Func.FuncData {
s.Func.FuncData[i].Sym.Name = garbleSymbolName(s.Func.FuncData[i].Sym.Name, privImports, garbledImports, sb)
s.Func.FuncData[i].Sym.Name = garbleSymbolName(s.Func.FuncData[i].Sym.Name, privImports, sb)
}
for _, inl := range s.Func.InlTree {
inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, garbledImports, sb)
inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, sb)
if opts.Tiny {
inl.Line = 1
}
@ -533,7 +511,7 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled
}
}
for i := range am.SymRefs {
am.SymRefs[i].Name = garbleSymbolName(am.SymRefs[i].Name, privImports, garbledImports, sb)
am.SymRefs[i].Name = garbleSymbolName(am.SymRefs[i].Name, privImports, sb)
}
// remove dwarf file list, it isn't needed as we pass "-w, -s" to the linker
@ -542,7 +520,7 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled
// garbleSymbolName finds all private imports in a symbol name, garbles them,
// and returns the modified symbol name.
func garbleSymbolName(symName string, privImports privateImports, garbledImports map[string]string, sb *strings.Builder) string {
func garbleSymbolName(symName string, privImports privateImports, sb *strings.Builder) string {
prefix, name, skipSym := splitSymbolPrefix(symName)
if skipSym {
// log.Printf("\t\t? Skipped symbol: %s", symName)
@ -572,7 +550,7 @@ func garbleSymbolName(symName string, privImports privateImports, garbledImports
}
sb.WriteString(name[off : off+o])
sb.WriteString(hashImport(name[off+o:off+o+l], privName.seed, garbledImports))
sb.WriteString(hashImport(name[off+o:off+o+l], privName.seed))
off += o + l
}
@ -748,7 +726,7 @@ func isSymbol(c byte) bool {
// garbleSymData finds all private imports in a symbol's data blob,
// garbles them, and returns the modified symbol data.
func garbleSymData(data []byte, privImports privateImports, garbledImports map[string]string, dataTyp dataType, buf *bytes.Buffer) []byte {
func garbleSymData(data []byte, privImports privateImports, dataTyp dataType, buf *bytes.Buffer) []byte {
var symData []byte
switch dataTyp {
case importPath:
@ -772,11 +750,11 @@ func garbleSymData(data []byte, privImports privateImports, garbledImports map[s
// there is only one import path in the symbol's data, garble it and return
if dataTyp == importPath {
return createImportPathData(hashImport(string(symData[o:o+l]), privName.seed, garbledImports))
return createImportPathData(hashImport(string(symData[o:o+l]), privName.seed))
}
buf.Write(symData[off : off+o])
buf.WriteString(hashImport(string(symData[off+o:off+o+l]), privName.seed, garbledImports))
buf.WriteString(hashImport(string(symData[off+o:off+o+l]), privName.seed))
off += o + l
}
@ -817,7 +795,7 @@ func patchReflectData(newName []byte, data []byte) []byte {
}
// garbleImportCfg writes a new importcfg with private import paths garbled.
func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, replacedFiles map[string]string) error {
func garbleImportCfg(path string, importCfg goobj2.ImportCfg, replacedFiles map[string]string) error {
newCfg, err := os.Create(path)
if err != nil {
return fmt.Errorf("error creating importcfg: %v", err)
@ -827,10 +805,10 @@ func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, re
for pkgPath, otherPath := range importCfg.ImportMap {
if isPrivate(pkgPath) {
pkgPath = hashImport(pkgPath, nil, garbledImports)
pkgPath = hashImport(pkgPath, nil)
}
if isPrivate(otherPath) {
otherPath = hashImport(otherPath, nil, garbledImports)
otherPath = hashImport(otherPath, nil)
}
newCfgWr.WriteString("importmap ")
newCfgWr.WriteString(pkgPath)
@ -841,7 +819,7 @@ func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, re
for pkgPath, info := range importCfg.Packages {
if isPrivate(pkgPath) {
pkgPath = hashImport(pkgPath, nil, garbledImports)
pkgPath = hashImport(pkgPath, nil)
}
if info.IsSharedLib {
newCfgWr.WriteString("packageshlib ")

@ -128,8 +128,7 @@ var (
const (
// Note that these are capped at 16 bytes.
headerPrivateNameMap = "garble/privMap"
headerDebugSource = "garble/debugSrc"
headerDebugSource = "garble/debugSrc"
)
func garbledImport(path string) (*types.Package, error) {
@ -465,8 +464,6 @@ func transformCompile(args []string) ([]string, func() error, error) {
return nil, nil, fmt.Errorf("typecheck error: %v", err)
}
tf.privateNameMap = make(map[string]string)
tf.existingNames = collectExistingNames(files)
tf.recordReflectArgs(files)
if opts.GarbleLiterals {
@ -583,19 +580,9 @@ func transformCompile(args []string) ([]string, func() error, error) {
return err
}
nameMap, err := json.Marshal(tf.privateNameMap)
if err != nil {
return err
}
// Adding an extra archive header is safe,
// and shouldn't break other tools like the linker since our header name is unique
pkg.ArchiveMembers = append(pkg.ArchiveMembers,
goobj2.ArchiveMember{ArchiveHeader: goobj2.ArchiveHeader{
Name: headerPrivateNameMap,
Size: int64(len(nameMap)),
Data: nameMap,
}},
goobj2.ArchiveMember{ArchiveHeader: goobj2.ArchiveHeader{
Name: headerDebugSource,
Size: int64(obfSrcArchive.Len()),
@ -657,14 +644,9 @@ func (tf *transformer) handleDirectives(comments []string) {
// The name exists and was obfuscated; replace the
// comment with the obfuscated name.
if token.IsExported(name) {
obfName := hashWith(listedPkg.actionID, name)
fields[2] = pkg + "." + obfName
comments[i] = strings.Join(fields, " ")
} else if obfName, ok := tf.privateNameMap[fields[2]]; ok {
fields[2] = pkg + "." + obfName
comments[i] = strings.Join(fields, " ")
}
obfName := hashWith(listedPkg.actionID, name)
fields[2] = pkg + "." + obfName
comments[i] = strings.Join(fields, " ")
}
}
@ -873,22 +855,6 @@ func (tf *transformer) recordReflectArgs(files []*ast.File) {
}
}
// collectExistingNames collects all names, including the names of local
// variables, functions, global fields, etc.
func collectExistingNames(files []*ast.File) map[string]bool {
names := make(map[string]bool)
visit := func(node ast.Node) bool {
if ident, ok := node.(*ast.Ident); ok {
names[ident.Name] = true
}
return true
}
for _, file := range files {
ast.Inspect(file, visit)
}
return names
}
// transformer holds all the information and state necessary to obfuscate a
// single Go package.
type transformer struct {
@ -896,17 +862,6 @@ type transformer struct {
pkg *types.Package
info *types.Info
// existingNames contains all the existing names in the current package,
// to avoid name collisions.
existingNames map[string]bool
// privateNameMap records how unexported names were obfuscated. For
// example, "some/pkg.foo" could be mapped to "C" if it was one of the
// first private names to be obfuscated.
// TODO: why include the "some/pkg." prefix if it's always going to be
// the current package?
privateNameMap map[string]string
// ignoreObjects records all the objects we cannot obfuscate. An object
// is any named entity, such as a declared variable or type.
//
@ -918,11 +873,6 @@ type transformer struct {
// * Types or variables from external packages which were not
// obfuscated, for caching reasons; see transformGo.
ignoreObjects map[types.Object]bool
// nameCounter keeps track of how many unique identifier names we've
// obfuscated, so that the obfuscated names get assigned incrementing
// short names like "a", "b", "c", etc.
nameCounter int
}
// transformGo garbles the provided Go syntax node.
@ -1149,33 +1099,8 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File {
origName := node.Name
_ = origName // used for debug prints below
// The exported names cannot be shortened as counter synchronization
// between packages is not currently implemented
if token.IsExported(node.Name) {
node.Name = hashWith(actionID, node.Name)
// log.Printf("%q hashed with %x to %q", origName, actionID, node.Name)
return true
}
fullName := tf.pkg.Path() + "." + node.Name
if name, ok := tf.privateNameMap[fullName]; ok {
node.Name = name
// log.Printf("%q retrieved private name %q for %q", origName, name, path)
return true
}
var name string
for {
tf.nameCounter++
name = encodeIntToName(tf.nameCounter)
if !tf.existingNames[name] {
break
}
}
tf.privateNameMap[fullName] = name
node.Name = name
// log.Printf("%q assigned private name %q for %q", origName, name, path)
node.Name = hashWith(actionID, node.Name)
// log.Printf("%q hashed with %x to %q", origName, actionID, node.Name)
return true
}
return astutil.Apply(file, pre, nil).(*ast.File)
@ -1251,7 +1176,7 @@ func transformLink(args []string) ([]string, func() error, error) {
importMap := func(importPath string) (objectPath string) {
return buildInfo.imports[importPath].packagefile
}
garbledObj, garbledImports, privateNameMap, err := obfuscateImports(paths[0], importMap)
garbledObj, err := obfuscateImports(paths[0], importMap)
if err != nil {
return nil, nil, err
}
@ -1279,17 +1204,10 @@ func transformLink(args []string) ([]string, func() error, error) {
// the import config map.
pkgPath = buildInfo.firstImport
}
if id := buildInfo.imports[pkgPath].actionID; len(id) > 0 {
// We use privateNameMap because unexported names are obfuscated
// to short names like "A", "B", "C" etc, which is not reproducible
// here. If the name isn't in the map, a hash will do.
newName, ok := privateNameMap[pkg+"."+name]
if !ok {
newName = hashWith(id, name)
}
garbledPkg := garbledImports[pkg]
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", garbledPkg, newName, str))
}
id := buildInfo.imports[pkgPath].actionID
newName := hashWith(id, name)
garbledPkg := hashWith(id, pkg)
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", garbledPkg, newName, str))
})
// Ensure we strip the -buildid flag, to not leak any build IDs for the

@ -23,7 +23,7 @@ stdout 'unknown'
! stdout $gofullversion
# The binary can't contain the version string either.
! binsubstr main$exe ${WORK@R} 'main.go' 'globalVar' 'globalFunc' $gofullversion
! binsubstr main$exe ${WORK@R} 'main.go' 'globalVar' 'globalFunc' 'garble' $gofullversion
[short] stop # checking that the build is reproducible is slow

Loading…
Cancel
Save