From 5f74a1c9f06ff762cac7f29d8eecc2332157be08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 9 Dec 2021 23:15:06 +0000 Subject: [PATCH] unify the definition and storage of flag values The parent garble process parses the original flags, as provided by the user via the command line. Previously, those got stored in the shared cache file, so that child processes spawned by toolexec could see them. Unfortunately, this made the code relatively easy to misuse. A child process would always see flagLiterals as zero value, given that it should never see such a flag argument directly. Similarly, one would have to be careful with cached options, as they could only be consumed after the cache file is loaded. Simplify the situation by deduplicating the storage of flags. Now, the parent passes all flags onto children via toolexec. One exception is GarbleDir, which now becomes an env var. This seems in line with other top-level dirs like GARBLE_SHARED. Finally, we turn -seed into a flag.Value, which lets us implement its "set" behavior as part of flag.Parse. Overall, we barely reduce the amount of code involved, but we certainly remove a couple of footguns. As part of the cleanup, we also introduce appendFlags. --- hash.go | 28 +++++++++----- main.go | 109 ++++++++++++++++++++++++++++++++++++++-------------- position.go | 2 +- shared.go | 73 ----------------------------------- 4 files changed, 101 insertions(+), 111 deletions(-) diff --git a/hash.go b/hash.go index 2e29c22..0c17ffd 100644 --- a/hash.go +++ b/hash.go @@ -98,17 +98,27 @@ func addGarbleToHash(inputHash []byte) []byte { if cache.GOGARBLE != "" { fmt.Fprintf(h, " GOGARBLE=%s", cache.GOGARBLE) } - if opts.ObfuscateLiterals { - fmt.Fprintf(h, " -literals") + appendFlags(h) + return h.Sum(nil)[:buildIDComponentLength] +} + +// appendFlags writes garble's own flags to w in string form. +// Errors are ignored, as w is always a buffer or hasher. +func appendFlags(w io.Writer) { + if flagLiterals { + io.WriteString(w, " -literals") } - if opts.Tiny { - fmt.Fprintf(h, " -tiny") + if flagTiny { + io.WriteString(w, " -tiny") } - if len(opts.Seed) > 0 { - fmt.Fprintf(h, " -seed=%x", opts.Seed) + if flagDebugDir != "" { + io.WriteString(w, " -debugdir=") + io.WriteString(w, flagDebugDir) + } + if len(flagSeed.bytes) > 0 { + io.WriteString(w, " -seed=") + io.WriteString(w, flagSeed.String()) } - - return h.Sum(nil)[:buildIDComponentLength] } // buildIDComponentLength is the number of bytes each build ID component takes, @@ -196,7 +206,7 @@ func hashWith(salt []byte, name string) string { d := sha256.New() d.Write(salt) - d.Write(opts.Seed) + d.Write(flagSeed.bytes) io.WriteString(d, name) sum := make([]byte, nameBase64.EncodedLen(d.Size())) nameBase64.Encode(sum, d.Sum(nil)) diff --git a/main.go b/main.go index 8874aa7..a32d715 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "bytes" + "crypto/rand" "encoding/base64" "encoding/binary" "encoding/gob" @@ -19,7 +20,6 @@ import ( "go/types" "io" "io/fs" - "io/ioutil" "log" mathrand "math/rand" "os" @@ -48,18 +48,50 @@ var ( ) var ( - flagObfuscateLiterals bool - flagGarbleTiny bool - flagDebugDir string - flagSeed string + flagLiterals bool + flagTiny bool + flagDebugDir string + flagSeed seedFlag ) func init() { flagSet.Usage = usage - flagSet.BoolVar(&flagObfuscateLiterals, "literals", false, "Obfuscate literals such as strings") - flagSet.BoolVar(&flagGarbleTiny, "tiny", false, "Optimize for binary size, losing some ability to reverse the process") + 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.StringVar(&flagDebugDir, "debugdir", "", "Write the obfuscated source to a directory, e.g. -debugdir=out") - flagSet.StringVar(&flagSeed, "seed", "", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random") + flagSet.Var(&flagSeed, "seed", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random") +} + +type seedFlag struct { + random bool + bytes []byte +} + +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() { @@ -100,6 +132,7 @@ 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 @@ -122,8 +155,6 @@ var ( } return os.Open(pkgfile) }).(types.ImporterFrom) - - opts *flagOptions ) type importerWithMap func(path, dir string, mode types.ImportMode) (*types.Package, error) @@ -146,7 +177,7 @@ func obfuscatedTypesPackage(path string) *types.Package { if pkg := cachedTypesPackages[path]; pkg != nil { return pkg } - pkg, err := garbledImporter.ImportFrom(path, opts.GarbleDir, 0) + pkg, err := garbledImporter.ImportFrom(path, parentWorkDir, 0) if err != nil { panic(err) } @@ -175,8 +206,8 @@ func main1() int { // 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(opts.Seed)) + if flagSeed.random { + fmt.Fprintf(os.Stderr, "random seed: %s\n", base64.RawStdEncoding.EncodeToString(flagSeed.bytes)) } return 1 } @@ -264,7 +295,6 @@ func mainErr(args []string) error { if err := loadSharedCache(); err != nil { return err } - opts = &cache.Options _, tool := filepath.Split(args[0]) if runtime.GOOS == "windows" { @@ -330,13 +360,9 @@ This command wraps "go %s". Below is its help: return nil, errJustExit(2) } - if err := setFlagOptions(); err != nil { - return nil, err - } - // Here is the only place we initialize the cache. // The sub-processes will parse it from a shared gob file. - cache = &sharedCache{Options: *opts} + cache = &sharedCache{} // Note that we also need to pass build flags to 'go list', such // as -tags. @@ -369,11 +395,38 @@ This command wraps "go %s". Below is its help: } 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 || errors.Is(err, fs.ErrExist) { + err := os.MkdirAll(flagDebugDir, 0o755) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("debugdir error: %v", err) + } + } + + // 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) goArgs := []string{ command, "-trimpath", - "-toolexec=" + cache.ExecPath, + toolexecFlag.String(), } if flagDebugDir != "" { // In case the user deletes the debug directory, @@ -595,11 +648,11 @@ func transformCompile(args []string) ([]string, error) { // because obfuscated literals sometimes escape to heap, // and that's not allowed in the runtime itself. if runtimeAndDeps[curPkg.ImportPath] { - opts.ObfuscateLiterals = false + flagLiterals = false } // Literal obfuscation uses math/rand, so seed it deterministically. - randSeed := opts.Seed + randSeed := flagSeed.bytes if len(randSeed) == 0 { randSeed = curPkg.GarbleActionID } @@ -621,7 +674,7 @@ func transformCompile(args []string) ([]string, error) { for i, file := range files { name := filepath.Base(paths[i]) - if curPkg.ImportPath == "runtime" && opts.Tiny { + if curPkg.ImportPath == "runtime" && flagTiny { // strip unneeded runtime code stripRuntime(name, file) } @@ -650,9 +703,9 @@ func transformCompile(args []string) ([]string, error) { } else { newPaths = append(newPaths, path) } - if opts.DebugDir != "" { + if flagDebugDir != "" { osPkgPath := filepath.FromSlash(curPkg.ImportPath) - pkgDebugDir := filepath.Join(opts.DebugDir, osPkgPath) + pkgDebugDir := filepath.Join(flagDebugDir, osPkgPath) if err := os.MkdirAll(pkgDebugDir, 0o755); err != nil { return nil, err } @@ -1229,7 +1282,7 @@ func (tf *transformer) recordType(t types.Type) { 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. - if opts.ObfuscateLiterals && curPkg.ToObfuscate { + if flagLiterals && curPkg.ToObfuscate { file = literals.Obfuscate(file, tf.info, fset, tf.ignoreObjects) } @@ -1473,7 +1526,7 @@ func locateForeignAlias(dependentImportPath, aliasName string) *types.TypeName { panic(err) // shouldn't happen } for _, importedPath := range lpkg.Imports { - pkg2, err := origImporter.ImportFrom(importedPath, opts.GarbleDir, 0) + pkg2, err := origImporter.ImportFrom(importedPath, parentWorkDir, 0) if err != nil { panic(err) } @@ -1842,7 +1895,7 @@ How to install Go: https://golang.org/doc/install // 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 := ioutil.ReadFile(cache.GoEnv.GOMOD); err == nil { + if mod, err := os.ReadFile(cache.GoEnv.GOMOD); err == nil { modpath := modfile.ModulePath(mod) if modpath != "" { cache.GOGARBLE = modpath + "," + modpath + "_test" diff --git a/position.go b/position.go index 06da04c..5c13856 100644 --- a/position.go +++ b/position.go @@ -103,7 +103,7 @@ func printFile(file1 *ast.File) ([]byte, error) { origNode := origCallExprs[i] i++ newName := "" - if !opts.Tiny { + if !flagTiny { origPos := fmt.Sprintf("%s:%d", filename, fset.Position(origNode.Pos()).Offset) newName = hashWith(curPkg.GarbleActionID, origPos) + ".go" // log.Printf("%q hashed with %x to %q", origPos, curPkg.GarbleActionID, newName) diff --git a/shared.go b/shared.go index e21f1ce..d4d5359 100644 --- a/shared.go +++ b/shared.go @@ -2,13 +2,9 @@ package main import ( "bytes" - "crypto/rand" - "encoding/base64" "encoding/gob" "encoding/json" - "errors" "fmt" - "io/fs" "os" "os/exec" "path/filepath" @@ -25,8 +21,6 @@ type sharedCache struct { ExecPath string // absolute path to the garble binary being used ForwardBuildFlags []string // build flags fed to the original "garble ..." command - Options flagOptions // garble options being used, i.e. our own flags - // ListedPackages contains data obtained via 'go list -json -export -deps'. // This allows us to obtain the non-obfuscated export data of all dependencies, // useful for type checking of the packages as we obfuscate them. @@ -120,73 +114,6 @@ func writeGobExclusive(name string, val interface{}) error { return err } -// flagOptions are derived from the flags -type flagOptions struct { - ObfuscateLiterals bool - Tiny bool - GarbleDir string - DebugDir string - Seed []byte -} - -// setFlagOptions sets flagOptions from the user supplied flags. -func setFlagOptions() error { - wd, err := os.Getwd() - if err != nil { - return err - } - - if cache != nil { - panic("opts set twice?") - } - opts = &flagOptions{ - GarbleDir: wd, - ObfuscateLiterals: flagObfuscateLiterals, - Tiny: flagGarbleTiny, - } - - if flagSeed == "random" { - opts.Seed = make([]byte, 16) // random 128 bit seed - if _, err := rand.Read(opts.Seed); err != nil { - return fmt.Errorf("error generating random seed: %v", err) - } - - } else if len(flagSeed) > 0 { - // We expect unpadded base64, but to be nice, accept padded - // strings too. - flagSeed = strings.TrimRight(flagSeed, "=") - seed, err := base64.RawStdEncoding.DecodeString(flagSeed) - 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)) - } - - opts.Seed = seed - } - - if flagDebugDir != "" { - if !filepath.IsAbs(flagDebugDir) { - flagDebugDir = filepath.Join(wd, flagDebugDir) - } - - if err := os.RemoveAll(flagDebugDir); err == nil || errors.Is(err, fs.ErrExist) { - err := os.MkdirAll(flagDebugDir, 0o755) - if err != nil { - return err - } - } else { - return fmt.Errorf("debugdir error: %v", err) - } - - opts.DebugDir = flagDebugDir - } - - return nil -} - // listedPackage contains the 'go list -json -export' fields obtained by the // root process, shared with all garble sub-processes via a file. type listedPackage struct {