diff --git a/hash.go b/hash.go index 4e3bee2..47ebf61 100644 --- a/hash.go +++ b/hash.go @@ -10,14 +10,13 @@ import ( "fmt" "go/token" "io" - "os" "os/exec" "strings" ) const buildIDSeparator = "/" -// splitActionID returns the action ID half of a build ID, the first element. +// splitActionID returns the action ID half of a build ID, the first component. func splitActionID(buildID string) string { i := strings.Index(buildID, buildIDSeparator) if i < 0 { @@ -26,13 +25,13 @@ func splitActionID(buildID string) string { return buildID[:i] } -// splitContentID returns the content ID half of a build ID, the last element. +// splitContentID returns the content ID half of a build ID, the last component. func splitContentID(buildID string) string { return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:] } -// decodeHash is the opposite of hashToString, but with a panic for error -// handling since it should never happen. +// decodeHash is the opposite of hashToString, with a panic for error handling +// since it should never happen. func decodeHash(str string) []byte { h, err := base64.RawURLEncoding.DecodeString(str) if err != nil { @@ -61,7 +60,7 @@ func alterToolVersion(tool string, args []string) error { toolID = decodeHash(splitContentID(f[len(f)-1])) } else { // For a release, the output is like: "compile version go1.9.1 X:framepointer". - // Use the whole line. + // Use the whole line, as we can assume it's unique. toolID = []byte(line) } @@ -76,33 +75,29 @@ func alterToolVersion(tool string, args []string) error { // The slashes let us imitate a full binary build ID, but we assume that // the other components such as the action ID are not necessary, since the // only reader here is cmd/go and it only consumes the content ID. - fmt.Printf("%s +garble buildID=_/_/_/%s\n", line, contentID) + fmt.Printf("%s +garble buildID=_/_/_/%s\n", line, hashToString(contentID)) return nil } -func ownContentID(toolID []byte) (string, error) { +func ownContentID(toolID []byte) ([]byte, error) { // We can't rely on the module version to exist, because it's // missing in local builds without 'go get'. - // For now, use 'go tool buildid' on the binary that's running. Just - // like Go's own cache, we use hex-encoded sha256 sums. + // For now, use 'go tool buildid' on the garble binary. + // Just like Go's own cache, we use hex-encoded sha256 sums. // Once https://github.com/golang/go/issues/37475 is fixed, we // can likely just use that. - path, err := os.Executable() + binaryBuildID, err := buildidOf(cache.ExecPath) if err != nil { - return "", err + return nil, err } - buildID, err := buildidOf(path) - if err != nil { - return "", err - } - ownID := decodeHash(splitContentID(buildID)) + binaryContentID := decodeHash(splitContentID(binaryBuildID)) // Join the two content IDs together into a single base64-encoded sha256 // sum. This includes the original tool's content ID, and garble's own // content ID. h := sha256.New() h.Write(toolID) - h.Write(ownID) + h.Write(binaryContentID) // We also need to add the selected options to the full version string, // because all of them result in different output. We use spaces to @@ -120,13 +115,17 @@ func ownContentID(toolID []byte) (string, error) { fmt.Fprintf(h, " -seed=%x", opts.Seed) } - return hashToString(h.Sum(nil)), nil + return h.Sum(nil)[:buildIDComponentLength], nil } +// buildIDComponentLength is the number of bytes each build ID component takes, +// such as an action ID or a content ID. +const buildIDComponentLength = 15 + // hashToString encodes the first 120 bits of a sha256 sum in base64, the same -// format used for elements in a build ID. +// format used for components in a build ID. func hashToString(h []byte) string { - return base64.RawURLEncoding.EncodeToString(h[:15]) + return base64.RawURLEncoding.EncodeToString(h[:buildIDComponentLength]) } func buildidOf(path string) (string, error) { diff --git a/main.go b/main.go index 1842b51..4518200 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "compress/gzip" "encoding/base64" "encoding/binary" - "encoding/json" "errors" "flag" "fmt" @@ -100,35 +99,23 @@ var ( return os.Open(pkg.Export) }) - // Basic information about the package being currently compiled or - // linked. These variables are filled in early, and reused later. - curPkgPath string // note that this isn't filled for the linker yet - curActionID []byte - curImportCfg string + // Basic information about the package being currently compiled or linked. + curPkg *listedPackage buildInfo = struct { - // TODO: replace part of this with goobj.ParseImportCfg, so that - // we can also reuse it. For now, parsing ourselves is still - // necessary so that we can set firstImport. + // TODO: do we still need imports? imports map[string]importedPkg // parsed importCfg plus cached info - - firstImport string // first from -importcfg; the main package when linking }{imports: make(map[string]importedPkg)} garbledImporter = importer.ForCompiler(fset, "gc", func(path string) (io.ReadCloser, error) { return os.Open(buildInfo.imports[path].packagefile) }).(types.ImporterFrom) - opts *options + opts *flagOptions envGoPrivate = os.Getenv("GOPRIVATE") // complemented by 'go env' later ) -const ( - // Note that these are capped at 16 bytes. - headerDebugSource = "garble/debugSrc" -) - func garbledImport(path string) (*types.Package, error) { ipkg, ok := buildInfo.imports[path] if !ok { @@ -150,7 +137,6 @@ func garbledImport(path string) (*types.Package, error) { type importedPkg struct { packagefile string - actionID []byte pkg *types.Package } @@ -284,7 +270,7 @@ func mainErr(args []string) error { // We're in a toolexec sub-process, not directly called by the user. // Load the shared data and wrap the tool, like the compiler or linker. - if err := loadShared(); err != nil { + if err := loadSharedCache(); err != nil { return err } opts = &cache.Options @@ -334,10 +320,14 @@ func toolexecCmd(command string, args []string) (*exec.Cmd, error) { } } - if err := setOptions(); err != nil { + if err := setFlagOptions(); err != nil { return nil, err } + // Here is the only place we initialize the cache. + // The sub-processes will parse it from a shared gob file. + cache = &sharedCache{Options: *opts} + // Note that we also need to pass build flags to 'go list', such // as -tags. cache.BuildFlags = filterBuildFlags(flags) @@ -359,7 +349,7 @@ func toolexecCmd(command string, args []string) (*exec.Cmd, error) { return nil, err } - sharedTempDir, err = saveShared() + sharedTempDir, err = saveSharedCache() if err != nil { return nil, err } @@ -406,7 +396,7 @@ func transformAsm(args []string) ([]string, error) { symAbis = true } } - curPkgPath = flagValue(flags, "-p") + curPkgPath := flagValue(flags, "-p") // If we are generating symbol ABIs, the output does not actually // contain curPkgPath. Exported APIs show up as "".FooBar. @@ -415,14 +405,13 @@ func transformAsm(args []string) ([]string, error) { // To obfuscate the path in the -p flag, we need the current action ID, // which we recover from the file that transformCompile wrote for us. if !symAbis && curPkgPath != "main" && isPrivate(curPkgPath) { - savedActionID := filepath.Join(sharedTempDir, strings.ReplaceAll(curPkgPath, "/", ",")) - var err error - curActionID, err = ioutil.ReadFile(savedActionID) - if err != nil { - return nil, fmt.Errorf("could not read build ID: %v", err) + curPkgPathFull := curPkgPath + if curPkgPathFull == "main" { + // TODO(mvdan): this can go with TOOLEXEC_IMPORTPATH + curPkgPathFull = cache.MainImportPath } - newPkgPath := hashWith(curActionID, curPkgPath) + newPkgPath := hashWith(cache.ListedPackages[curPkgPathFull].GarbleActionID, curPkgPath) flags = flagSetValue(flags, "-p", newPkgPath) } @@ -437,7 +426,7 @@ func transformCompile(args []string) ([]string, error) { // generating it. flags = append(flags, "-dwarf=false") - curPkgPath = flagValue(flags, "-p") + curPkgPath := flagValue(flags, "-p") if (curPkgPath == "runtime" && opts.Tiny) || curPkgPath == "runtime/internal/sys" { // Even though these packages aren't private, we will still process // them later to remove build information and strip code from the @@ -465,27 +454,19 @@ func transformCompile(args []string) ([]string, error) { if !strings.Contains(trimpath, ";") { return nil, fmt.Errorf("-toolexec=garble should be used alongside -trimpath") } + + curPkgPathFull := curPkgPath + if curPkgPathFull == "main" { + // TODO(mvdan): this can go with TOOLEXEC_IMPORTPATH + curPkgPathFull = cache.MainImportPath + } + curPkg = cache.ListedPackages[curPkgPathFull] + newImportCfg, err := fillBuildInfo(flags) if err != nil { return nil, err } - // Tools which run after the main compile run, such as asm, also need - // the current action ID to obfuscate the package path in their -p flag. - // They lack the -buildid flag, so store it in a unique file here to be - // recovered by the other tools later, such as transformAsm. - // Import paths include slashes, which usually cannot be in filenames, - // so replace those with commas, which should be fine and cannot be part - // of an import path. - // We only write each file once, as we compile each package once. - // Each filename is also unique, since import paths are unique. - // TODO: perhaps error if the file already exists, to double check that - // the assumptions above are correct. - savedActionID := filepath.Join(sharedTempDir, strings.ReplaceAll(curPkgPath, "/", ",")) - if err := ioutil.WriteFile(savedActionID, curActionID, 0o666); err != nil { - return nil, fmt.Errorf("could not store action ID: %v", err) - } - var files []*ast.File for _, path := range paths { file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) @@ -497,7 +478,7 @@ func transformCompile(args []string) ([]string, error) { randSeed := opts.Seed if len(randSeed) == 0 { - randSeed = curActionID + randSeed = curPkg.GarbleActionID } // log.Printf("seeding math/rand with %x\n", randSeed) mathrand.Seed(int64(binary.BigEndian.Uint64(randSeed))) @@ -535,7 +516,7 @@ func transformCompile(args []string) ([]string, error) { } origTypesConfig := types.Config{Importer: origImporter} - tf.pkg, err = origTypesConfig.Check(curPkgPath, fset, files, tf.info) + tf.pkg, err = origTypesConfig.Check(curPkgPathFull, fset, files, tf.info) if err != nil { return nil, fmt.Errorf("typecheck error: %v", err) } @@ -575,7 +556,8 @@ func transformCompile(args []string) ([]string, error) { // package path. newPkgPath := curPkgPath if curPkgPath != "main" && isPrivate(curPkgPath) { - newPkgPath = hashWith(curActionID, curPkgPath) + newPkgPath = hashWith(curPkg.GarbleActionID, curPkgPath) + // println("compile -p:", curPkgPath, newPkgPath) flags = flagSetValue(flags, "-p", newPkgPath) } @@ -631,15 +613,14 @@ func transformCompile(args []string) ([]string, error) { // Replace the import path with its obfuscated version. // If the import was unnamed, give it the name of the // original package name, to keep references working. - actionID := buildInfo.imports[path].actionID - newPath := hashWith(actionID, path) + lpkg, err := listPackage(path) + if err != nil { + panic(err) // should never happen + } + newPath := hashWith(lpkg.GarbleActionID, path) imp.Path.Value = strconv.Quote(newPath) if imp.Name == nil { - pkg, err := listPackage(path) - if err != nil { - panic(err) // should never happen - } - imp.Name = &ast.Ident{Name: pkg.Name} + imp.Name = &ast.Ident{Name: lpkg.Name} } return true }) @@ -738,30 +719,30 @@ func (tf *transformer) handleDirectives(comments []string) { if len(target) != 2 { continue } - pkg, name := target[0], target[1] - if pkg == "runtime" && strings.HasPrefix(name, "cgo") { + pkgPath, name := target[0], target[1] + if pkgPath == "runtime" && strings.HasPrefix(name, "cgo") { continue // ignore cgo-generated linknames } - if !isPrivate(pkg) { + if !isPrivate(pkgPath) { continue // ignore non-private symbols } - listedPkg, ok := buildInfo.imports[pkg] - if !ok { + lpkg, err := listPackage(pkgPath) + if err != nil { continue // probably a made up symbol name } - garbledPkg, _ := garbledImport(pkg) + garbledPkg, _ := garbledImport(pkgPath) if garbledPkg != nil && garbledPkg.Scope().Lookup(name) != nil { continue // the name exists and was not garbled } // The name exists and was obfuscated; replace the // comment with the obfuscated name. - newName := hashWith(listedPkg.actionID, name) - newPkg := pkg - if pkg != "main" { - newPkg = hashWith(listedPkg.actionID, pkg) + newName := hashWith(lpkg.GarbleActionID, name) + newPkgPath := pkgPath + if pkgPath != "main" { + newPkgPath = hashWith(lpkg.GarbleActionID, pkgPath) } - fields[2] = newPkg + "." + newName + fields[2] = newPkgPath + "." + newName comments[i] = strings.Join(fields, " ") } } @@ -878,18 +859,11 @@ func isPrivate(path string) bool { // and constructs a new importcfg with the obfuscated import paths changed as // necessary. func fillBuildInfo(flags []string) (newImportCfg string, _ error) { - buildID := flagValue(flags, "-buildid") - switch buildID { - case "", "true": - return "", fmt.Errorf("could not find -buildid argument") - } - - curActionID = decodeHash(splitActionID(buildID)) - curImportCfg = flagValue(flags, "-importcfg") - if curImportCfg == "" { + importCfg := flagValue(flags, "-importcfg") + if importCfg == "" { return "", fmt.Errorf("could not find -importcfg argument") } - data, err := ioutil.ReadFile(curImportCfg) + data, err := ioutil.ReadFile(importCfg) if err != nil { return "", err } @@ -921,20 +895,8 @@ func fillBuildInfo(flags []string) (newImportCfg string, _ error) { continue } importPath, objectPath := args[:j], args[j+1:] - buildID, err := buildidOf(objectPath) - if err != nil { - return "", err - } - // log.Println("buildid:", buildID) - if len(buildInfo.imports) == 0 { - buildInfo.firstImport = importPath - } - actionID := decodeHash(splitActionID(buildID)) - impPkg := importedPkg{ - packagefile: objectPath, - actionID: actionID, - } + impPkg := importedPkg{packagefile: objectPath} buildInfo.imports[importPath] = impPkg if otherPath, ok := importMap[importPath]; ok { @@ -953,16 +915,23 @@ func fillBuildInfo(flags []string) (newImportCfg string, _ error) { return "", err } for beforePath, afterPath := range importMap { - if isPrivate(afterPath) { - actionID := buildInfo.imports[afterPath].actionID - afterPath = hashWith(actionID, afterPath) + if isPrivate(beforePath) { + println(beforePath, afterPath) + pkg, err := listPackage(beforePath) + if err != nil { + panic(err) // shouldn't happen + } + afterPath = hashWith(pkg.GarbleActionID, afterPath) } fmt.Fprintf(newCfg, "importmap %s=%s\n", beforePath, afterPath) } for impPath, pkg := range buildInfo.imports { if isPrivate(impPath) { - actionID := buildInfo.imports[impPath].actionID - impPath = hashWith(actionID, impPath) + pkg, err := listPackage(impPath) + if err != nil { + panic(err) // shouldn't happen + } + impPath = hashWith(pkg.GarbleActionID, impPath) } fmt.Fprintf(newCfg, "packagefile %s=%s\n", impPath, pkg.packagefile) } @@ -1172,69 +1141,15 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { return true // we only want to rename the above } - // Handle the case where the name is defined in an indirectly - // imported package. Since only direct imports show up in our - // importcfg, buildInfo.imports will not initially contain the - // package path we want. - // - // This edge case can happen, for example, if package A imports - // package B and calls its API, and B's API returns C's struct. - // Suddenly, A can use struct field names defined in C, even - // though A never directly imports C. - // - // For this rare case, for now, do an extra "go list -toolexec" - // call to retrieve its export path. - // TODO: Think about ways to avoid this extra exec call. Perhaps - // add an extra archive header to record all direct and indirect - // importcfg data, like we do with private name maps. - if _, e := buildInfo.imports[path]; !e && path != curPkgPath { - goArgs := []string{ - "list", - "-json", - "-export", - "-trimpath", - "-toolexec=" + cache.ExecPath, - } - goArgs = append(goArgs, cache.BuildFlags...) - goArgs = append(goArgs, path) - - cmd := exec.Command("go", goArgs...) - cmd.Dir = opts.GarbleDir - out, err := cmd.Output() - if err != nil { - if err := err.(*exec.ExitError); err != nil { - panic(fmt.Sprintf("%v: %s", err, err.Stderr)) - } - panic(err) - } - var pkg listedPackage - if err := json.Unmarshal(out, &pkg); err != nil { - panic(err) // shouldn't happen - } - buildID, err := buildidOf(pkg.Export) - if err != nil { - panic(err) // shouldn't happen - } - // Adding it to buildInfo.imports allows us to reuse the - // "if" branch below. Plus, if this edge case triggers - // multiple times in a single package compile, we can - // call "go list" once and cache its result. - if pkg.ImportPath != path { - panic(fmt.Sprintf("unexpected path: %q vs %q", pkg.ImportPath, path)) - } - buildInfo.imports[path] = importedPkg{ - packagefile: pkg.Export, - actionID: decodeHash(splitActionID(buildID)), - } - // log.Printf("fetched indirect dependency %q from: %s", path, pkg.Export) + lpkg, err := listPackage(path) + if err != nil { + panic(err) // shouldn't happen } - - actionID := curActionID // TODO: Make this check less prone to bugs, like the one we had // with indirect dependencies. If "path" is not our current // package, then it must exist in buildInfo.imports. Otherwise // we should panic. - if id := buildInfo.imports[path].actionID; len(id) > 0 { + if buildInfo.imports[path].packagefile != "" { garbledPkg, err := garbledImport(path) if err != nil { panic(err) // shouldn't happen @@ -1245,14 +1160,13 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { if o := garbledPkg.Scope().Lookup(obj.Name()); o != nil && reflect.TypeOf(o) == reflect.TypeOf(obj) { return true } - actionID = id } origName := node.Name _ = origName // used for debug prints below - node.Name = hashWith(actionID, node.Name) - // log.Printf("%q hashed with %x to %q", origName, actionID, node.Name) + node.Name = hashWith(lpkg.GarbleActionID, node.Name) + // log.Printf("%q hashed with %x to %q", origName, lpkg.GarbleActionID, node.Name) return true } return astutil.Apply(file, pre, nil).(*ast.File) @@ -1320,6 +1234,8 @@ func transformLink(args []string) ([]string, error) { // lack any extension. flags, args := splitFlagsFromArgs(args) + curPkg = cache.ListedPackages[cache.MainImportPath] + newImportCfg, err := fillBuildInfo(flags) if err != nil { return nil, err @@ -1344,11 +1260,9 @@ func transformLink(args []string) ([]string, error) { pkgPath := pkg if pkgPath == "main" { - // The main package is known under its import path in - // the import config map. - pkgPath = buildInfo.firstImport + pkgPath = cache.MainImportPath } - id := buildInfo.imports[pkgPath].actionID + id := cache.ListedPackages[pkgPath].GarbleActionID newName := hashWith(id, name) newPkg := pkg if pkg != "main" && isPrivate(pkg) { diff --git a/reverse.go b/reverse.go index 66b7b85..90e5a5d 100644 --- a/reverse.go +++ b/reverse.go @@ -37,6 +37,7 @@ func commandReverse(args []string) error { if err != nil { return err } + curPkg = cache.ListedPackages[cache.MainImportPath] stdout, err := cmd.StdoutPipe() if err != nil { @@ -69,16 +70,9 @@ func commandReverse(args []string) error { if isPrivate(pkg.ImportPath) { privatePkgPaths = append(privatePkgPaths, pkg.ImportPath) } - buildID, err := buildidOf(pkg.Export) - if err != nil { - return err - } // The action ID, and possibly the export file, will be used // later to reconstruct the mapping of obfuscated names. - buildInfo.imports[pkg.ImportPath] = importedPkg{ - packagefile: pkg.Export, - actionID: decodeHash(splitActionID(buildID)), - } + buildInfo.imports[pkg.ImportPath] = importedPkg{packagefile: pkg.Export} } if err := cmd.Wait(); err != nil { @@ -94,18 +88,17 @@ func commandReverse(args []string) error { fset := token.NewFileSet() for _, pkgPath := range privatePkgPaths { - ipkg := buildInfo.imports[pkgPath] + lpkg, err := listPackage(pkgPath) + if err != nil { + return err + } addReplace := func(str string) { - replaces = append(replaces, hashWith(ipkg.actionID, str), str) + replaces = append(replaces, hashWith(lpkg.GarbleActionID, str), str) } // Package paths are obfuscated, too. addReplace(pkgPath) - lpkg, err := listPackage(pkgPath) - if err != nil { - return err - } for _, goFile := range lpkg.GoFiles { goFile = filepath.Join(lpkg.Dir, goFile) file, err := parser.ParseFile(fset, goFile, nil, 0) diff --git a/shared.go b/shared.go index b352d75..1b5098d 100644 --- a/shared.go +++ b/shared.go @@ -3,6 +3,7 @@ package main import ( "bytes" "crypto/rand" + "crypto/sha256" "encoding/base64" "encoding/gob" "encoding/json" @@ -14,36 +15,48 @@ import ( "strings" ) -// shared this data is shared between the different garble processes -type shared struct { +// sharedCache this data is sharedCache between the different garble processes. +// +// Note that we fill this cache once from the root process in saveListedPackages, +// store it into a temporary file via gob encoding, and then reuse that file +// in each of the garble toolexec sub-processes. +type sharedCache struct { ExecPath string // absolute path to the garble binary being used BuildFlags []string // build flags fed to the original "garble ..." command - Options options // garble options being used, i.e. our own flags - ListedPackages listedPackages // non-garbled view of all packages to build + Options flagOptions // garble options being used, i.e. our own flags + + // ListedPackages contains data obtained via 'go list -json -export -deps'. This + // allows us to obtain the non-garbled export data of all dependencies, useful + // for type checking of the packages as we obfuscate them. + ListedPackages map[string]*listedPackage + MainImportPath string // TODO: remove with TOOLEXEC_IMPORTPATH } -var cache *shared +var cache *sharedCache -// loadShared the shared data passed from the entry garble process -func loadShared() error { - if cache == nil { - f, err := os.Open(filepath.Join(sharedTempDir, "main-cache.gob")) - if err != nil { - return fmt.Errorf(`cannot open shared file, this is most likely due to not running "garble [command]"`) - } - defer f.Close() - if err := gob.NewDecoder(f).Decode(&cache); err != nil { - return err - } +// loadSharedCache the shared data passed from the entry garble process +func loadSharedCache() error { + if cache != nil { + panic("shared cache loaded twice?") + } + f, err := os.Open(filepath.Join(sharedTempDir, "main-cache.gob")) + if err != nil { + return fmt.Errorf(`cannot open shared file, this is most likely due to not running "garble [command]"`) + } + defer f.Close() + if err := gob.NewDecoder(f).Decode(&cache); err != nil { + return err } - return nil } -// saveShared creates a temporary directory to share between garble processes. +// saveSharedCache creates a temporary directory to share between garble processes. // This directory also includes the gob-encoded cache global. -func saveShared() (string, error) { +func saveSharedCache() (string, error) { + if cache == nil { + panic("saving a missing cache?") + } dir, err := ioutil.TempDir("", "garble-shared") if err != nil { return "", err @@ -62,8 +75,8 @@ func saveShared() (string, error) { return dir, nil } -// options are derived from the flags -type options struct { +// flagOptions are derived from the flags +type flagOptions struct { GarbleLiterals bool Tiny bool GarbleDir string @@ -72,14 +85,17 @@ type options struct { Random bool } -// setOptions sets all options from the user supplied flags. -func setOptions() error { +// setFlagOptions sets flagOptions from the user supplied flags. +func setFlagOptions() error { wd, err := os.Getwd() if err != nil { return err } - opts = &options{ + if cache != nil { + panic("opts set twice?") + } + opts = &flagOptions{ GarbleDir: wd, GarbleLiterals: flagGarbleLiterals, Tiny: flagGarbleTiny, @@ -124,31 +140,28 @@ func setOptions() error { opts.DebugDir = flagDebugDir } - cache = &shared{Options: *opts} - return nil } -// listedPackages contains data obtained via 'go list -json -export -deps'. This -// allows us to obtain the non-garbled export data of all dependencies, useful -// for type checking of the packages as we obfuscate them. -// -// Note that we obtain this data once in saveListedPackages, store it into a -// temporary file via gob encoding, and then reuse that file in each of the -// garble processes that wrap a package compilation. -type listedPackages map[string]*listedPackage - -// listedPackage contains information useful for obfuscating a package +// listedPackage contains the 'go list -json -export' fields obtained by the +// root process, shared with all garble sub-processes via a file. type listedPackage struct { Name string ImportPath string Export string + BuildID string Deps []string ImportMap map[string]string Dir string GoFiles []string + // The fields below are not part of 'go list', but are still reused + // between garble processes. Use "Garble" as a prefix to ensure no + // collisions with the JSON fields from 'go list'. + + GarbleActionID []byte + // TODO(mvdan): reuse this field once TOOLEXEC_IMPORTPATH is used private bool } @@ -156,7 +169,7 @@ type listedPackage struct { // setListedPackages gets information about the current package // and all of its dependencies func setListedPackages(patterns []string) error { - args := []string{"list", "-json", "-deps", "-export"} + args := []string{"list", "-json", "-deps", "-export", "-trimpath"} args = append(args, cache.BuildFlags...) args = append(args, patterns...) cmd := exec.Command("go", args...) @@ -172,13 +185,42 @@ func setListedPackages(patterns []string) error { if err := cmd.Start(); err != nil { return fmt.Errorf("go list error: %v", err) } + + binaryBuildID, err := buildidOf(cache.ExecPath) + if err != nil { + return err + } + binaryContentID := decodeHash(splitContentID(binaryBuildID)) + dec := json.NewDecoder(stdout) - cache.ListedPackages = make(listedPackages) + cache.ListedPackages = make(map[string]*listedPackage) for dec.More() { var pkg listedPackage if err := dec.Decode(&pkg); err != nil { return err } + if pkg.Export != "" { + buildID := pkg.BuildID + if buildID == "" { + // go list only includes BuildID in 1.16+ + buildID, err = buildidOf(pkg.Export) + if err != nil { + panic(err) // shouldn't happen + } + } + actionID := decodeHash(splitActionID(buildID)) + h := sha256.New() + h.Write(actionID) + h.Write(binaryContentID) + + pkg.GarbleActionID = h.Sum(nil)[:buildIDComponentLength] + } + if pkg.Name == "main" { + if cache.MainImportPath != "" { + return fmt.Errorf("found two main packages: %s %s", cache.MainImportPath, pkg.ImportPath) + } + cache.MainImportPath = pkg.ImportPath + } cache.ListedPackages[pkg.ImportPath] = &pkg } @@ -217,10 +259,8 @@ func listPackage(path string) (*listedPackage, error) { // If the path is listed in the top-level ImportMap, use its mapping instead. // This is a common scenario when dealing with vendored packages in GOROOT. // The map is flat, so we don't need to recurse. - if fromPkg, ok := cache.ListedPackages[curPkgPath]; ok { - if path2 := fromPkg.ImportMap[path]; path2 != "" { - path = path2 - } + if path2 := curPkg.ImportMap[path]; path2 != "" { + path = path2 } pkg, ok := cache.ListedPackages[path]