You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/shared.go

436 lines
14 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bytes"
"crypto/sha256"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"golang.org/x/mod/module"
)
//go:generate ./scripts/gen-go-std-tables.sh
// sharedCacheType is shared as a read-only cache between the many garble toolexec
// sub-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 sharedCacheType struct {
ExecPath string // absolute path to the garble binary being used
ForwardBuildFlags []string // build flags fed to the original "garble ..." command
CacheDir string // absolute path to the GARBLE_CACHE directory being used
// ListedPackages contains data obtained via 'go list -json -export -deps'.
// This allows us to obtain the non-obfuscated export data of all dependencies,
// useful for type checking of the packages as we obfuscate them.
ListedPackages map[string]*listedPackage
// We can't use garble's own module version, as it may not exist.
// We can't use the stamped VCS information either,
// as uncommitted changes simply show up as "dirty".
//
// The only unique way to identify garble's version without being published
// or committed is to use its content ID from the build cache.
BinaryContentID []byte
GOGARBLE string
// GoVersion is a version of the Go toolchain currently being used,
// as reported by "go env GOVERSION" and compatible with go/version.
// Note that the version of Go that built the garble binary might be newer.
// Also note that a devel version like "go1.22-231f290e51" is
// currently represented as "go1.22", as the suffix is ignored by go/version.
GoVersion string
// Filled directly from "go env".
// Keep in sync with fetchGoEnv.
GoEnv struct {
GOOS string // i.e. the GOOS build target
GOMOD string
GOVERSION string
GOROOT string
}
}
var sharedCache *sharedCacheType
// loadSharedCache the shared data passed from the entry garble process
func loadSharedCache() error {
if sharedCache != nil {
panic("shared cache loaded twice?")
}
startTime := time.Now()
f, err := os.Open(filepath.Join(sharedTempDir, "main-cache.gob"))
if err != nil {
return fmt.Errorf(`cannot open shared file: %v\ndid you run "go [command] -toolexec=garble" instead of "garble [command]"?`, err)
}
defer func() {
log.Printf("shared cache loaded in %s from %s", debugSince(startTime), f.Name())
}()
defer f.Close()
if err := gob.NewDecoder(f).Decode(&sharedCache); err != nil {
return fmt.Errorf("cannot decode shared file: %v", err)
}
return nil
}
// saveSharedCache creates a temporary directory to share between garble processes.
// This directory also includes the gob-encoded cache global.
func saveSharedCache() (string, error) {
if sharedCache == nil {
panic("saving a missing cache?")
}
dir, err := os.MkdirTemp("", "garble-shared")
if err != nil {
return "", err
}
cachePath := filepath.Join(dir, "main-cache.gob")
if err := writeGobExclusive(cachePath, &sharedCache); err != nil {
return "", err
}
return dir, nil
}
func createExclusive(name string) (*os.File, error) {
return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
}
func writeFileExclusive(name string, data []byte) error {
f, err := createExclusive(name)
if err != nil {
return err
}
_, err = f.Write(data)
if err2 := f.Close(); err == nil {
err = err2
}
return err
}
func writeGobExclusive(name string, val any) error {
f, err := createExclusive(name)
if err != nil {
return err
}
// Always close the file, and return the first error we get.
err = gob.NewEncoder(f).Encode(val)
if err2 := f.Close(); err == nil {
err = err2
}
return err
}
// 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
ForTest string
Export string
BuildID string
Deps []string
ImportMap map[string]string
Standard bool
Dir string
CompiledGoFiles []string
IgnoredGoFiles []string
Imports []string
Error *packageError // to report package loading errors to the user
// 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 is a hash combining the Action ID from BuildID,
// with Garble's own inputs as per addGarbleToHash.
// It is set even when ToObfuscate is false, as it is also used for random
// seeds and build cache paths, and not just to obfuscate names.
GarbleActionID [sha256.Size]byte `json:"-"`
// ToObfuscate records whether the package should be obfuscated.
// When true, GarbleActionID must not be empty.
ToObfuscate bool `json:"-"`
}
type packageError struct {
Pos string
Err string
}
func (p *listedPackage) obfuscatedImportPath() string {
// We can't obfuscate these standard library import paths,
// as the toolchain expects to recognize the packages by them:
//
// * runtime: it is special in many ways
// * reflect: its presence turns down dead code elimination
// * embed: its presence enables using //go:embed
// * others like syscall are allowed by import path to have more ABI tricks
//
// TODO: collect directly from cmd/internal/objabi/pkgspecial.go,
// in this particular case from allowAsmABIPkgs.
switch p.ImportPath {
case "runtime", "reflect", "embed", "syscall", "runtime/internal/startlinetest":
return p.ImportPath
}
// Intrinsics are matched by package import path as well.
if compilerIntrinsicsPkgs[p.ImportPath] {
return p.ImportPath
}
if !p.ToObfuscate {
return p.ImportPath
}
newPath := hashWithPackage(p, p.ImportPath)
log.Printf("import path %q hashed with %x to %q", p.ImportPath, p.GarbleActionID, newPath)
return newPath
}
// garbleBuildFlags are always passed to top-level build commands such as
// "go build", "go list", or "go test".
var garbleBuildFlags = []string{"-trimpath", "-buildvcs=false"}
// appendListedPackages gets information about the current package
// and all of its dependencies
func appendListedPackages(packages []string, mainBuild bool) error {
startTime := time.Now()
args := []string{
"list",
// Similar flags to what go/packages uses.
"-json", "-export", "-compiled", "-e",
}
if mainBuild {
// When loading the top-level packages we are building,
// we want to transitively load all their dependencies as well.
// That is not the case when loading standard library packages,
// as runtimeLinknamed already contains transitive dependencies.
args = append(args, "-deps")
}
args = append(args, garbleBuildFlags...)
args = append(args, sharedCache.ForwardBuildFlags...)
if !mainBuild {
// If the top-level build included the -mod or -modfile flags,
// they should be used when loading the top-level packages.
// However, when loading standard library packages,
// using those flags would likely result in an error,
// as the standard library uses its own Go module and vendoring.
args = append(args, "-mod=", "-modfile=")
}
args = append(args, packages...)
cmd := exec.Command("go", args...)
defer func() {
log.Printf("original build info obtained in %s via: go %s", debugSince(startTime), strings.Join(args, " "))
}()
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("go list error: %v", err)
}
dec := json.NewDecoder(stdout)
if sharedCache.ListedPackages == nil {
sharedCache.ListedPackages = make(map[string]*listedPackage)
}
var pkgErrors strings.Builder
for dec.More() {
var pkg listedPackage
if err := dec.Decode(&pkg); err != nil {
return err
}
if perr := pkg.Error; perr != nil {
if pkg.Standard && len(pkg.CompiledGoFiles) == 0 && len(pkg.IgnoredGoFiles) > 0 {
// Some packages in runtimeLinknamed need a build tag to be importable,
// like crypto/internal/boring/fipstls with boringcrypto,
// so any pkg.Error should be ignored when the build tag isn't set.
} else {
if pkgErrors.Len() > 0 {
pkgErrors.WriteString("\n")
}
if perr.Pos != "" {
pkgErrors.WriteString(perr.Pos)
pkgErrors.WriteString(": ")
}
// Error messages sometimes include a trailing newline.
pkgErrors.WriteString(strings.TrimRight(perr.Err, "\n"))
}
}
// Note that we use the `-e` flag above with `go list`.
// If a package fails to load, the Incomplete and Error fields will be set.
// We still record failed packages in the ListedPackages map,
// because some like crypto/internal/boring/fipstls simply fall under
// "build constraints exclude all Go files" and can be ignored.
// Real build errors will still be surfaced by `go build -toolexec` later.
if sharedCache.ListedPackages[pkg.ImportPath] != nil {
return fmt.Errorf("duplicate package: %q", pkg.ImportPath)
}
if pkg.BuildID != "" {
actionID := decodeBuildIDHash(splitActionID(pkg.BuildID))
pkg.GarbleActionID = addGarbleToHash(actionID)
}
sharedCache.ListedPackages[pkg.ImportPath] = &pkg
}
if err := cmd.Wait(); err != nil {
return fmt.Errorf("go list error: %v:\nargs: %q\n%s", err, args, stderr.Bytes())
}
if pkgErrors.Len() > 0 {
return errors.New(pkgErrors.String())
}
anyToObfuscate := false
for path, pkg := range sharedCache.ListedPackages {
// If "GOGARBLE=foo/bar", "foo/bar_test" should also match.
if pkg.ForTest != "" {
path = pkg.ForTest
}
switch {
// We do not support obfuscating the runtime nor its dependencies.
case runtimeAndDeps[path],
// "unknown pc" crashes on windows in the cgo test otherwise.
path == "runtime/cgo":
// No point in obfuscating empty packages, like OS-specific ones that don't match.
case len(pkg.CompiledGoFiles) == 0:
// Test main packages like "foo/bar.test" are always obfuscated,
// just like unnamed and plugin main packages.
case pkg.Name == "main" && strings.HasSuffix(path, ".test"),
path == "command-line-arguments",
strings.HasPrefix(path, "plugin/unnamed"),
module.MatchPrefixPatterns(sharedCache.GOGARBLE, path):
pkg.ToObfuscate = true
anyToObfuscate = true
if len(pkg.GarbleActionID) == 0 {
return fmt.Errorf("package %q to be obfuscated lacks build id?", pkg.ImportPath)
}
}
}
// Don't error if the user ran: GOGARBLE='*' garble build runtime
if !anyToObfuscate && !module.MatchPrefixPatterns(sharedCache.GOGARBLE, "runtime") {
return fmt.Errorf("GOGARBLE=%q does not match any packages to be built", sharedCache.GOGARBLE)
}
return nil
}
var listedRuntimeLinknamed = false
var ErrNotFound = errors.New("not found")
var ErrNotDependency = errors.New("not a dependency")
// listPackage gets the listedPackage information for a certain package
func listPackage(from *listedPackage, path string) (*listedPackage, error) {
if path == from.ImportPath {
return from, nil
}
// 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 path2 := from.ImportMap[path]; path2 != "" {
path = path2
}
pkg, ok := sharedCache.ListedPackages[path]
// A std package may list any other package in std, even those it doesn't depend on.
// This is due to how runtime linkname-implements std packages,
// such as sync/atomic or reflect, without importing them in any way.
// A few other cases don't involve runtime, like time/tzdata linknaming to time,
// but luckily those few cases are covered by runtimeLinknamed as well.
//
// If ListedPackages lacks such a package we fill it via runtimeLinknamed.
// TODO: can we instead add runtimeLinknamed to the top-level "go list" args?
if from.Standard {
if ok {
return pkg, nil
}
if listedRuntimeLinknamed {
return nil, fmt.Errorf("package %q still missing after go list call", path)
}
startTime := time.Now()
missing := make([]string, 0, len(runtimeLinknamed))
for _, linknamed := range runtimeLinknamed {
switch {
case sharedCache.ListedPackages[linknamed] != nil:
// We already have it; skip.
case sharedCache.GoEnv.GOOS != "js" && linknamed == "syscall/js":
// GOOS-specific package.
case sharedCache.GoEnv.GOOS != "darwin" && sharedCache.GoEnv.GOOS != "ios" && linknamed == "crypto/x509/internal/macos":
// GOOS-specific package.
default:
missing = append(missing, linknamed)
}
}
// We don't need any information about their dependencies, in this case.
if err := appendListedPackages(missing, false); err != nil {
return nil, fmt.Errorf("failed to load missing runtime-linknamed packages: %v", err)
}
pkg, ok := sharedCache.ListedPackages[path]
if !ok {
return nil, fmt.Errorf("std listed another std package that we can't find: %s", path)
}
listedRuntimeLinknamed = true
log.Printf("listed %d missing runtime-linknamed packages in %s", len(missing), debugSince(startTime))
return pkg, nil
}
if !ok {
return nil, fmt.Errorf("list %s: %w", path, ErrNotFound)
}
// Packages outside std can list any package,
// as long as they depend on it directly or indirectly.
for _, dep := range from.Deps {
if dep == pkg.ImportPath {
return pkg, nil
}
}
// As a special case, any package can list runtime or its dependencies,
// since those are always an implicit dependency.
// We need to handle this ourselves as runtime does not appear in Deps.
// TODO: it might be faster to bring back a "runtimeAndDeps" map or func.
if pkg.ImportPath == "runtime" {
return pkg, nil
}
for _, dep := range sharedCache.ListedPackages["runtime"].Deps {
if dep == pkg.ImportPath {
return pkg, nil
}
}
return nil, fmt.Errorf("list %s: %w", path, ErrNotDependency)
}