mirror of
https://github.com/burrowers/garble.git
synced 2026-04-22 15:47:04 +08:00
cdc4b23ac5
When compiling a package that instantiates a generic type from another package, info.Types only contains the instantiated *types.Named. The *types.Struct case in recordFieldToStruct returns early on instantiated structs (field != field.Origin()), so the origin struct was never recorded in fieldToStruct. At rewrite time, looking up the origin field then failed with "could not find struct for field X". Fix by also recursing into typ.Origin() for instantiated named types, so the uninstantiated struct is visited and its fields are recorded. Fixes #1027.
1478 lines
47 KiB
Go
1478 lines
47 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"cmp"
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"encoding/gob"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"go/ast"
|
||
"go/token"
|
||
"go/types"
|
||
"io/fs"
|
||
"log"
|
||
"maps"
|
||
mathrand "math/rand"
|
||
"os"
|
||
"path/filepath"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
"golang.org/x/tools/go/ast/astutil"
|
||
"golang.org/x/tools/go/ssa"
|
||
"mvdan.cc/garble/internal/ctrlflow"
|
||
"mvdan.cc/garble/internal/literals"
|
||
)
|
||
|
||
// 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 tool bundle -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted
|
||
//go:generate sed -i /go:generate/d cmdgo_quoted.go
|
||
|
||
// computeLinkerVariableStrings iterates over the -ldflags arguments,
|
||
// filling a map with all the string values set via the linker's -X flag.
|
||
// TODO: can we put this in sharedCache, using objectString as a key?
|
||
func computeLinkerVariableStrings(pkg *types.Package) (map[*types.Var]string, error) {
|
||
linkerVariableStrings := make(map[*types.Var]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(sharedCache.ForwardBuildFlags, "-ldflags"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for val := range flagValues(ldflags, "-X") {
|
||
// val is in the form of "foo.com/bar.name=value".
|
||
fullName, stringValue, found := strings.Cut(val, "=")
|
||
if !found {
|
||
continue // invalid
|
||
}
|
||
|
||
// fullName is "foo.com/bar.name"
|
||
i := strings.LastIndexByte(fullName, '.')
|
||
path, name := fullName[:i], fullName[i+1:]
|
||
|
||
// Note that package main always has import path "main" as part of a build.
|
||
if path != pkg.Path() && (path != "main" || pkg.Name() != "main") {
|
||
continue // not the current package
|
||
}
|
||
|
||
obj, _ := pkg.Scope().Lookup(name).(*types.Var)
|
||
if obj == nil {
|
||
continue // no such variable; skip
|
||
}
|
||
linkerVariableStrings[obj] = stringValue
|
||
}
|
||
return linkerVariableStrings, nil
|
||
}
|
||
|
||
func typecheck(pkgPath string, files []*ast.File, origImporter importerWithMap) (*types.Package, *types.Info, error) {
|
||
info := &types.Info{
|
||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||
Defs: make(map[*ast.Ident]types.Object),
|
||
Uses: make(map[*ast.Ident]types.Object),
|
||
Implicits: make(map[ast.Node]types.Object),
|
||
Scopes: make(map[ast.Node]*types.Scope),
|
||
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
||
Instances: make(map[*ast.Ident]types.Instance),
|
||
}
|
||
origTypesConfig := types.Config{
|
||
// Note that we don't set GoVersion here. Any Go language version checks
|
||
// are performed by the upfront `go list -json -compiled` call.
|
||
Importer: origImporter,
|
||
Sizes: types.SizesFor("gc", sharedCache.GoEnv.GOARCH),
|
||
}
|
||
pkg, err := origTypesConfig.Check(pkgPath, fset, files, info)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("typecheck error: %v", err)
|
||
}
|
||
return pkg, info, err
|
||
}
|
||
|
||
// computeFieldToStruct visits every reachable type after typechecking a package
|
||
// to map from a struct field back to its containing struct type.
|
||
// We only record uninstantiated or "origin" fields and types,
|
||
// given that a generic type and its instance must share names.
|
||
//
|
||
// Since types can be recursive, we need a map to avoid cycles.
|
||
// We only need to track named types as done, as all cycles must use them.
|
||
func computeFieldToStruct(info *types.Info) map[*types.Var]*types.Struct {
|
||
done := make(map[*types.Named]bool)
|
||
fieldToStruct := make(map[*types.Var]*types.Struct)
|
||
|
||
// Run recordType on all types reachable via [types.Info].
|
||
for _, tv := range info.Types {
|
||
recordFieldToStruct(tv.Type, done, fieldToStruct)
|
||
}
|
||
return fieldToStruct
|
||
}
|
||
|
||
func recordFieldToStruct(typ types.Type, done map[*types.Named]bool, fieldToStruct map[*types.Var]*types.Struct) {
|
||
switch typ := typ.(type) {
|
||
case interface{ Elem() types.Type }:
|
||
recordFieldToStruct(typ.Elem(), done, fieldToStruct)
|
||
case *types.Alias:
|
||
recordFieldToStruct(typ.Rhs(), done, fieldToStruct)
|
||
case *types.Named:
|
||
if done[typ] {
|
||
return
|
||
}
|
||
done[typ] = true
|
||
recordFieldToStruct(typ.Origin().Underlying(), done, fieldToStruct)
|
||
case *types.Struct:
|
||
for field := range typ.Fields() {
|
||
if field != field.Origin() {
|
||
// Part of this struct is instantiated; do not record it at all.
|
||
// TODO: can we discard these types earlier on in the recursion?
|
||
return
|
||
}
|
||
}
|
||
for field := range typ.Fields() {
|
||
prev := fieldToStruct[field]
|
||
if prev == nil {
|
||
fieldToStruct[field] = typ
|
||
} else if prev != typ {
|
||
// If this struct field is already done, ensure we got the same result.
|
||
// If we didn't, then this is most likely a bug we need to fix.
|
||
panic(fmt.Sprintf("inconsistent fieldToStruct results: %s vs %s", prev, typ))
|
||
}
|
||
if field.Embedded() {
|
||
recordFieldToStruct(field.Type(), done, fieldToStruct)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// isSafeForInstanceType returns true if the passed type is safe for var declaration.
|
||
// Unsafe types: generic types and non-method interfaces.
|
||
func isSafeForInstanceType(t types.Type) bool {
|
||
switch t := types.Unalias(t).(type) {
|
||
case *types.Basic:
|
||
return t.Kind() != types.Invalid
|
||
case *types.Named:
|
||
if t.TypeParams().Len() > 0 {
|
||
return false
|
||
}
|
||
return isSafeForInstanceType(t.Underlying())
|
||
case *types.Signature:
|
||
return t.TypeParams().Len() == 0
|
||
case *types.Interface:
|
||
return t.IsMethodSet()
|
||
}
|
||
return true
|
||
}
|
||
|
||
// namedType tries to obtain the *types.TypeName 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.
|
||
// Note that, for a type alias, this gives the alias name.
|
||
func namedType(t types.Type) *types.TypeName {
|
||
switch t := t.(type) {
|
||
case *types.Alias:
|
||
return t.Obj()
|
||
case *types.Named:
|
||
return t.Obj()
|
||
case *types.Pointer:
|
||
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
|
||
}
|
||
tname := namedType(params.At(0).Type())
|
||
if tname == nil {
|
||
return false // the only parameter isn't named, like "string"
|
||
}
|
||
return tname.Pkg().Path() == "testing" && tname.Name() == "T"
|
||
}
|
||
|
||
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 {
|
||
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)
|
||
}
|
||
|
||
// transformer holds all the information and state necessary to obfuscate a
|
||
// single Go package.
|
||
type transformer struct {
|
||
// curPkg holds basic information about the package being currently compiled or linked.
|
||
curPkg *listedPackage
|
||
|
||
// curPkgCache is the pkgCache for curPkg.
|
||
curPkgCache pkgCache
|
||
|
||
// The type-checking results; the package itself, and the Info struct.
|
||
pkg *types.Package
|
||
info *types.Info
|
||
|
||
// linkerVariableStrings records objects for variables used in -ldflags=-X flags,
|
||
// as well as the strings the user wants to inject them with.
|
||
// Used when obfuscating literals, so that we obfuscate the injected value.
|
||
linkerVariableStrings map[*types.Var]string
|
||
|
||
// fieldToStruct helps locate struct types from any of their field
|
||
// objects. Useful when obfuscating field names. See [computeFieldToStruct].
|
||
fieldToStruct map[*types.Var]*types.Struct
|
||
|
||
// obfRand is initialized by transformCompile and used during obfuscation.
|
||
// It is left nil at init time, so that we only use it after it has been
|
||
// properly initialized with a deterministic seed.
|
||
// It must only be used for deterministic obfuscation;
|
||
// if it is used for any other purpose, we may lose determinism.
|
||
obfRand *mathrand.Rand
|
||
|
||
// 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
|
||
|
||
// usedAllImportsFiles is used to prevent multiple calls of tf.useAllImports function on one file
|
||
// in case of simultaneously applied control flow and literals obfuscation
|
||
usedAllImportsFiles map[*ast.File]bool
|
||
}
|
||
|
||
var transformMethods = map[string]func(*transformer, []string) ([]string, error){
|
||
"asm": (*transformer).transformAsm,
|
||
"compile": (*transformer).transformCompile,
|
||
"link": (*transformer).transformLink,
|
||
}
|
||
|
||
func (tf *transformer) transformAsm(args []string) ([]string, error) {
|
||
flags, paths := splitFlagsFromFiles(args, ".s")
|
||
|
||
// When assembling, the import path can make its way into the output object file.
|
||
flags = flagSetValue(flags, "-p", tf.curPkg.obfuscatedImportPath())
|
||
|
||
flags = alterTrimpath(flags)
|
||
|
||
// The assembler runs twice; the first with -gensymabis,
|
||
// where we continue below and we obfuscate all the source.
|
||
// The second time, without -gensymabis, we reconstruct the paths to the
|
||
// obfuscated source files and reuse them to avoid work.
|
||
newPaths := make([]string, 0, len(paths))
|
||
if !slices.Contains(args, "-gensymabis") {
|
||
// Replace go_asm.h constant names; see [saveGoAsmNames].
|
||
// This can't be done in the gensymabis pass as the compiler hasn't run by then.
|
||
var replacer *strings.Replacer
|
||
if nameMap := loadGoAsmNames(tf.curPkg); len(nameMap) > 0 {
|
||
// Note that we sort the names from longest to shortest,
|
||
// so that a shorter name doesn't match a prefix of a longer one.
|
||
origNames := slices.SortedFunc(maps.Keys(nameMap), func(a, b string) int {
|
||
return cmp.Compare(len(b), len(a))
|
||
})
|
||
pairs := make([]string, 0, 2*len(nameMap))
|
||
for _, orig := range origNames {
|
||
pairs = append(pairs, orig, nameMap[orig])
|
||
}
|
||
replacer = strings.NewReplacer(pairs...)
|
||
}
|
||
for _, path := range paths {
|
||
name := hashWithPackage(tf.curPkg, filepath.Base(path)) + ".s"
|
||
pkgDir := filepath.Join(sharedTempDir, tf.curPkg.obfuscatedSourceDir())
|
||
newPath := filepath.Join(pkgDir, name)
|
||
if replacer != nil {
|
||
content, err := os.ReadFile(newPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if new := replacer.Replace(string(content)); new != string(content) {
|
||
if err := os.WriteFile(newPath, []byte(new), 0o666); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
newPaths = append(newPaths, newPath)
|
||
}
|
||
return append(flags, newPaths...), nil
|
||
}
|
||
|
||
const missingHeader = "missing header path"
|
||
newHeaderPaths := make(map[string]string)
|
||
var debugArtifacts cachedDebugArtifacts
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.SourceFiles = make(map[string][]byte)
|
||
debugArtifacts.GarbledFiles = make(map[string][]byte)
|
||
}
|
||
var buf, includeBuf bytes.Buffer
|
||
for _, path := range paths {
|
||
buf.Reset()
|
||
var asmContent bytes.Buffer
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
basename := filepath.Base(path)
|
||
defer f.Close() // in case of error
|
||
scanner := bufio.NewScanner(f)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
if flagDebugDir != "" {
|
||
asmContent.WriteString(line)
|
||
asmContent.WriteByte('\n')
|
||
}
|
||
|
||
// Whole-line comments might be directives, leave them in place.
|
||
// For example: //go:build race
|
||
// Any other comment, including inline ones, can be discarded entirely.
|
||
line, comment, hasComment := strings.Cut(line, "//")
|
||
if hasComment && line == "" {
|
||
buf.WriteString("//")
|
||
buf.WriteString(comment)
|
||
buf.WriteByte('\n')
|
||
continue
|
||
}
|
||
|
||
// Preprocessor lines to include another file.
|
||
// For example: #include "foo.h"
|
||
if quoted, ok := strings.CutPrefix(line, "#include"); ok {
|
||
quoted = strings.TrimSpace(quoted)
|
||
includePath, err := strconv.Unquote(quoted)
|
||
if err != nil { // note that strconv.Unquote errors do not include the input string
|
||
return nil, fmt.Errorf("cannot unquote %q: %v", quoted, err)
|
||
}
|
||
newPath := newHeaderPaths[includePath]
|
||
switch newPath {
|
||
case missingHeader: // no need to try again
|
||
buf.WriteString(line)
|
||
buf.WriteByte('\n')
|
||
continue
|
||
case "": // first time we see this header
|
||
includeBuf.Reset()
|
||
content, err := os.ReadFile(includePath)
|
||
if errors.Is(err, fs.ErrNotExist) {
|
||
newHeaderPaths[includePath] = missingHeader
|
||
buf.WriteString(line)
|
||
buf.WriteByte('\n')
|
||
continue // a header file provided by Go or the system
|
||
} else if err != nil {
|
||
return nil, err
|
||
}
|
||
basename := filepath.Base(includePath)
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.SourceFiles[basename] = content
|
||
if err := writeDebugDirFile(debugDirSourceSubdir, tf.curPkg, basename, content); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
tf.replaceAsmNames(&includeBuf, content)
|
||
|
||
// For now, we replace `foo.h` or `dir/foo.h` with `garbled_foo.h`.
|
||
// The different name ensures we don't use the unobfuscated file.
|
||
// This is far from perfect, but does the job for the time being.
|
||
// In the future, use a randomized name.
|
||
newPath = "garbled_" + basename
|
||
content = includeBuf.Bytes()
|
||
if _, err := tf.writeSourceFile(basename, newPath, content); err != nil {
|
||
return nil, err
|
||
}
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.GarbledFiles[basename] = content
|
||
}
|
||
newHeaderPaths[includePath] = newPath
|
||
}
|
||
buf.WriteString("#include ")
|
||
buf.WriteString(strconv.Quote(newPath))
|
||
buf.WriteByte('\n')
|
||
continue
|
||
}
|
||
|
||
// Anything else is regular assembly; replace the names.
|
||
tf.replaceAsmNames(&buf, []byte(line))
|
||
buf.WriteByte('\n')
|
||
}
|
||
if err := scanner.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
f.Close() // do not keep len(paths) files open
|
||
if flagDebugDir != "" {
|
||
content := asmContent.Bytes()
|
||
debugArtifacts.SourceFiles[basename] = content
|
||
if err := writeDebugDirFile(debugDirSourceSubdir, tf.curPkg, basename, content); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
content := buf.Bytes()
|
||
|
||
// With assembly files, we obfuscate the filename in the temporary
|
||
// directory, as assembly files do not support `/*line` directives.
|
||
// TODO(mvdan): per cmd/asm/internal/lex, they do support `#line`.
|
||
newName := hashWithPackage(tf.curPkg, basename) + ".s"
|
||
if path, err := tf.writeSourceFile(basename, newName, content); err != nil {
|
||
return nil, err
|
||
} else {
|
||
newPaths = append(newPaths, path)
|
||
}
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.GarbledFiles[basename] = content
|
||
}
|
||
}
|
||
if err := saveDebugArtifactsForPkg(tf.curPkg, debugCacheKindAsm, debugArtifacts); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return append(flags, newPaths...), nil
|
||
}
|
||
|
||
// saveGoAsmNames saves go_asm.h constant name mappings to the build cache;
|
||
// see https://go.dev/doc/asm#data-offsets.
|
||
func (tf *transformer) saveGoAsmNames() error {
|
||
nameMap := make(map[string]string) // original -> obfuscated
|
||
scope := tf.pkg.Scope()
|
||
for _, name := range scope.Names() {
|
||
obj := scope.Lookup(name)
|
||
tn, ok := obj.(*types.TypeName)
|
||
if !ok {
|
||
continue
|
||
}
|
||
strct, ok := tn.Type().Underlying().(*types.Struct)
|
||
if !ok {
|
||
continue
|
||
}
|
||
obfTypeName := hashWithPackage(tf.curPkg, name)
|
||
nameMap[name+"__size"] = obfTypeName + "__size"
|
||
for field := range strct.Fields() {
|
||
obfFieldName := hashWithStruct(strct, field)
|
||
nameMap[name+"_"+field.Name()] = obfTypeName + "_" + obfFieldName
|
||
}
|
||
}
|
||
if len(nameMap) == 0 {
|
||
return nil
|
||
}
|
||
fsCache, err := openCache()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// TODO: if we end up with an "obfuscated name map" artifact per package,
|
||
// then use that directly.
|
||
var buf bytes.Buffer
|
||
if err := gob.NewEncoder(&buf).Encode(nameMap); err != nil {
|
||
return err
|
||
}
|
||
return fsCache.PutBytes(goAsmCacheID(tf.curPkg.GarbleActionID), buf.Bytes())
|
||
}
|
||
|
||
func goAsmCacheID(garbleActionID [sha256.Size]byte) [sha256.Size]byte {
|
||
hasher := sha256.New()
|
||
hasher.Write(garbleActionID[:])
|
||
hasher.Write([]byte("\x00go-asm-names-v1\x00"))
|
||
var sum [sha256.Size]byte
|
||
hasher.Sum(sum[:0])
|
||
return sum
|
||
}
|
||
|
||
// loadGoAsmNames loads the go_asm.h name mapping saved by [transformer.saveGoAsmNames].
|
||
func loadGoAsmNames(lpkg *listedPackage) map[string]string {
|
||
fsCache, err := openCache()
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
filename, _, err := fsCache.GetFile(goAsmCacheID(lpkg.GarbleActionID))
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
f, err := os.Open(filename)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
defer f.Close()
|
||
var nameMap map[string]string
|
||
if err := gob.NewDecoder(f).Decode(&nameMap); err != nil {
|
||
return nil
|
||
}
|
||
return nameMap
|
||
}
|
||
|
||
func (tf *transformer) replaceAsmNames(buf *bytes.Buffer, remaining []byte) {
|
||
// 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
|
||
// TEXT runtime∕internal∕sys·Ctz64(SB), NOSPLIT, $0-12
|
||
//
|
||
// Note that import paths in assembly, like `runtime∕internal∕sys` above,
|
||
// use Unicode periods and slashes rather than the ASCII ones used by `go list`.
|
||
// We need to convert to ASCII to find the right package information.
|
||
const (
|
||
asmPeriod = '·'
|
||
goPeriod = '.'
|
||
asmSlash = '∕'
|
||
goSlash = '/'
|
||
)
|
||
asmPeriodLen := utf8.RuneLen(asmPeriod)
|
||
|
||
for {
|
||
periodIdx := bytes.IndexRune(remaining, asmPeriod)
|
||
if periodIdx < 0 {
|
||
buf.Write(remaining)
|
||
remaining = nil
|
||
break
|
||
}
|
||
|
||
// The package name ends at the first rune which cannot be part of a Go
|
||
// import path, such as a comma or space.
|
||
pkgStart := periodIdx
|
||
for pkgStart >= 0 {
|
||
c, size := utf8.DecodeLastRune(remaining[:pkgStart])
|
||
if !unicode.IsLetter(c) && c != '_' && c != asmSlash && !unicode.IsDigit(c) {
|
||
break
|
||
}
|
||
pkgStart -= size
|
||
}
|
||
// The package name might actually be longer, e.g:
|
||
//
|
||
// JMP test∕with·many·dots∕main∕imported·PublicAdd(SB)
|
||
//
|
||
// We have `test∕with` so far; grab `·many·dots∕main∕imported` as well.
|
||
pkgEnd := periodIdx
|
||
lastAsmPeriod := -1
|
||
for i := pkgEnd + asmPeriodLen; i <= len(remaining); {
|
||
c, size := utf8.DecodeRune(remaining[i:])
|
||
if c == asmPeriod {
|
||
lastAsmPeriod = i
|
||
} else if !unicode.IsLetter(c) && c != '_' && c != asmSlash && !unicode.IsDigit(c) {
|
||
if lastAsmPeriod > 0 {
|
||
pkgEnd = lastAsmPeriod
|
||
}
|
||
break
|
||
}
|
||
i += size
|
||
}
|
||
asmPkgPath := string(remaining[pkgStart:pkgEnd])
|
||
|
||
// Write the bytes before our unqualified `·foo` or qualified `pkg·foo`.
|
||
buf.Write(remaining[:pkgStart])
|
||
|
||
// If the name was qualified, fetch the package, and write the
|
||
// obfuscated import path if needed.
|
||
lpkg := tf.curPkg
|
||
if asmPkgPath != "" {
|
||
if asmPkgPath != tf.curPkg.Name {
|
||
goPkgPath := asmPkgPath
|
||
goPkgPath = strings.ReplaceAll(goPkgPath, string(asmPeriod), string(goPeriod))
|
||
goPkgPath = strings.ReplaceAll(goPkgPath, string(asmSlash), string(goSlash))
|
||
var err error
|
||
lpkg, err = listPackage(tf.curPkg, goPkgPath)
|
||
if err != nil {
|
||
panic(err) // shouldn't happen
|
||
}
|
||
}
|
||
if lpkg.ToObfuscate {
|
||
// Note that we don't need to worry about asmSlash here,
|
||
// because our obfuscated import paths contain no slashes right now.
|
||
buf.WriteString(lpkg.obfuscatedImportPath())
|
||
} else {
|
||
buf.WriteString(asmPkgPath)
|
||
}
|
||
}
|
||
|
||
// Write the middle dot and advance the remaining slice.
|
||
buf.WriteRune(asmPeriod)
|
||
remaining = remaining[pkgEnd+asmPeriodLen:]
|
||
|
||
// The declared 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 lpkg.ToObfuscate && !compilerIntrinsics[lpkg.ImportPath][name] {
|
||
newName := hashWithPackage(lpkg, name)
|
||
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
|
||
log.Printf("asm name %q hashed with %x to %q", name, tf.curPkg.GarbleActionID, newName)
|
||
}
|
||
buf.WriteString(newName)
|
||
} else {
|
||
buf.WriteString(name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// writeSourceFile 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 (tf *transformer) writeSourceFile(basename, obfuscated string, content []byte) (string, error) {
|
||
// Uncomment for some quick debugging. Do not delete.
|
||
// fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, basename, content)
|
||
|
||
if flagDebugDir != "" {
|
||
if err := writeDebugDirFile(debugDirGarbledSubdir, tf.curPkg, basename, content); err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
// We use the obfuscated import path to hold the temporary files.
|
||
// Assembly files do not support line directives to set positions,
|
||
// so the only way to not leak the import path is to replace it.
|
||
pkgDir := filepath.Join(sharedTempDir, tf.curPkg.obfuscatedSourceDir())
|
||
if err := os.MkdirAll(pkgDir, 0o777); err != nil {
|
||
return "", err
|
||
}
|
||
dstPath := filepath.Join(pkgDir, obfuscated)
|
||
if err := writeFileExclusive(dstPath, content); err != nil {
|
||
return "", err
|
||
}
|
||
return dstPath, nil
|
||
}
|
||
|
||
func (tf *transformer) transformCompile(args []string) ([]string, error) {
|
||
flags, paths := splitFlagsFromFiles(args, ".go")
|
||
var debugArtifacts cachedDebugArtifacts
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.SourceFiles = make(map[string][]byte)
|
||
debugArtifacts.GarbledFiles = make(map[string][]byte)
|
||
for _, path := range paths {
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
basename := filepath.Base(path)
|
||
debugArtifacts.SourceFiles[basename] = content
|
||
if err := writeDebugDirFile(debugDirSourceSubdir, tf.curPkg, basename, content); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
|
||
// We will force the linker to drop DWARF via -w, so don't spend time
|
||
// generating it.
|
||
flags = append(flags, "-dwarf=false")
|
||
|
||
// The Go file paths given to the compiler are always absolute paths.
|
||
files, err := parseFiles(tf.curPkg, "", paths)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Literal and control flow obfuscation uses math/rand, so seed it deterministically.
|
||
randSeed := tf.curPkg.GarbleActionID[:]
|
||
if flagSeed.present() {
|
||
randSeed = flagSeed.bytes
|
||
}
|
||
// log.Printf("seeding math/rand with %x\n", randSeed)
|
||
tf.obfRand = mathrand.New(mathrand.NewSource(int64(binary.BigEndian.Uint64(randSeed))))
|
||
|
||
// Even if loadPkgCache below finds a direct cache hit,
|
||
// other parts of garble still need type information to obfuscate.
|
||
// We could potentially avoid this by saving the type info we need in the cache,
|
||
// although in general that wouldn't help much, since it's rare for Go's cache
|
||
// to miss on a package and for our cache to hit.
|
||
if tf.pkg, tf.info, err = typecheck(tf.curPkg.ImportPath, files, tf.origImporter); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var (
|
||
ssaPkg *ssa.Package
|
||
requiredPkgs []string
|
||
)
|
||
if flagControlFlow {
|
||
ssaPkg = ssaBuildPkg(tf.pkg, files, tf.info)
|
||
|
||
newFileName, newFile, affectedFiles, err := ctrlflow.Obfuscate(fset, ssaPkg, files, tf.obfRand)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if newFile != nil {
|
||
files = append(files, newFile)
|
||
paths = append(paths, newFileName)
|
||
for _, file := range affectedFiles {
|
||
tf.useAllImports(file)
|
||
}
|
||
if tf.pkg, tf.info, err = typecheck(tf.curPkg.ImportPath, files, tf.origImporter); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, imp := range newFile.Imports {
|
||
path, err := strconv.Unquote(imp.Path.Value)
|
||
if err != nil {
|
||
panic(err) // should never happen
|
||
}
|
||
requiredPkgs = append(requiredPkgs, path)
|
||
}
|
||
}
|
||
}
|
||
|
||
if tf.curPkgCache, err = loadPkgCache(tf.curPkg, tf.pkg, files, tf.info, ssaPkg); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// These maps are not kept in pkgCache, since they are only needed to obfuscate curPkg.
|
||
tf.fieldToStruct = computeFieldToStruct(tf.info)
|
||
if flagLiterals {
|
||
if tf.linkerVariableStrings, err = computeLinkerVariableStrings(tf.pkg); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if len(tf.curPkg.SFiles) > 0 && tf.curPkg.ToObfuscate {
|
||
if err := tf.saveGoAsmNames(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
flags = alterTrimpath(flags)
|
||
newImportCfg, err := tf.processImportCfg(flags, requiredPkgs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Note that the main package always uses `-p main`, even though it's not an import path.
|
||
flags = flagSetValue(flags, "-p", tf.curPkg.obfuscatedImportPath())
|
||
|
||
newPaths := make([]string, 0, len(files))
|
||
|
||
for i, file := range files {
|
||
basename := filepath.Base(paths[i])
|
||
log.Printf("obfuscating %s", basename)
|
||
switch tf.curPkg.ImportPath {
|
||
case "runtime":
|
||
if flagTiny {
|
||
// strip unneeded runtime code
|
||
stripRuntime(basename, file)
|
||
tf.useAllImports(file)
|
||
}
|
||
if basename == "symtab.go" {
|
||
updateEntryOffset(file, entryOffKey())
|
||
}
|
||
case "internal/abi":
|
||
if basename == "symtab.go" {
|
||
updateMagicValue(file, magicValue())
|
||
}
|
||
}
|
||
if err := tf.transformDirectives(file.Comments); err != nil {
|
||
return nil, err
|
||
}
|
||
file = tf.transformGoFile(file)
|
||
file.Name.Name = tf.curPkg.obfuscatedPackageName()
|
||
|
||
src, err := printFile(tf.curPkg, file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if tf.curPkg.Name == "main" && strings.HasSuffix(reflectPatchFile, basename) {
|
||
src = reflectMainPostPatch(src, tf.curPkg, tf.curPkgCache)
|
||
}
|
||
|
||
// We hide Go source filenames via "//line" directives,
|
||
// so there is no need to use obfuscated filenames here.
|
||
if path, err := tf.writeSourceFile(basename, basename, src); err != nil {
|
||
return nil, err
|
||
} else {
|
||
newPaths = append(newPaths, path)
|
||
}
|
||
if flagDebugDir != "" {
|
||
debugArtifacts.GarbledFiles[basename] = src
|
||
}
|
||
}
|
||
if err := saveDebugArtifactsForPkg(tf.curPkg, debugCacheKindCompile, debugArtifacts); err != nil {
|
||
return nil, err
|
||
}
|
||
flags = flagSetValue(flags, "-importcfg", newImportCfg)
|
||
|
||
return append(flags, newPaths...), nil
|
||
}
|
||
|
||
// transformDirectives rewrites toolchain directives in comments to replace
|
||
// local names with their obfuscated versions.
|
||
func (tf *transformer) transformDirectives(comments []*ast.CommentGroup) error {
|
||
for _, group := range comments {
|
||
for _, comment := range group.List {
|
||
switch {
|
||
case strings.HasPrefix(comment.Text, "//go:linkname "):
|
||
// We can have either just one argument:
|
||
//
|
||
// //go:linkname localName
|
||
//
|
||
// Or two arguments, where the second may refer to a name in a
|
||
// different package:
|
||
//
|
||
// //go:linkname localName newName
|
||
// //go:linkname localName pkg.newName
|
||
fields := strings.Fields(comment.Text)
|
||
localName := fields[1]
|
||
newName := ""
|
||
if len(fields) == 3 {
|
||
newName = fields[2]
|
||
}
|
||
switch newName {
|
||
case "runtime.lastmoduledatap", "runtime.moduledataverify1":
|
||
// Linknaming to the var and function above is used by github.com/bytedance/sonic/loader
|
||
// to inject functions into the runtime, but that breaks as garble patches
|
||
// the runtime to change the function header magic number.
|
||
//
|
||
// Given that Go is locking down access to runtime internals via go:linkname,
|
||
// and what sonic does was never supported and is a hack,
|
||
// refuse to build before the user sees confusing run-time panics.
|
||
return fmt.Errorf("garble does not support packages with a //go:linkname to %s", newName)
|
||
}
|
||
|
||
localName, newName = tf.transformLinkname(localName, newName)
|
||
fields[1] = localName
|
||
if len(fields) == 3 {
|
||
fields[2] = newName
|
||
}
|
||
|
||
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
|
||
log.Printf("linkname %q changed to %q", comment.Text, strings.Join(fields, " "))
|
||
}
|
||
comment.Text = strings.Join(fields, " ")
|
||
case strings.HasPrefix(comment.Text, "//go:cgo_import_dynamic "),
|
||
strings.HasPrefix(comment.Text, "//go:cgo_import_static "):
|
||
fields := strings.Fields(comment.Text)
|
||
if len(fields) < 2 {
|
||
continue
|
||
}
|
||
if localNamePart, ok := strings.CutPrefix(fields[1], tf.curPkg.ImportPath+"."); ok {
|
||
fields[1] = tf.curPkg.obfuscatedImportPath() + "." + tf.directiveLocalName(localNamePart)
|
||
}
|
||
comment.Text = strings.Join(fields, " ")
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (tf *transformer) directiveLocalName(localName string) string {
|
||
if tf.curPkg.ToObfuscate && !compilerIntrinsics[tf.curPkg.ImportPath][localName] {
|
||
return hashWithPackage(tf.curPkg, localName)
|
||
}
|
||
return localName
|
||
}
|
||
|
||
func (tf *transformer) transformLinkname(localName, newName string) (string, string) {
|
||
localName = tf.directiveLocalName(localName)
|
||
if newName == "" {
|
||
return localName, ""
|
||
}
|
||
// 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.
|
||
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.
|
||
return localName, newName
|
||
}
|
||
switch newName {
|
||
case "main.main", "main..inittask", "runtime..inittask":
|
||
// The runtime uses some special symbols with "..".
|
||
// We aren't touching those at the moment.
|
||
return localName, newName
|
||
}
|
||
|
||
pkgSplit := 0
|
||
var foreignName string
|
||
var lpkg *listedPackage
|
||
for {
|
||
i := strings.Index(newName[pkgSplit:], ".")
|
||
if i < 0 {
|
||
// We couldn't find a prefix that matched a known package.
|
||
// Probably a made up name like above, but with a dot.
|
||
return localName, newName
|
||
}
|
||
pkgSplit += i
|
||
pkgPath := newName[:pkgSplit]
|
||
pkgSplit++ // skip over the dot
|
||
|
||
if strings.HasSuffix(pkgPath, "_test") {
|
||
// runtime uses a go:linkname to metrics_test;
|
||
// we don't need this to work for now on regular builds,
|
||
// though we might need to rethink this if we want "go test std" to work.
|
||
continue
|
||
}
|
||
|
||
var err error
|
||
lpkg, err = listPackage(tf.curPkg, pkgPath)
|
||
if err == nil {
|
||
foreignName = newName[pkgSplit:]
|
||
break
|
||
}
|
||
if errors.Is(err, ErrNotFound) {
|
||
// No match; find the next dot.
|
||
continue
|
||
}
|
||
if errors.Is(err, ErrNotDependency) {
|
||
fmt.Fprintf(os.Stderr,
|
||
"//go:linkname refers to %s - add `import _ %q` for garble to find the package",
|
||
newName, pkgPath)
|
||
return localName, newName
|
||
}
|
||
panic(err) // shouldn't happen
|
||
}
|
||
|
||
if !lpkg.ToObfuscate || compilerIntrinsics[lpkg.ImportPath][foreignName] {
|
||
// We're not obfuscating that package or name.
|
||
return localName, newName
|
||
}
|
||
|
||
var newForeignName string
|
||
if receiver, name, ok := strings.Cut(foreignName, "."); ok {
|
||
if receiver, ok = strings.CutPrefix(receiver, "(*"); ok {
|
||
// pkg/path.(*Receiver).method
|
||
receiver, _ = strings.CutSuffix(receiver, ")")
|
||
receiver = "(*" + hashWithPackage(lpkg, receiver) + ")"
|
||
} else {
|
||
// pkg/path.Receiver.method
|
||
receiver = hashWithPackage(lpkg, receiver)
|
||
}
|
||
// Exported methods are never obfuscated.
|
||
//
|
||
// TODO(mvdan): We're duplicating the logic behind these decisions.
|
||
// Reuse the logic with transformCompile.
|
||
if !token.IsExported(name) {
|
||
name = hashWithPackage(lpkg, name)
|
||
}
|
||
newForeignName = receiver + "." + name
|
||
} else {
|
||
// pkg/path.function
|
||
newForeignName = hashWithPackage(lpkg, foreignName)
|
||
}
|
||
|
||
newName = lpkg.obfuscatedImportPath() + "." + newForeignName
|
||
return localName, newName
|
||
}
|
||
|
||
// 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 (tf *transformer) processImportCfg(flags []string, requiredPkgs []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
|
||
|
||
// using for track required but not imported packages
|
||
var newIndirectImports map[string]bool
|
||
if requiredPkgs != nil {
|
||
newIndirectImports = make(map[string]bool)
|
||
for _, pkg := range requiredPkgs {
|
||
// unsafe is a special case, it's not a real dependency
|
||
if pkg == "unsafe" {
|
||
continue
|
||
}
|
||
|
||
newIndirectImports[pkg] = true
|
||
}
|
||
}
|
||
|
||
for line := range strings.SplitSeq(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})
|
||
delete(newIndirectImports, importPath)
|
||
}
|
||
}
|
||
|
||
// 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(tf.curPkg, beforePath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
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)
|
||
}
|
||
|
||
if len(newIndirectImports) > 0 {
|
||
f, err := os.Open(filepath.Join(sharedTempDir, actionGraphFileName))
|
||
if err != nil {
|
||
return "", fmt.Errorf("cannot open action graph file: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
var actions []struct {
|
||
Mode string
|
||
Package string
|
||
Objdir string
|
||
}
|
||
if err := json.NewDecoder(f).Decode(&actions); err != nil {
|
||
return "", fmt.Errorf("cannot parse action graph file: %v", err)
|
||
}
|
||
|
||
// theoretically action graph can be long, to optimise it process it in one pass
|
||
// with an early exit when all the required imports are found
|
||
for _, action := range actions {
|
||
if action.Mode != "build" {
|
||
continue
|
||
}
|
||
if ok := newIndirectImports[action.Package]; !ok {
|
||
continue
|
||
}
|
||
|
||
packagefiles = append(packagefiles, [2]string{action.Package, filepath.Join(action.Objdir, "_pkg_.a")}) // file name hardcoded in compiler
|
||
delete(newIndirectImports, action.Package)
|
||
if len(newIndirectImports) == 0 {
|
||
break
|
||
}
|
||
}
|
||
|
||
if len(newIndirectImports) > 0 {
|
||
return "", fmt.Errorf("cannot resolve required packages from action graph file: %v", requiredPkgs)
|
||
}
|
||
}
|
||
|
||
for _, pair := range packagefiles {
|
||
impPath, pkgfile := pair[0], pair[1]
|
||
lpkg, err := listPackage(tf.curPkg, 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.
|
||
// This still triggers as of go1.24.
|
||
if strings.HasSuffix(tf.curPkg.ImportPath, ".test]") && strings.HasPrefix(tf.curPkg.ImportPath, impPath) {
|
||
continue
|
||
}
|
||
return "", err
|
||
}
|
||
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
|
||
}
|
||
|
||
func (tf *transformer) useAllImports(file *ast.File) {
|
||
if tf.usedAllImportsFiles == nil {
|
||
tf.usedAllImportsFiles = make(map[*ast.File]bool)
|
||
} else if ok := tf.usedAllImportsFiles[file]; ok {
|
||
return
|
||
}
|
||
tf.usedAllImportsFiles[file] = true
|
||
|
||
for _, imp := range file.Imports {
|
||
if imp.Name != nil && imp.Name.Name == "_" {
|
||
continue
|
||
}
|
||
|
||
pkgName := tf.info.PkgNameOf(imp)
|
||
pkgScope := pkgName.Imported().Scope()
|
||
var nameObj types.Object
|
||
for _, name := range pkgScope.Names() {
|
||
if obj := pkgScope.Lookup(name); obj.Exported() && isSafeForInstanceType(obj.Type()) {
|
||
nameObj = obj
|
||
break
|
||
}
|
||
}
|
||
if nameObj == nil {
|
||
// A very unlikely situation where there is no suitable declaration for a reference variable
|
||
// and almost certainly means that there is another import reference in code.
|
||
continue
|
||
}
|
||
spec := &ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("_")}}
|
||
decl := &ast.GenDecl{Specs: []ast.Spec{spec}}
|
||
|
||
nameIdent := ast.NewIdent(nameObj.Name())
|
||
var nameExpr ast.Expr
|
||
switch {
|
||
case imp.Name == nil: // import "pkg/path"
|
||
nameExpr = &ast.SelectorExpr{
|
||
X: ast.NewIdent(pkgName.Name()),
|
||
Sel: nameIdent,
|
||
}
|
||
case imp.Name.Name != ".": // import path2 "pkg/path"
|
||
nameExpr = &ast.SelectorExpr{
|
||
X: ast.NewIdent(imp.Name.Name),
|
||
Sel: nameIdent,
|
||
}
|
||
default: // import . "pkg/path"
|
||
nameExpr = nameIdent
|
||
}
|
||
|
||
switch nameObj.(type) {
|
||
case *types.Const:
|
||
// const _ = <value>
|
||
decl.Tok = token.CONST
|
||
spec.Values = []ast.Expr{nameExpr}
|
||
case *types.Var, *types.Func:
|
||
// var _ = <value>
|
||
decl.Tok = token.VAR
|
||
spec.Values = []ast.Expr{nameExpr}
|
||
case *types.TypeName:
|
||
// var _ <type>
|
||
decl.Tok = token.VAR
|
||
spec.Type = nameExpr
|
||
default:
|
||
continue // skip *types.Builtin and others
|
||
}
|
||
|
||
// Ensure that types.Info.Uses is up to date.
|
||
tf.info.Uses[nameIdent] = nameObj
|
||
file.Decls = append(file.Decls, decl)
|
||
}
|
||
}
|
||
|
||
// transformGoFile obfuscates the provided Go syntax file.
|
||
func (tf *transformer) transformGoFile(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 && tf.curPkg.ToObfuscate {
|
||
file = literals.Obfuscate(tf.obfRand, file, tf.info, tf.linkerVariableStrings, randomName)
|
||
|
||
// some imported constants might not be needed anymore, remove unnecessary imports
|
||
tf.useAllImports(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.
|
||
tname := namedType(obj.Type())
|
||
if tname == nil {
|
||
return true // unnamed type (probably a basic type, e.g. int)
|
||
}
|
||
obj = tname
|
||
pkg = obj.Pkg()
|
||
}
|
||
if pkg == nil {
|
||
return true // universe scope
|
||
}
|
||
|
||
// TODO: We match by object name here, which is actually imprecise.
|
||
// For example, in package embed we match the type FS, but we would also
|
||
// match any field or method named FS.
|
||
// Can we instead use an object map like ReflectObjects?
|
||
path := pkg.Path()
|
||
switch path {
|
||
case "sync/atomic", "runtime/internal/atomic":
|
||
if name == "align64" {
|
||
return true
|
||
}
|
||
case "embed":
|
||
// FS is detected by the compiler for //go:embed.
|
||
if name == "FS" {
|
||
return true
|
||
}
|
||
case "reflect":
|
||
switch name {
|
||
// Per the linker's deadcode.go docs,
|
||
// the Method and MethodByName methods are what drive the logic.
|
||
case "Method", "MethodByName":
|
||
return true
|
||
}
|
||
case "crypto/x509/pkix":
|
||
// For better or worse, encoding/asn1 detects a "SET" suffix on slice type names
|
||
// to tell whether those slices should be treated as sets or sequences.
|
||
// Do not obfuscate those names to prevent breaking x509 certificates.
|
||
// TODO: we can surely do better; ideally propose a non-string-based solution
|
||
// upstream, or as a fallback, obfuscate to a name ending with "SET".
|
||
if strings.HasSuffix(name, "SET") {
|
||
return true
|
||
}
|
||
}
|
||
|
||
lpkg, err := listPackage(tf.curPkg, 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"
|
||
|
||
// log.Printf("%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.
|
||
originObj := obj.Origin()
|
||
strct := tf.fieldToStruct[originObj]
|
||
if strct == nil {
|
||
panic("could not find struct for field " + name)
|
||
}
|
||
node.Name = hashWithStruct(strct, originObj)
|
||
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
|
||
log.Printf("%s %q hashed with struct fields to %q", debugName, name, node.Name)
|
||
}
|
||
return true
|
||
|
||
case *types.TypeName:
|
||
debugName = "type"
|
||
case *types.Func:
|
||
if compilerIntrinsics[path][name] {
|
||
return true
|
||
}
|
||
|
||
sign := obj.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
|
||
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
|
||
log.Printf("%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.
|
||
lpkg, err := listPackage(tf.curPkg, path)
|
||
if err != nil {
|
||
panic(err) // should never happen
|
||
}
|
||
// Note that a main package is imported via its original import path.
|
||
imp.Path.Value = strconv.Quote(lpkg.obfuscatedImportPath())
|
||
// If the import was unnamed, give it the name of the
|
||
// original package name, to keep references working.
|
||
if imp.Name == nil {
|
||
imp.Name = &ast.Ident{
|
||
NamePos: imp.Path.ValuePos, // ensure it ends up on the same line
|
||
Name: lpkg.Name,
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
return astutil.Apply(file, pre, post).(*ast.File)
|
||
}
|
||
|
||
func (tf *transformer) 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 := tf.processImportCfg(flags, nil)
|
||
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.
|
||
for val := range flagValues(flags, "-X") {
|
||
// val is in the form of "foo.com/bar.name=value".
|
||
fullName, stringValue, found := strings.Cut(val, "=")
|
||
if !found {
|
||
continue // 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 := tf.curPkg
|
||
if path != "main" {
|
||
lpkg = sharedCache.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.
|
||
continue
|
||
}
|
||
newName := hashWithPackage(lpkg, name)
|
||
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", lpkg.obfuscatedImportPath(), 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
|
||
}
|