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/main.go

2092 lines
62 KiB
Go

// Copyright (c) 2019, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/gob"
"encoding/json"
"errors"
"flag"
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io"
"io/fs"
"log"
mathrand "math/rand"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"golang.org/x/exp/slices"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
"golang.org/x/tools/go/ast/astutil"
"mvdan.cc/garble/internal/literals"
)
var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError)
var (
flagLiterals bool
flagTiny bool
flagDebug bool
flagDebugDir string
flagSeed seedFlag
)
func init() {
flagSet.Usage = usage
flagSet.BoolVar(&flagLiterals, "literals", false, "Obfuscate literals such as strings")
flagSet.BoolVar(&flagTiny, "tiny", false, "Optimize for binary size, losing some ability to reverse the process")
flagSet.BoolVar(&flagDebug, "debug", false, "Print debug logs to stderr")
flagSet.StringVar(&flagDebugDir, "debugdir", "", "Write the obfuscated source to a directory, e.g. -debugdir=out")
flagSet.Var(&flagSeed, "seed", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random")
}
var rxGarbleFlag = regexp.MustCompile(`-(?:literals|tiny|debug|debugdir|seed)(?:$|=)`)
type seedFlag struct {
random bool
bytes []byte
}
func (f seedFlag) present() bool { return len(f.bytes) > 0 }
func (f seedFlag) String() string {
return base64.RawStdEncoding.EncodeToString(f.bytes)
}
func (f *seedFlag) Set(s string) error {
if s == "random" {
f.bytes = make([]byte, 16) // random 128 bit seed
if _, err := rand.Read(f.bytes); err != nil {
return fmt.Errorf("error generating random seed: %v", err)
}
} else {
// We expect unpadded base64, but to be nice, accept padded
// strings too.
s = strings.TrimRight(s, "=")
seed, err := base64.RawStdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("error decoding seed: %v", err)
}
if len(seed) < 8 {
return fmt.Errorf("-seed needs at least 8 bytes, have %d", len(seed))
}
f.bytes = seed
}
return nil
}
func usage() {
fmt.Fprintf(os.Stderr, `
Garble obfuscates Go code by wrapping the Go toolchain.
garble [garble flags] command [go flags] [go arguments]
For example, to build an obfuscated program:
garble build ./cmd/foo
Similarly, to combine garble flags and Go build flags:
garble -literals build -tags=purego ./cmd/foo
The following commands are supported:
build replace "go build"
test replace "go test"
version print Garble version
reverse de-obfuscate output such as stack traces
To learn more about a command, run "garble help <command>".
garble accepts the following flags before a command:
`[1:])
flagSet.PrintDefaults()
fmt.Fprintf(os.Stderr, `
For more information, see https://github.com/burrowers/garble.
`[1:])
}
func main() { os.Exit(main1()) }
var (
fset = token.NewFileSet()
sharedTempDir = os.Getenv("GARBLE_SHARED")
parentWorkDir = os.Getenv("GARBLE_PARENT_WORK")
// origImporter is a go/types importer which uses the original versions
// of packages, without any obfuscation. This is helpful to make
// decisions on how to obfuscate our input code.
origImporter = importerWithMap(importer.ForCompiler(fset, "gc", func(path string) (io.ReadCloser, error) {
pkg, err := listPackage(path)
if err != nil {
return nil, err
}
return os.Open(pkg.Export)
}).(types.ImporterFrom).ImportFrom)
// Basic information about the package being currently compiled or linked.
curPkg *listedPackage
)
type importerWithMap func(path, dir string, mode types.ImportMode) (*types.Package, error)
func (fn importerWithMap) Import(path string) (*types.Package, error) {
panic("should never be called")
}
func (fn importerWithMap) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
if path2 := curPkg.ImportMap[path]; path2 != "" {
path = path2
}
return fn(path, dir, mode)
}
// uniqueLineWriter sits underneath log.SetOutput to deduplicate log lines.
// We log bits of useful information for debugging,
// and logging the same detail twice is not going to help the user.
// Duplicates are relatively normal, given that names tend to repeat.
type uniqueLineWriter struct {
out io.Writer
seen map[string]bool
}
func (w *uniqueLineWriter) Write(p []byte) (n int, err error) {
if !flagDebug {
panic("unexpected use of uniqueLineWriter with -debug unset")
}
if bytes.Count(p, []byte("\n")) != 1 {
panic(fmt.Sprintf("log write wasn't just one line: %q", p))
}
if w.seen[string(p)] {
return len(p), nil
}
if w.seen == nil {
w.seen = make(map[string]bool)
}
w.seen[string(p)] = true
return w.out.Write(p)
}
// debugf is like log.Printf, but it is a no-op by default.
// TODO(mvdan): use our own debug logger instead of hijacking the global one,
// and drop the "flagDebug" no-op check now that we require Go 1.18 or later.
func debugf(format string, args ...any) {
if !flagDebug {
return
}
log.Printf(format, args...)
}
// debugSince is like time.Since but resulting in shorter output.
// A build process takes at least hundreds of milliseconds,
// so extra decimal points in the order of microseconds aren't meaningful.
func debugSince(start time.Time) time.Duration {
return time.Since(start).Truncate(10 * time.Microsecond)
}
func main1() int {
defer func() {
if os.Getenv("GARBLE_WRITE_ALLOCS") != "true" {
return
}
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Fprintf(os.Stderr, "garble allocs: %d\n", memStats.Mallocs)
}()
if err := flagSet.Parse(os.Args[1:]); err != nil {
return 2
}
log.SetPrefix("[garble] ")
log.SetFlags(0) // no timestamps, as they aren't very useful
if flagDebug {
// TODO: cover this in the tests.
log.SetOutput(&uniqueLineWriter{out: os.Stderr})
} else {
log.SetOutput(io.Discard)
}
args := flagSet.Args()
if len(args) < 1 {
usage()
return 2
}
if err := mainErr(args); err != nil {
if code, ok := err.(errJustExit); ok {
return int(code)
}
fmt.Fprintln(os.Stderr, err)
// If the build failed and a random seed was used,
// the failure might not reproduce with a different seed.
// Print it before we exit.
if flagSeed.random {
fmt.Fprintf(os.Stderr, "random seed: %s\n", base64.RawStdEncoding.EncodeToString(flagSeed.bytes))
}
return 1
}
return 0
}
type errJustExit int
func (e errJustExit) Error() string { return fmt.Sprintf("exit: %d", e) }
// toolchainVersionSemver is a semver-compatible version of the Go toolchain currently
// being used, as reported by "go env GOVERSION".
// Note that the version of Go that built the garble binary might be newer.
var toolchainVersionSemver string
func goVersionOK() bool {
const (
minGoVersionSemver = "v1.18.0"
suggestedGoVersion = "1.18.x"
)
// rxVersion looks for a version like "go1.2" or "go1.2.3"
rxVersion := regexp.MustCompile(`go\d+\.\d+(?:\.\d+)?`)
toolchainVersionFull := cache.GoEnv.GOVERSION
toolchainVersion := rxVersion.FindString(cache.GoEnv.GOVERSION)
if toolchainVersion == "" {
// Go 1.15.x and older do not have GOVERSION yet.
// We could go the extra mile and fetch it via 'go toolchainVersion',
// but we'd have to error anyway.
fmt.Fprintf(os.Stderr, "Go version is too old; please upgrade to Go %s or a newer devel version\n", suggestedGoVersion)
return false
}
toolchainVersionSemver = "v" + strings.TrimPrefix(toolchainVersion, "go")
if semver.Compare(toolchainVersionSemver, minGoVersionSemver) < 0 {
fmt.Fprintf(os.Stderr, "Go version %q is too old; please upgrade to Go %s\n", toolchainVersionFull, suggestedGoVersion)
return false
}
// Ensure that the version of Go that built the garble binary is equal or
// newer than toolchainVersionSemver.
builtVersionFull := os.Getenv("GARBLE_TEST_GOVERSION")
if builtVersionFull == "" {
builtVersionFull = runtime.Version()
}
builtVersion := rxVersion.FindString(builtVersionFull)
if builtVersion == "" {
// If garble built itself, we don't know what Go version was used.
// Fall back to not performing the check against the toolchain version.
return true
}
builtVersionSemver := "v" + strings.TrimPrefix(builtVersion, "go")
if semver.Compare(builtVersionSemver, toolchainVersionSemver) < 0 {
fmt.Fprintf(os.Stderr, "garble was built with %q and is being used with %q; please rebuild garble with the newer version\n",
builtVersionFull, toolchainVersionFull)
return false
}
return true
}
func mainErr(args []string) error {
// If we recognize an argument, we're not running within -toolexec.
switch command, args := args[0], args[1:]; command {
case "help":
if hasHelpFlag(args) || len(args) > 1 {
fmt.Fprintf(os.Stderr, "usage: garble help [command]\n")
return errJustExit(2)
}
if len(args) == 1 {
return mainErr([]string{args[0], "-h"})
}
usage()
return errJustExit(2)
case "version":
if hasHelpFlag(args) || len(args) > 0 {
fmt.Fprintf(os.Stderr, "usage: garble version\n")
return errJustExit(2)
}
info, ok := debug.ReadBuildInfo()
if !ok {
// The build binary was stripped of build info?
// Could be the case if garble built itself.
fmt.Println("unknown")
return nil
}
mod := &info.Main
if mod.Replace != nil {
mod = mod.Replace
}
// For the tests.
if v := os.Getenv("GARBLE_TEST_VERSION"); v != "" {
mod.Version = v
}
if v := os.Getenv("GARBLE_TEST_SUM"); v != "" {
mod.Sum = v
}
if v := os.Getenv("GARBLE_TEST_SETTINGS"); v != "" {
var extra []debug.BuildSetting
if err := json.Unmarshal([]byte(v), &extra); err != nil {
return err
}
info.Settings = append(info.Settings, extra...)
}
fmt.Printf("%s %s %s\n\n", mod.Path, mod.Version, mod.Sum)
fmt.Printf("Build settings:\n")
for _, setting := range info.Settings {
if setting.Value == "" {
continue // do empty build settings even matter?
}
// The padding helps keep readability by aligning:
//
// veryverylong.key value
// short.key some-other-value
//
// Empirically, 16 is enough; the longest key seen is "vcs.revision".
fmt.Printf("%16s %s\n", setting.Key, setting.Value)
}
return nil
case "reverse":
return commandReverse(args)
case "build", "test":
cmd, err := toolexecCmd(command, args)
if err != nil {
return err
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
debugf("calling via toolexec: %s", cmd)
return cmd.Run()
}
if !filepath.IsAbs(args[0]) {
// -toolexec gives us an absolute path to the tool binary to
// run, so this is most likely misuse of garble by a user.
return fmt.Errorf("unknown command: %q", args[0])
}
// 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 := loadSharedCache(); err != nil {
return err
}
_, tool := filepath.Split(args[0])
if runtime.GOOS == "windows" {
tool = strings.TrimSuffix(tool, ".exe")
}
if len(args) == 2 && args[1] == "-V=full" {
return alterToolVersion(tool, args)
}
toolexecImportPath := os.Getenv("TOOLEXEC_IMPORTPATH")
curPkg = cache.ListedPackages[toolexecImportPath]
if curPkg == nil {
return fmt.Errorf("TOOLEXEC_IMPORTPATH not found in listed packages: %s", toolexecImportPath)
}
transform := transformFuncs[tool]
transformed := args[1:]
if transform != nil {
startTime := time.Now()
debugf("transforming %s with args: %s", tool, strings.Join(transformed, " "))
var err error
if transformed, err = transform(transformed); err != nil {
return err
}
debugf("transformed args for %s in %s: %s", tool, debugSince(startTime), strings.Join(transformed, " "))
} else {
debugf("skipping transform on %s with args: %s", tool, strings.Join(transformed, " "))
}
cmd := exec.Command(args[0], transformed...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
func hasHelpFlag(flags []string) bool {
for _, f := range flags {
switch f {
case "-h", "-help", "--help":
return true
}
}
return false
}
// toolexecCmd builds an *exec.Cmd which is set up for running "go <command>"
// with -toolexec=garble and the supplied arguments.
//
// Note that it uses and modifies global state; in general, it should only be
// called once from mainErr in the top-level garble process.
func toolexecCmd(command string, args []string) (*exec.Cmd, error) {
// Split the flags from the package arguments, since we'll need
// to run 'go list' on the same set of packages.
flags, args := splitFlagsFromArgs(args)
if hasHelpFlag(flags) {
out, _ := exec.Command("go", command, "-h").CombinedOutput()
fmt.Fprintf(os.Stderr, `
usage: garble [garble flags] %s [arguments]
This command wraps "go %s". Below is its help:
%s`[1:], command, command, out)
return nil, errJustExit(2)
}
for _, flag := range flags {
if rxGarbleFlag.MatchString(flag) {
return nil, fmt.Errorf("garble flags must precede command, like: garble %s build ./pkg", flag)
}
}
// Here is the only place we initialize the cache.
// The sub-processes will parse it from a shared gob file.
cache = &sharedCache{}
// Note that we also need to pass build flags to 'go list', such
// as -tags.
cache.ForwardBuildFlags, _ = filterForwardBuildFlags(flags)
if command == "test" {
cache.ForwardBuildFlags = append(cache.ForwardBuildFlags, "-test")
}
if err := fetchGoEnv(); err != nil {
return nil, err
}
if !goVersionOK() {
return nil, errJustExit(1)
}
var err error
cache.ExecPath, err = os.Executable()
if err != nil {
return nil, err
}
binaryBuildID, err := buildidOf(cache.ExecPath)
if err != nil {
return nil, err
}
cache.BinaryContentID = decodeHash(splitContentID(binaryBuildID))
if err := appendListedPackages(args, true); err != nil {
return nil, err
}
sharedTempDir, err = saveSharedCache()
if err != nil {
return nil, err
}
os.Setenv("GARBLE_SHARED", sharedTempDir)
defer os.Remove(sharedTempDir)
wd, err := os.Getwd()
if err != nil {
return nil, err
}
os.Setenv("GARBLE_PARENT_WORK", wd)
if flagDebugDir != "" {
if !filepath.IsAbs(flagDebugDir) {
flagDebugDir = filepath.Join(wd, flagDebugDir)
}
if err := os.RemoveAll(flagDebugDir); err != nil {
return nil, fmt.Errorf("could not empty debugdir: %v", err)
}
if err := os.MkdirAll(flagDebugDir, 0o755); err != nil {
return nil, err
}
}
goArgs := []string{
command,
"-trimpath",
"-buildvcs=false",
}
// Pass the garble flags down to each toolexec invocation.
// This way, all garble processes see the same flag values.
var toolexecFlag strings.Builder
toolexecFlag.WriteString("-toolexec=")
toolexecFlag.WriteString(cache.ExecPath)
appendFlags(&toolexecFlag, false)
goArgs = append(goArgs, toolexecFlag.String())
if flagDebugDir != "" {
// In case the user deletes the debug directory,
// and a previous build is cached,
// rebuild all packages to re-fill the debug dir.
goArgs = append(goArgs, "-a")
}
if command == "test" {
// vet is generally not useful on obfuscated code; keep it
// disabled by default.
goArgs = append(goArgs, "-vet=off")
}
goArgs = append(goArgs, flags...)
goArgs = append(goArgs, args...)
return exec.Command("go", goArgs...), nil
}
var transformFuncs = map[string]func([]string) ([]string, error){
"asm": transformAsm,
"compile": transformCompile,
"link": transformLink,
}
func transformAsm(args []string) ([]string, error) {
if !curPkg.ToObfuscate {
return args, nil // we're not obfuscating this package
}
flags, paths := splitFlagsFromFiles(args, ".s")
// When assembling, the import path can make its way into the output object file.
if curPkg.Name != "main" {
flags = flagSetValue(flags, "-p", curPkg.obfuscatedImportPath())
}
flags = alterTrimpath(flags)
// If the assembler is running just for -gensymabis,
// don't obfuscate the source, as we are not assembling yet.
// The assembler will run again later; obfuscating twice is just wasteful.
newPaths := make([]string, 0, len(paths))
if !slices.Contains(args, "-gensymabis") {
for _, path := range paths {
name := filepath.Base(path)
pkgDir := filepath.Join(sharedTempDir, filepath.FromSlash(curPkg.ImportPath))
newPath := filepath.Join(pkgDir, name)
newPaths = append(newPaths, newPath)
}
return append(flags, newPaths...), nil
}
// We need to replace all function references with their obfuscated name
// counterparts.
// Luckily, all func names in Go assembly files are immediately followed
// by the unicode "middle dot", like:
//
// TEXT ·privateAdd(SB),$0-24
const middleDot = '·'
middleDotLen := utf8.RuneLen(middleDot)
for _, path := range paths {
// Read the entire file into memory.
// If we find issues with large files, we can use bufio.
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Find all middle-dot names, and replace them.
remaining := content
var buf bytes.Buffer
for {
i := bytes.IndexRune(remaining, middleDot)
if i < 0 {
buf.Write(remaining)
remaining = nil
break
}
// We want to replace "OP ·foo" and "OP $·foo",
// but not "OP somepkg·foo" just yet.
// "somepkg" is often runtime, syscall, etc.
// We don't obfuscate any of those for now.
//
// TODO: we'll likely need to deal with this
// when we start obfuscating the runtime.
// When we do, note that we can't hash with curPkg.
localName := false
if i >= 0 {
switch remaining[i-1] {
case ' ', '\t', '$':
localName = true
}
}
i += middleDotLen
buf.Write(remaining[:i])
remaining = remaining[i:]
// The name ends at the first rune which cannot be part
// of a Go identifier, such as a comma or space.
nameEnd := 0
for nameEnd < len(remaining) {
c, size := utf8.DecodeRune(remaining[nameEnd:])
if !unicode.IsLetter(c) && c != '_' && !unicode.IsDigit(c) {
break
}
nameEnd += size
}
name := string(remaining[:nameEnd])
remaining = remaining[nameEnd:]
if !localName {
buf.WriteString(name)
continue
}
newName := hashWithPackage(curPkg, name)
debugf("asm name %q hashed with %x to %q", name, curPkg.GarbleActionID, newName)
buf.WriteString(newName)
}
// Uncomment for some quick debugging. Do not delete.
// if curPkg.ToObfuscate {
// fmt.Fprintf(os.Stderr, "\n-- %s --\n%s", path, buf.Bytes())
// }
name := filepath.Base(path)
if path, err := writeTemp(name, buf.Bytes()); err != nil {
return nil, err
} else {
newPaths = append(newPaths, path)
}
}
return append(flags, newPaths...), nil
}
// writeTemp is a mix between os.CreateTemp and os.WriteFile, as it writes a
// named source file in sharedTempDir given an input buffer.
//
// Note that the file is created under a directory tree following curPkg's
// import path, mimicking how files are laid out in modules and GOROOT.
func writeTemp(name string, content []byte) (string, error) {
pkgDir := filepath.Join(sharedTempDir, filepath.FromSlash(curPkg.ImportPath))
if err := os.MkdirAll(pkgDir, 0o777); err != nil {
return "", err
}
dstPath := filepath.Join(pkgDir, name)
if err := writeFileExclusive(dstPath, content); err != nil {
return "", err
}
return dstPath, nil
}
func transformCompile(args []string) ([]string, error) {
var err error
flags, paths := splitFlagsFromFiles(args, ".go")
// We will force the linker to drop DWARF via -w, so don't spend time
// generating it.
flags = append(flags, "-dwarf=false")
for i, path := range paths {
if filepath.Base(path) == "_gomod_.go" {
// never include module info
// TODO: this seems to no longer trigger for our tests?
paths = append(paths[:i], paths[i+1:]...)
break
}
}
var files []*ast.File
for _, path := range paths {
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
files = append(files, file)
}
tf := newTransformer()
if err := tf.typecheck(files); err != nil {
return nil, err
}
flags = alterTrimpath(flags)
// Note that if the file already exists in the cache from another build,
// we don't need to write to it again thanks to the hash.
// TODO: as an optimization, just load that one gob file.
if err := loadCachedOutputs(); err != nil {
return nil, err
}
tf.findReflectFunctions(files)
newImportCfg, err := processImportCfg(flags)
if err != nil {
return nil, err
}
// Literal obfuscation uses math/rand, so seed it deterministically.
randSeed := curPkg.GarbleActionID
if flagSeed.present() {
randSeed = flagSeed.bytes
}
// debugf("seeding math/rand with %x\n", randSeed)
mathrand.Seed(int64(binary.BigEndian.Uint64(randSeed)))
if err := tf.prefillObjectMaps(files); err != nil {
return nil, err
}
// If this is a package to obfuscate, swap the -p flag with the new package path.
// We don't if it's the main package, as that just uses "-p main".
// We only set newPkgPath if we're obfuscating the import path,
// to replace the original package name in the package clause below.
newPkgPath := ""
if curPkg.Name != "main" && curPkg.ToObfuscate {
newPkgPath = curPkg.obfuscatedImportPath()
flags = flagSetValue(flags, "-p", newPkgPath)
}
newPaths := make([]string, 0, len(files))
for i, file := range files {
filename := filepath.Base(paths[i])
debugf("obfuscating %s", filename)
if curPkg.ImportPath == "runtime" && flagTiny {
// strip unneeded runtime code
stripRuntime(filename, file)
tf.removeUnnecessaryImports(file)
}
tf.handleDirectives(file.Comments)
file = tf.transformGo(file)
if newPkgPath != "" {
file.Name.Name = newPkgPath
}
src, err := printFile(file)
if err != nil {
return nil, err
}
// It is possible to end up in an edge case where two instances of the
// same package have different Action IDs, but their obfuscation and
// builds produce exactly the same results.
// In such an edge case, Go's build cache is smart enough for the second
// instance to reuse the first's build artifact.
// However, garble's caching via garbleExportFile is not as smart,
// as we base the location of these files purely based on Action IDs.
// Thus, the incremental build can fail to find garble's cached file.
// To sidestep this bug entirely, ensure that different action IDs never
// produce the same cached output when building with garble.
// Note that this edge case tends to happen when a -seed is provided,
// as then a package's Action ID is not used as an obfuscation seed.
// TODO(mvdan): replace this workaround with an actual fix if we can.
// This workaround is presumably worse on the build cache,
// as we end up with extra near-duplicate cached artifacts.
if i == 0 {
src = append(src, fmt.Sprintf(
"\nvar garbleActionID = %q\n", hashToString(curPkg.GarbleActionID),
)...)
}
// Uncomment for some quick debugging. Do not delete.
// if curPkg.ToObfuscate {
// fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, filename, src)
// }
if path, err := writeTemp(filename, src); err != nil {
return nil, err
} else {
newPaths = append(newPaths, path)
}
if flagDebugDir != "" {
osPkgPath := filepath.FromSlash(curPkg.ImportPath)
pkgDebugDir := filepath.Join(flagDebugDir, osPkgPath)
if err := os.MkdirAll(pkgDebugDir, 0o755); err != nil {
return nil, err
}
debugFilePath := filepath.Join(pkgDebugDir, filename)
if err := os.WriteFile(debugFilePath, src, 0o666); err != nil {
return nil, err
}
}
}
flags = flagSetValue(flags, "-importcfg", newImportCfg)
if err := writeGobExclusive(
garbleExportFile(curPkg),
cachedOutput,
); err != nil && !errors.Is(err, fs.ErrExist) {
return nil, err
}
return append(flags, newPaths...), nil
}
// handleDirectives looks at all the comments in a file containing build
// directives, and does the necessary for the obfuscation process to work.
//
// Right now, this means recording what local names are used with go:linkname,
// and rewriting those directives to use obfuscated name from other packages.
func (tf *transformer) handleDirectives(comments []*ast.CommentGroup) {
for _, group := range comments {
for _, comment := range group.List {
if !strings.HasPrefix(comment.Text, "//go:linkname ") {
continue
}
fields := strings.Fields(comment.Text)
if len(fields) != 3 {
// TODO: the 2nd argument is optional, handle when it's not present
continue
}
// This directive has two arguments: "go:linkname localName newName"
// obfuscate the local name, if the current package is obfuscated
if curPkg.ToObfuscate {
fields[1] = hashWithPackage(curPkg, fields[1])
}
// If the new name is of the form "pkgpath.Name", and
// we've obfuscated "Name" in that package, rewrite the
// directive to use the obfuscated name.
newName := fields[2]
dotCnt := strings.Count(newName, ".")
if dotCnt < 1 {
// cgo-generated code uses linknames to made up symbol names,
// which do not have a package path at all.
// Replace the comment in case the local name was obfuscated.
comment.Text = strings.Join(fields, " ")
continue
}
switch newName {
case "main.main", "main..inittask", "runtime..inittask":
// The runtime uses some special symbols with "..".
// We aren't touching those at the moment.
continue
}
// If the package path has multiple dots, split on the
// last one.
lastDotIdx := strings.LastIndex(newName, ".")
pkgPath, name := newName[:lastDotIdx], newName[lastDotIdx+1:]
lpkg, err := listPackage(pkgPath)
if err != nil {
// Probably a made up name like above, but with a dot.
comment.Text = strings.Join(fields, " ")
continue
}
if lpkg.ToObfuscate {
// The name exists and was obfuscated; obfuscate
// the new name.
newName := hashWithPackage(lpkg, name)
newPkgPath := pkgPath
if pkgPath != "main" {
newPkgPath = lpkg.obfuscatedImportPath()
}
fields[2] = newPkgPath + "." + newName
}
comment.Text = strings.Join(fields, " ")
}
}
}
// processImportCfg parses the importcfg file passed to a compile or link step.
// It also builds a new importcfg file to account for obfuscated import paths.
func processImportCfg(flags []string) (newImportCfg string, _ error) {
importCfg := flagValue(flags, "-importcfg")
if importCfg == "" {
return "", fmt.Errorf("could not find -importcfg argument")
}
data, err := os.ReadFile(importCfg)
if err != nil {
return "", err
}
var packagefiles, importmaps [][2]string
for _, line := range strings.Split(string(data), "\n") {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
verb, args, found := strings.Cut(line, " ")
if !found {
continue
}
switch verb {
case "importmap":
beforePath, afterPath, found := strings.Cut(args, "=")
if !found {
continue
}
importmaps = append(importmaps, [2]string{beforePath, afterPath})
case "packagefile":
importPath, objectPath, found := strings.Cut(args, "=")
if !found {
continue
}
packagefiles = append(packagefiles, [2]string{importPath, objectPath})
}
}
// Produce the modified importcfg file.
// This is mainly replacing the obfuscated paths.
// Note that we range over maps, so this is non-deterministic, but that
// should not matter as the file is treated like a lookup table.
newCfg, err := os.CreateTemp(sharedTempDir, "importcfg")
if err != nil {
return "", err
}
for _, pair := range importmaps {
beforePath, afterPath := pair[0], pair[1]
lpkg, err := listPackage(beforePath)
if err != nil {
panic(err) // shouldn't happen
}
if lpkg.ToObfuscate {
// Note that beforePath is not the canonical path.
// For beforePath="vendor/foo", afterPath and
// lpkg.ImportPath can be just "foo".
// Don't use obfuscatedImportPath here.
beforePath = hashWithPackage(lpkg, beforePath)
afterPath = lpkg.obfuscatedImportPath()
}
fmt.Fprintf(newCfg, "importmap %s=%s\n", beforePath, afterPath)
}
for _, pair := range packagefiles {
impPath, pkgfile := pair[0], pair[1]
lpkg, err := listPackage(impPath)
if err != nil {
// TODO: it's unclear why an importcfg can include an import path
// that's not a dependency in an edge case with "go test ./...".
// See exporttest/*.go in testdata/scripts/test.txt.
// For now, spot the pattern and avoid the unnecessary error;
// the dependency is unused, so the packagefile line is redundant.
if strings.HasSuffix(curPkg.ImportPath, ".test]") && strings.HasPrefix(curPkg.ImportPath, impPath) {
continue
}
panic(err) // shouldn't happen
}
if lpkg.Name != "main" {
impPath = lpkg.obfuscatedImportPath()
}
fmt.Fprintf(newCfg, "packagefile %s=%s\n", impPath, pkgfile)
}
// Uncomment to debug the transformed importcfg. Do not delete.
// newCfg.Seek(0, 0)
// io.Copy(os.Stderr, newCfg)
if err := newCfg.Close(); err != nil {
return "", err
}
return newCfg.Name(), nil
}
type (
funcFullName = string // as per go/types.Func.FullName
objectString = string // as per recordedObjectString
reflectParameter struct {
Position int // 0-indexed
Variadic bool // ...int
}
typeName struct {
PkgPath, Name string
}
)
// TODO: read-write globals like these should probably be inside transformer
// knownCannotObfuscateUnexported is like KnownCannotObfuscate but for
// unexported names. We don't need to store this in the build cache,
// because these names cannot be referenced by downstream packages.
var knownCannotObfuscateUnexported = map[types.Object]bool{}
// cachedOutput contains information that will be stored as per garbleExportFile.
var cachedOutput = struct {
// KnownReflectAPIs is a static record of what std APIs use reflection on their
// parameters, so we can avoid obfuscating types used with them.
//
// TODO: we're not including fmt.Printf, as it would have many false positives,
// unless we were smart enough to detect which arguments get used as %#v or %T.
KnownReflectAPIs map[funcFullName][]reflectParameter
// KnownCannotObfuscate is filled with the fully qualified names from each
// package that we could not obfuscate as per cannotObfuscateNames.
// This record is necessary for knowing what names from imported packages
// weren't obfuscated, so we can obfuscate their local uses accordingly.
KnownCannotObfuscate map[objectString]struct{}
// KnownEmbeddedAliasFields records which embedded fields use a type alias.
// They are the only instance where a type alias matters for obfuscation,
// because the embedded field name is derived from the type alias itself,
// and not the type that the alias points to.
// In that way, the type alias is obfuscated as a form of named type,
// bearing in mind that it may be owned by a different package.
KnownEmbeddedAliasFields map[objectString]typeName
}{
KnownReflectAPIs: map[funcFullName][]reflectParameter{
"reflect.TypeOf": {{Position: 0, Variadic: false}},
"reflect.ValueOf": {{Position: 0, Variadic: false}},
},
KnownCannotObfuscate: map[objectString]struct{}{},
KnownEmbeddedAliasFields: map[objectString]typeName{},
}
// garbleExportFile returns an absolute path to a build cache entry
// which belongs to garble and corresponds to the given Go package.
//
// Unlike pkg.Export, it is only read and written by garble itself.
// Also unlike pkg.Export, it includes GarbleActionID,
// so its path will change if the obfuscated build changes.
//
// The purpose of such a file is to store garble-specific information
// in the build cache, to be reused at a later time.
// The file should have the same lifetime as pkg.Export,
// as it lives under the same cache directory that gets trimmed automatically.
func garbleExportFile(pkg *listedPackage) string {
trimmed := strings.TrimSuffix(pkg.Export, "-d")
if trimmed == pkg.Export {
panic(fmt.Sprintf("unexpected export path of %s: %q", pkg.ImportPath, pkg.Export))
}
return trimmed + "-garble-" + hashToString(pkg.GarbleActionID) + "-d"
}
func loadCachedOutputs() error {
startTime := time.Now()
loaded := 0
for _, path := range curPkg.Deps {
pkg, err := listPackage(path)
if err != nil {
panic(err) // shouldn't happen
}
if pkg.Export == "" {
continue // nothing to load
}
// this function literal is used for the deferred close
if err := func() error {
filename := garbleExportFile(pkg)
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// Decode appends new entries to the existing maps
if err := gob.NewDecoder(f).Decode(&cachedOutput); err != nil {
return fmt.Errorf("gob decode: %w", err)
}
return nil
}(); err != nil {
return fmt.Errorf("cannot load garble export file for %s: %w", path, err)
}
loaded++
}
debugf("%d cached output files loaded in %s", loaded, debugSince(startTime))
return nil
}
func (tf *transformer) findReflectFunctions(files []*ast.File) {
visitFuncDecl := func(funcDecl *ast.FuncDecl) {
funcObj := tf.info.ObjectOf(funcDecl.Name).(*types.Func)
funcType := funcObj.Type().(*types.Signature)
funcParams := funcType.Params()
seenReflectParams := make(map[*types.Var]bool)
for i := 0; i < funcParams.Len(); i++ {
seenReflectParams[funcParams.At(i)] = false
}
ast.Inspect(funcDecl, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
calledFunc, _ := tf.info.ObjectOf(sel.Sel).(*types.Func)
if calledFunc == nil || calledFunc.Pkg() == nil {
return true
}
fullName := calledFunc.FullName()
for _, reflectParam := range cachedOutput.KnownReflectAPIs[fullName] {
// We need a range to handle any number of variadic arguments,
// which could be 0 or multiple.
// The non-variadic case is always one argument,
// but we still use the range to deduplicate code.
argStart := reflectParam.Position
argEnd := argStart + 1
if reflectParam.Variadic {
argEnd = len(call.Args)
}
for _, arg := range call.Args[argStart:argEnd] {
ident, ok := arg.(*ast.Ident)
if !ok {
continue
}
obj, _ := tf.info.ObjectOf(ident).(*types.Var)
if obj == nil {
continue
}
if _, ok := seenReflectParams[obj]; ok {
seenReflectParams[obj] = true
}
}
}
var reflectParams []reflectParameter
for i := 0; i < funcParams.Len(); i++ {
if seenReflectParams[funcParams.At(i)] {
reflectParams = append(reflectParams, reflectParameter{
Position: i,
Variadic: funcType.Variadic() && i == funcParams.Len()-1,
})
}
}
if len(reflectParams) > 0 {
cachedOutput.KnownReflectAPIs[funcObj.FullName()] = reflectParams
}
return true
})
}
lenPrevKnownReflectAPIs := len(cachedOutput.KnownReflectAPIs)
for _, file := range files {
for _, decl := range file.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok {
visitFuncDecl(decl)
}
}
}
// if a new reflectAPI is found we need to Re-evaluate all functions which might be using that API
if len(cachedOutput.KnownReflectAPIs) > lenPrevKnownReflectAPIs {
tf.findReflectFunctions(files)
}
}
// cmd/bundle will include a go:generate directive in its output by default.
// Ours specifies a version and doesn't assume bundle is in $PATH, so drop it.
//go:generate go run golang.org/x/tools/cmd/bundle@v0.1.9 -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted
//go:generate sed -i /go:generate/d cmdgo_quoted.go
// prefillObjectMaps collects objects which should not be obfuscated,
// such as those used as arguments to reflect.TypeOf or reflect.ValueOf.
// Since we obfuscate one package at a time, we only detect those if the type
// definition and the reflect usage are both in the same package.
func (tf *transformer) prefillObjectMaps(files []*ast.File) error {
tf.linkerVariableStrings = make(map[types.Object]string)
// TODO: this is a linker flag that affects how we obfuscate a package at
// compile time. Note that, if the user changes ldflags, then Go may only
// re-link the final binary, without re-compiling any packages at all.
// It's possible that this could result in:
//
// garble -literals build -ldflags=-X=pkg.name=before # name="before"
// garble -literals build -ldflags=-X=pkg.name=after # name="before" as cached
//
// We haven't been able to reproduce this problem for now,
// but it's worth noting it and keeping an eye out for it in the future.
// If we do confirm this theoretical bug,
// the solution will be to either find a different solution for -literals,
// or to force including -ldflags into the build cache key.
ldflags, err := cmdgoQuotedSplit(flagValue(cache.ForwardBuildFlags, "-ldflags"))
if err != nil {
return err
}
flagValueIter(ldflags, "-X", func(val string) {
// val is in the form of "foo.com/bar.name=value".
fullName, stringValue, found := strings.Cut(val, "=")
if !found {
return // invalid
}
// fullName is "foo.com/bar.name"
i := strings.LastIndexByte(fullName, '.')
path, name := fullName[:i], fullName[i+1:]
// -X represents the main package as "main", not its import path.
if path != curPkg.ImportPath && !(path == "main" && curPkg.Name == "main") {
return // not the current package
}
obj := tf.pkg.Scope().Lookup(name)
if obj == nil {
return // not found; skip
}
tf.linkerVariableStrings[obj] = stringValue
})
visit := func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
ident, ok := call.Fun.(*ast.Ident)
if !ok {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
ident = sel.Sel
}
fnType, _ := tf.info.ObjectOf(ident).(*types.Func)
if fnType == nil || fnType.Pkg() == nil {
return true
}
fullName := fnType.FullName()
for _, reflectParam := range cachedOutput.KnownReflectAPIs[fullName] {
argStart := reflectParam.Position
argEnd := argStart + 1
if reflectParam.Variadic {
argEnd = len(call.Args)
}
for _, arg := range call.Args[argStart:argEnd] {
argType := tf.info.TypeOf(arg)
tf.recursivelyRecordAsNotObfuscated(argType)
}
}
return true
}
for _, file := range files {
for _, group := range file.Comments {
for _, comment := range group.List {
name := strings.TrimPrefix(comment.Text, "//export ")
if name == comment.Text {
continue
}
name = strings.TrimSpace(name)
obj := tf.pkg.Scope().Lookup(name)
if obj == nil {
continue // not found; skip
}
// TODO(mvdan): it seems like removing this doesn't break any tests.
recordAsNotObfuscated(obj)
}
}
ast.Inspect(file, visit)
}
return nil
}
// transformer holds all the information and state necessary to obfuscate a
// single Go package.
type transformer struct {
// The type-checking results; the package itself, and the Info struct.
pkg *types.Package
info *types.Info
// linkerVariableStrings is also initialized by prefillObjectMaps.
// It records objects for variables used in -ldflags=-X flags,
// as well as the strings the user wants to inject them with.
linkerVariableStrings map[types.Object]string
// recordTypeDone helps avoid cycles in recordType.
recordTypeDone map[types.Type]bool
// fieldToStruct helps locate struct types from any of their field
// objects. Useful when obfuscating field names.
fieldToStruct map[*types.Var]*types.Struct
}
// newTransformer helps initialize some maps.
func newTransformer() *transformer {
return &transformer{
info: &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
},
recordTypeDone: make(map[types.Type]bool),
fieldToStruct: make(map[*types.Var]*types.Struct),
}
}
func (tf *transformer) typecheck(files []*ast.File) error {
origTypesConfig := types.Config{Importer: origImporter}
pkg, err := origTypesConfig.Check(curPkg.ImportPath, fset, files, tf.info)
if err != nil {
return fmt.Errorf("typecheck error: %v", err)
}
tf.pkg = pkg
// Run recordType on all types reachable via types.Info.
// A bit hacky, but I could not find an easier way to do this.
for _, obj := range tf.info.Defs {
if obj != nil {
tf.recordType(obj.Type())
}
}
for name, obj := range tf.info.Uses {
if obj == nil {
continue
}
tf.recordType(obj.Type())
// Record into KnownEmbeddedAliasFields.
obj, ok := obj.(*types.TypeName)
if !ok || !obj.IsAlias() {
continue
}
vr, _ := tf.info.Defs[name].(*types.Var)
if vr == nil || !vr.Embedded() {
continue
}
vrStr := recordedObjectString(vr)
if vrStr == "" {
continue
}
aliasTypeName := typeName{
PkgPath: obj.Pkg().Path(),
Name: obj.Name(),
}
cachedOutput.KnownEmbeddedAliasFields[vrStr] = aliasTypeName
}
for _, tv := range tf.info.Types {
tf.recordType(tv.Type)
}
return nil
}
// recordType visits every reachable type after typechecking a package.
// Right now, all it does is fill the fieldToStruct field.
// Since types can be recursive, we need a map to avoid cycles.
func (tf *transformer) recordType(t types.Type) {
if tf.recordTypeDone[t] {
return
}
tf.recordTypeDone[t] = true
switch t := t.(type) {
case interface{ Elem() types.Type }:
tf.recordType(t.Elem())
case *types.Named:
tf.recordType(t.Underlying())
}
strct, _ := t.(*types.Struct)
if strct == nil {
return
}
for i := 0; i < strct.NumFields(); i++ {
field := strct.Field(i)
tf.fieldToStruct[field] = strct
if field.Embedded() {
tf.recordType(field.Type())
}
}
}
// TODO: consider caching recordedObjectString via a map,
// if that shows an improvement in our benchmark
func recordedObjectString(obj types.Object) objectString {
if obj, ok := obj.(*types.Var); ok && obj.IsField() {
// For exported fields, "pkgpath.Field" is not unique,
// because two exported top-level types could share "Field".
//
// Moreover, note that not all fields belong to named struct types;
// an API could be exposing:
//
// var usedInReflection = struct{Field string}
//
// For now, a hack: assume that packages don't declare the same field
// more than once in the same line. This works in practice, but one
// could craft Go code to break this assumption.
// Also note that the compiler's object files include filenames and line
// numbers, but not column numbers nor byte offsets.
// TODO(mvdan): give this another think, and add tests involving anon types.
pos := fset.Position(obj.Pos())
return fmt.Sprintf("%s.%s - %s:%d", obj.Pkg().Path(), obj.Name(),
filepath.Base(pos.Filename), pos.Line)
}
// Names which are not at the top level cannot be imported,
// so we don't need to record them either.
// Note that this doesn't apply to fields, which are never top-level.
if obj.Pkg().Scope().Lookup(obj.Name()) != obj {
return ""
}
// For top-level exported names, "pkgpath.Name" is unique.
return fmt.Sprintf("%s.%s", obj.Pkg().Path(), obj.Name())
}
// recordAsNotObfuscated records all the objects whose names we cannot obfuscate.
// An object is any named entity, such as a declared variable or type.
//
// So far, it records:
//
// - Types which are used for reflection.
// - Declarations exported via "//export".
// - Types or variables from external packages which were not obfuscated.
func recordAsNotObfuscated(obj types.Object) {
if obj.Pkg().Path() != curPkg.ImportPath {
panic("called recordedAsNotObfuscated with a foreign object")
}
if !obj.Exported() {
// Unexported names will never be used by other packages,
// so we don't need to bother recording them in cachedOutput.
knownCannotObfuscateUnexported[obj] = true
return
}
objStr := recordedObjectString(obj)
if objStr == "" {
// If the object can't be described via a qualified string,
// then other packages can't use it.
// TODO: should we still record it in knownCannotObfuscateUnexported?
return
}
cachedOutput.KnownCannotObfuscate[objStr] = struct{}{}
}
func recordedAsNotObfuscated(obj types.Object) bool {
if knownCannotObfuscateUnexported[obj] {
return true
}
objStr := recordedObjectString(obj)
if objStr == "" {
return false
}
_, ok := cachedOutput.KnownCannotObfuscate[objStr]
return ok
}
func (tf *transformer) removeUnnecessaryImports(file *ast.File) {
usedImports := make(map[string]bool)
ast.Inspect(file, func(n ast.Node) bool {
node, ok := n.(*ast.Ident)
if !ok {
return true
}
uses, ok := tf.info.Uses[node].(*types.PkgName)
if !ok {
return true
}
usedImports[uses.Imported().Path()] = true
return true
})
for _, imp := range file.Imports {
if imp.Name != nil && (imp.Name.Name == "_" || imp.Name.Name == ".") {
continue
}
path, err := strconv.Unquote(imp.Path.Value)
if err != nil {
panic(err)
}
// The import path can't be used directly here, because the actual
// path resolved via go/types might be different from the naive path.
lpkg, err := listPackage(path)
if err != nil {
panic(err)
}
if usedImports[lpkg.ImportPath] {
continue
}
imp.Name = ast.NewIdent("_")
}
}
// transformGo obfuscates the provided Go syntax file.
func (tf *transformer) transformGo(file *ast.File) *ast.File {
// Only obfuscate the literals here if the flag is on
// and if the package in question is to be obfuscated.
//
// We can't obfuscate literals in the runtime and its dependencies,
// because obfuscated literals sometimes escape to heap,
// and that's not allowed in the runtime itself.
if flagLiterals && curPkg.ToObfuscate {
file = literals.Obfuscate(file, tf.info, fset, tf.linkerVariableStrings)
// some imported constants might not be needed anymore, remove unnecessary imports
tf.removeUnnecessaryImports(file)
}
pre := func(cursor *astutil.Cursor) bool {
node, ok := cursor.Node().(*ast.Ident)
if !ok {
return true
}
name := node.Name
if name == "_" {
return true // unnamed remains unnamed
}
obj := tf.info.ObjectOf(node)
if obj == nil {
_, isImplicit := tf.info.Defs[node]
_, parentIsFile := cursor.Parent().(*ast.File)
if !isImplicit || parentIsFile {
// We only care about nil objects in the switch scenario below.
return true
}
// In a type switch like "switch foo := bar.(type) {",
// "foo" is being declared as a symbolic variable,
// as it is only actually declared in each "case SomeType:".
//
// As such, the symbolic "foo" in the syntax tree has no object,
// but it is still recorded under Defs with a nil value.
// We still want to obfuscate that syntax tree identifier,
// so if we detect the case, create a dummy types.Var for it.
//
// Note that "package mypkg" also denotes a nil object in Defs,
// and we don't want to treat that "mypkg" as a variable,
// so avoid that case by checking the type of cursor.Parent.
obj = types.NewVar(node.Pos(), tf.pkg, name, nil)
}
pkg := obj.Pkg()
if vr, ok := obj.(*types.Var); ok && vr.Embedded() {
// The docs for ObjectOf say:
//
// If id is an embedded struct field, ObjectOf returns the
// field (*Var) it defines, not the type (*TypeName) it uses.
//
// If this embedded field is a type alias, we want to
// handle the alias's TypeName instead of treating it as
// the type the alias points to.
//
// Alternatively, if we don't have an alias, we still want to
// use the embedded type, not the field.
vrStr := recordedObjectString(vr)
aliasTypeName, ok := cachedOutput.KnownEmbeddedAliasFields[vrStr]
if ok {
pkg2 := tf.pkg
if path := aliasTypeName.PkgPath; pkg2.Path() != path {
// If the package is a dependency, import it.
// We can't grab the package via tf.pkg.Imports,
// because some of the packages under there are incomplete.
// ImportFrom will cache complete imports, anyway.
var err error
pkg2, err = origImporter.ImportFrom(path, parentWorkDir, 0)
if err != nil {
panic(err)
}
}
tname, ok := pkg2.Scope().Lookup(aliasTypeName.Name).(*types.TypeName)
if !ok || !tname.IsAlias() {
if !ok {
panic(fmt.Sprintf("KnownEmbeddedAliasFields pointed %q to a missing type %q", vrStr, aliasTypeName))
}
panic(fmt.Sprintf("KnownEmbeddedAliasFields pointed %q to a non-alias type %q", vrStr, aliasTypeName))
}
obj = tname
} else {
named := namedType(obj.Type())
if named == nil {
return true // unnamed type (probably a basic type, e.g. int)
}
obj = named.Obj()
}
pkg = obj.Pkg()
}
if pkg == nil {
return true // universe scope
}
if pkg.Path() == "embed" {
// The Go compiler needs to detect types such as embed.FS.
// That will fail if we change the import path or type name.
// Leave it as is.
// Luckily, the embed package just declares the FS type.
return true
}
// The package that declared this object did not obfuscate it.
if recordedAsNotObfuscated(obj) {
return true
}
// TODO(mvdan): investigate obfuscating these too.
filename := fset.Position(obj.Pos()).Filename
if strings.HasPrefix(filename, "_cgo_") || strings.Contains(filename, ".cgo1.") {
return true
}
path := pkg.Path()
lpkg, err := listPackage(path)
if err != nil {
panic(err) // shouldn't happen
}
if !lpkg.ToObfuscate {
return true // we're not obfuscating this package
}
hashToUse := lpkg.GarbleActionID
debugName := "variable"
// debugf("%s: %#v %T", fset.Position(node.Pos()), node, obj)
switch obj := obj.(type) {
case *types.Var:
if !obj.IsField() {
// Identifiers denoting variables are always obfuscated.
break
}
debugName = "field"
// From this point on, we deal with struct fields.
// Fields don't get hashed with the package's action ID.
// They get hashed with the type of their parent struct.
// This is because one struct can be converted to another,
// as long as the underlying types are identical,
// even if the structs are defined in different packages.
//
// TODO: Consider only doing this for structs where all
// fields are exported. We only need this special case
// for cross-package conversions, which can't work if
// any field is unexported. If that is done, add a test
// that ensures unexported fields from different
// packages result in different obfuscated names.
strct := tf.fieldToStruct[obj]
if strct == nil {
panic("could not find for " + name)
}
node.Name = hashWithStruct(strct, name)
debugf("%s %q hashed with struct fields to %q", debugName, name, node.Name)
return true
case *types.TypeName:
debugName = "type"
case *types.Func:
sign := obj.Type().(*types.Signature)
if sign.Recv() == nil {
debugName = "func"
} else {
debugName = "method"
}
if obj.Exported() && sign.Recv() != nil {
return true // might implement an interface
}
switch name {
case "main", "init", "TestMain":
return true // don't break them
}
if strings.HasPrefix(name, "Test") && isTestSignature(sign) {
return true // don't break tests
}
default:
return true // we only want to rename the above
}
node.Name = hashWithPackage(lpkg, name)
// TODO: probably move the debugf lines inside the hash funcs
debugf("%s %q hashed with %x… to %q", debugName, name, hashToUse[:4], node.Name)
return true
}
post := func(cursor *astutil.Cursor) bool {
imp, ok := cursor.Node().(*ast.ImportSpec)
if !ok {
return true
}
path, err := strconv.Unquote(imp.Path.Value)
if err != nil {
panic(err) // should never happen
}
// We're importing an obfuscated package.
// 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.
lpkg, err := listPackage(path)
if err != nil {
panic(err) // should never happen
}
if !lpkg.ToObfuscate {
return true
}
newPath := lpkg.obfuscatedImportPath()
imp.Path.Value = strconv.Quote(newPath)
if imp.Name == nil {
imp.Name = &ast.Ident{Name: lpkg.Name}
}
return true
}
return astutil.Apply(file, pre, post).(*ast.File)
}
// recursivelyRecordAsNotObfuscated calls recordAsNotObfuscated on any named
// types and fields under typ.
//
// Only the names declared in the current package are recorded. This is to ensure
// that reflection detection only happens within the package declaring a type.
// Detecting it in downstream packages could result in inconsistencies.
func (tf *transformer) recursivelyRecordAsNotObfuscated(t types.Type) {
switch t := t.(type) {
case *types.Named:
obj := t.Obj()
if obj.Pkg() == nil || obj.Pkg() != tf.pkg {
return // not from the specified package
}
if recordedAsNotObfuscated(obj) {
return // prevent endless recursion
}
recordAsNotObfuscated(obj)
// Record the underlying type, too.
tf.recursivelyRecordAsNotObfuscated(t.Underlying())
case *types.Struct:
for i := 0; i < t.NumFields(); i++ {
field := t.Field(i)
// This check is similar to the one in *types.Named.
// It's necessary for unnamed struct types,
// as they aren't named but still have named fields.
if field.Pkg() == nil || field.Pkg() != tf.pkg {
return // not from the specified package
}
// Record the field itself, too.
recordAsNotObfuscated(field)
tf.recursivelyRecordAsNotObfuscated(field.Type())
}
case interface{ Elem() types.Type }:
// Get past pointers, slices, etc.
tf.recursivelyRecordAsNotObfuscated(t.Elem())
}
}
// named tries to obtain the *types.Named behind a type, if there is one.
// This is useful to obtain "testing.T" from "*testing.T", or to obtain the type
// declaration object from an embedded field.
func namedType(t types.Type) *types.Named {
switch t := t.(type) {
case *types.Named:
return t
case interface{ Elem() types.Type }:
return namedType(t.Elem())
default:
return nil
}
}
// isTestSignature returns true if the signature matches "func _(*testing.T)".
func isTestSignature(sign *types.Signature) bool {
if sign.Recv() != nil {
return false // test funcs don't have receivers
}
params := sign.Params()
if params.Len() != 1 {
return false // too many parameters for a test func
}
named := namedType(params.At(0).Type())
if named == nil {
return false // the only parameter isn't named, like "string"
}
obj := named.Obj()
return obj != nil && obj.Pkg().Path() == "testing" && obj.Name() == "T"
}
func transformLink(args []string) ([]string, error) {
// We can't split by the ".a" extension, because cached object files
// lack any extension.
flags, args := splitFlagsFromArgs(args)
newImportCfg, err := processImportCfg(flags)
if err != nil {
return nil, err
}
// TODO: unify this logic with the -X handling when using -literals.
// We should be able to handle both cases via the syntax tree.
//
// Make sure -X works with obfuscated identifiers.
// To cover both obfuscated and non-obfuscated names,
// duplicate each flag with a obfuscated version.
flagValueIter(flags, "-X", func(val string) {
// val is in the form of "foo.com/bar.name=value".
fullName, stringValue, found := strings.Cut(val, "=")
if !found {
return // invalid
}
// fullName is "foo.com/bar.name"
i := strings.LastIndexByte(fullName, '.')
path, name := fullName[:i], fullName[i+1:]
// If the package path is "main", it's the current top-level
// package we are linking.
// Otherwise, find it in the cache.
lpkg := curPkg
if path != "main" {
lpkg = cache.ListedPackages[path]
}
if lpkg == nil {
// We couldn't find the package.
// Perhaps a typo, perhaps not part of the build.
// cmd/link ignores those, so we should too.
return
}
// As before, the main package must remain as "main".
newPath := path
if path != "main" {
newPath = lpkg.obfuscatedImportPath()
}
newName := hashWithPackage(lpkg, name)
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", newPath, newName, stringValue))
})
// Starting in Go 1.17, Go's version is implicitly injected by the linker.
// It's the same method as -X, so we can override it with an extra flag.
flags = append(flags, "-X=runtime.buildVersion=unknown")
// Ensure we strip the -buildid flag, to not leak any build IDs for the
// link operation or the main package's compilation.
flags = flagSetValue(flags, "-buildid", "")
// Strip debug information and symbol tables.
flags = append(flags, "-w", "-s")
flags = flagSetValue(flags, "-importcfg", newImportCfg)
return append(flags, args...), nil
}
func splitFlagsFromArgs(all []string) (flags, args []string) {
for i := 0; i < len(all); i++ {
arg := all[i]
if !strings.HasPrefix(arg, "-") {
return all[:i:i], all[i:]
}
if booleanFlags[arg] || strings.Contains(arg, "=") {
// Either "-bool" or "-name=value".
continue
}
// "-name value", so the next arg is part of this flag.
i++
}
return all, nil
}
func alterTrimpath(flags []string) []string {
// If the value of -trimpath doesn't contain the separator ';', the 'go
// build' command is most likely not using '-trimpath'.
trimpath := flagValue(flags, "-trimpath")
// Add our temporary dir to the beginning of -trimpath, so that we don't
// leak temporary dirs. Needs to be at the beginning, since there may be
// shorter prefixes later in the list, such as $PWD if TMPDIR=$PWD/tmp.
return flagSetValue(flags, "-trimpath", sharedTempDir+"=>;"+trimpath)
}
// forwardBuildFlags is obtained from 'go help build' as of Go 1.18beta1.
var forwardBuildFlags = map[string]bool{
// These shouldn't be used in nested cmd/go calls.
"-a": false,
"-n": false,
"-x": false,
"-v": false,
// These are always set by garble.
"-trimpath": false,
"-toolexec": false,
"-buildvcs": false,
"-p": true,
"-race": true,
"-msan": true,
"-asan": true,
"-work": true,
"-asmflags": true,
"-buildmode": true,
"-compiler": true,
"-gccgoflags": true,
"-gcflags": true,
"-installsuffix": true,
"-ldflags": true,
"-linkshared": true,
"-mod": true,
"-modcacherw": true,
"-modfile": true,
"-pkgdir": true,
"-tags": true,
"-workfile": true,
"-overlay": true,
}
// booleanFlags is obtained from 'go help build' and 'go help testflag' as of Go 1.18beta1.
var booleanFlags = map[string]bool{
// Shared build flags.
"-a": true,
"-i": true,
"-n": true,
"-v": true,
"-x": true,
"-race": true,
"-msan": true,
"-asan": true,
"-linkshared": true,
"-modcacherw": true,
"-trimpath": true,
"-buildvcs": true,
// Test flags (TODO: support its special -args flag)
"-c": true,
"-json": true,
"-cover": true,
"-failfast": true,
"-short": true,
"-benchmem": true,
}
func filterForwardBuildFlags(flags []string) (filtered []string, firstUnknown string) {
for i := 0; i < len(flags); i++ {
arg := flags[i]
if strings.HasPrefix(arg, "--") {
arg = arg[1:] // "--name" to "-name"; keep the short form
}
name, _, _ := strings.Cut(arg, "=") // "-name=value" to "-name"
buildFlag := forwardBuildFlags[name]
if buildFlag {
filtered = append(filtered, arg)
} else {
firstUnknown = name
}
if booleanFlags[arg] || strings.Contains(arg, "=") {
// Either "-bool" or "-name=value".
continue
}
// "-name value", so the next arg is part of this flag.
if i++; buildFlag && i < len(flags) {
filtered = append(filtered, flags[i])
}
}
return filtered, firstUnknown
}
// splitFlagsFromFiles splits args into a list of flag and file arguments. Since
// we can't rely on "--" being present, and we don't parse all flags upfront, we
// rely on finding the first argument that doesn't begin with "-" and that has
// the extension we expect for the list of paths.
//
// This function only makes sense for lower-level tool commands, such as
// "compile" or "link", since their arguments are predictable.
func splitFlagsFromFiles(all []string, ext string) (flags, paths []string) {
for i, arg := range all {
if !strings.HasPrefix(arg, "-") && strings.HasSuffix(arg, ext) {
return all[:i:i], all[i:]
}
}
return all, nil
}
// flagValue retrieves the value of a flag such as "-foo", from strings in the
// list of arguments like "-foo=bar" or "-foo" "bar". If the flag is repeated,
// the last value is returned.
func flagValue(flags []string, name string) string {
lastVal := ""
flagValueIter(flags, name, func(val string) {
lastVal = val
})
return lastVal
}
// flagValueIter retrieves all the values for a flag such as "-foo", like
// flagValue. The difference is that it allows handling complex flags, such as
// those whose values compose a list.
func flagValueIter(flags []string, name string, fn func(string)) {
for i, arg := range flags {
if val := strings.TrimPrefix(arg, name+"="); val != arg {
// -name=value
fn(val)
}
if arg == name { // -name ...
if i+1 < len(flags) {
// -name value
fn(flags[i+1])
}
}
}
}
func flagSetValue(flags []string, name, value string) []string {
for i, arg := range flags {
if strings.HasPrefix(arg, name+"=") {
// -name=value
flags[i] = name + "=" + value
return flags
}
if arg == name { // -name ...
if i+1 < len(flags) {
// -name value
flags[i+1] = value
return flags
}
return flags
}
}
return append(flags, name+"="+value)
}
func fetchGoEnv() error {
out, err := exec.Command("go", "env", "-json",
"GOOS", "GOPRIVATE", "GOMOD", "GOVERSION", "GOCACHE",
).CombinedOutput()
if err != nil {
// TODO: cover this in the tests.
fmt.Fprintf(os.Stderr, `Can't find the Go toolchain: %v
This is likely due to Go not being installed/setup correctly.
To install Go, see: https://go.dev/doc/install
`, err)
return errJustExit(1)
}
if err := json.Unmarshal(out, &cache.GoEnv); err != nil {
return fmt.Errorf(`cannot unmarshal from "go env -json": %w`, err)
}
cache.GOGARBLE = os.Getenv("GOGARBLE")
if cache.GOGARBLE != "" {
// GOGARBLE is non-empty; nothing to do.
} else if cache.GoEnv.GOPRIVATE != "" {
// GOGARBLE is empty and GOPRIVATE is non-empty.
// Set GOGARBLE to GOPRIVATE's value.
cache.GOGARBLE = cache.GoEnv.GOPRIVATE
} else {
// If GOPRIVATE isn't set and we're in a module, use its module
// path as a GOPRIVATE default. Include a _test variant too.
// TODO(mvdan): we shouldn't need the _test variant here,
// as the import path should not include it; only the package name.
if mod, err := os.ReadFile(cache.GoEnv.GOMOD); err == nil {
modpath := modfile.ModulePath(mod)
if modpath != "" {
cache.GOGARBLE = modpath + "," + modpath + "_test"
}
}
}
return nil
}