From 88a27d491be07b6cacac01a6f077c53ad6a8cde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 4 Mar 2022 16:29:28 +0000 Subject: [PATCH] add support for -ldflags using quotes In particular, using -ldflags with - In particular, a command like: garble -literals build -ldflags='-X "main.foo=foo bar"' would fail, because we would try to use "\"main" as the package name for the -X qualified name, with the leading quote character. This is because we used strings.Split(ldflags, " "). Instead, use the same quoted.Split that cmd/go uses, copied over thanks to x/tools/cmd/bundle and go:generate. Updates #492. --- cmdgo_quoted.go | 128 +++++++++++++++++++++++++++++++++++ main.go | 16 +++-- testdata/scripts/ldflags.txt | 12 +++- 3 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 cmdgo_quoted.go diff --git a/cmdgo_quoted.go b/cmdgo_quoted.go new file mode 100644 index 0000000..e2cea81 --- /dev/null +++ b/cmdgo_quoted.go @@ -0,0 +1,128 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted + +// Package quoted provides string manipulation utilities. +// + +package main + +import ( + "flag" + "fmt" + "strings" + "unicode" +) + +func cmdgoQuotedisSpaceByte(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' +} + +// Split splits s into a list of fields, +// allowing single or double quotes around elements. +// There is no unescaping or other processing within +// quoted fields. +func cmdgoQuotedSplit(s string) ([]string, error) { + // Split fields allowing '' or "" around elements. + // Quotes further inside the string do not count. + var f []string + for len(s) > 0 { + for len(s) > 0 && cmdgoQuotedisSpaceByte(s[0]) { + s = s[1:] + } + if len(s) == 0 { + break + } + // Accepted quoted string. No unescaping inside. + if s[0] == '"' || s[0] == '\'' { + quote := s[0] + s = s[1:] + i := 0 + for i < len(s) && s[i] != quote { + i++ + } + if i >= len(s) { + return nil, fmt.Errorf("unterminated %c string", quote) + } + f = append(f, s[:i]) + s = s[i+1:] + continue + } + i := 0 + for i < len(s) && !cmdgoQuotedisSpaceByte(s[i]) { + i++ + } + f = append(f, s[:i]) + s = s[i:] + } + return f, nil +} + +// Join joins a list of arguments into a string that can be parsed +// with Split. Arguments are quoted only if necessary; arguments +// without spaces or quotes are kept as-is. No argument may contain both +// single and double quotes. +func cmdgoQuotedJoin(args []string) (string, error) { + var buf []byte + for i, arg := range args { + if i > 0 { + buf = append(buf, ' ') + } + var sawSpace, sawSingleQuote, sawDoubleQuote bool + for _, c := range arg { + switch { + case c > unicode.MaxASCII: + continue + case cmdgoQuotedisSpaceByte(byte(c)): + sawSpace = true + case c == '\'': + sawSingleQuote = true + case c == '"': + sawDoubleQuote = true + } + } + switch { + case !sawSpace && !sawSingleQuote && !sawDoubleQuote: + buf = append(buf, []byte(arg)...) + + case !sawSingleQuote: + buf = append(buf, '\'') + buf = append(buf, []byte(arg)...) + buf = append(buf, '\'') + + case !sawDoubleQuote: + buf = append(buf, '"') + buf = append(buf, []byte(arg)...) + buf = append(buf, '"') + + default: + return "", fmt.Errorf("argument %q contains both single and double quotes and cannot be quoted", arg) + } + } + return string(buf), nil +} + +// A Flag parses a list of string arguments encoded with Join. +// It is useful for flags like cmd/link's -extldflags. +type cmdgoQuotedFlag []string + +var _ flag.Value = (*cmdgoQuotedFlag)(nil) + +func (f *cmdgoQuotedFlag) Set(v string) error { + fs, err := cmdgoQuotedSplit(v) + if err != nil { + return err + } + *f = fs[:len(fs):len(fs)] + return nil +} + +func (f *cmdgoQuotedFlag) String() string { + if f == nil { + return "" + } + s, err := cmdgoQuotedJoin(*f) + if err != nil { + return strings.Join(*f, " ") + } + return s +} diff --git a/main.go b/main.go index f8dbe05..2c68ef2 100644 --- a/main.go +++ b/main.go @@ -700,7 +700,9 @@ func transformCompile(args []string) ([]string, error) { // debugf("seeding math/rand with %x\n", randSeed) mathrand.Seed(int64(binary.BigEndian.Uint64(randSeed))) - tf.prefillObjectMaps(files) + 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". @@ -1108,15 +1110,20 @@ func (tf *transformer) findReflectFunctions(files []*ast.File) { } } +//go:generate go run golang.org/x/tools/cmd/bundle@v0.1.9 -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted + // 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) { +func (tf *transformer) prefillObjectMaps(files []*ast.File) error { tf.linkerVariableStrings = make(map[types.Object]string) - ldflags := flagValue(cache.ForwardBuildFlags, "-ldflags") - flagValueIter(strings.Split(ldflags, " "), "-X", func(val string) { + ldflags, err := cmdgoQuotedSplit(flagValue(cache.ForwardBuildFlags, "-ldflags")) + if err != nil { + return err + } + flagValueIter(ldflags, "-X", func(val string) { // val is in the form of "importpath.name=value". i := strings.IndexByte(val, '=') if i < 0 { @@ -1188,6 +1195,7 @@ func (tf *transformer) prefillObjectMaps(files []*ast.File) { } ast.Inspect(file, visit) } + return nil } // transformer holds all the information and state necessary to obfuscate a diff --git a/testdata/scripts/ldflags.txt b/testdata/scripts/ldflags.txt index 359d63a..3b74286 100644 --- a/testdata/scripts/ldflags.txt +++ b/testdata/scripts/ldflags.txt @@ -1,7 +1,13 @@ env GOGARBLE=* # Note the proper domain, since the dot adds an edge case. -env LDFLAGS='-X=main.unexportedVersion=v1.22.33 -X=main.replacedWithEmpty= -X=domain.test/main/imported.ExportedUnset=garble_replaced -X=domain.test/missing/path.missingVar=value' +# +# Also note that there are three forms of -X allowed: +# +# -X=name=value +# -X name=value +# -X "name=value" (or with single quotes, allows spaces in value) +env LDFLAGS='-X=main.unexportedVersion=v1.22.33 -X=main.replacedWithEmpty= -X "main.replacedWithSpaces= foo bar " -X=domain.test/main/imported.ExportedUnset=garble_replaced -X=domain.test/missing/path.missingVar=value' garble build -ldflags=${LDFLAGS} exec ./main @@ -39,9 +45,12 @@ var unexportedVersion = "unknown" var notReplacedBefore, replacedWithEmpty, notReplacedAfter = "kept_before", "original", "kept_after" +var replacedWithSpaces = "original" + func main() { fmt.Printf("version: %q\n", unexportedVersion) fmt.Printf("becomes empty: %q\n", replacedWithEmpty) + fmt.Printf("becomes string with spaces: %q\n", replacedWithSpaces) fmt.Printf("should be kept: %q, %q\n", notReplacedBefore, notReplacedAfter) fmt.Printf("no longer unset: %q\n", imported.ExportedUnset) } @@ -56,5 +65,6 @@ var ( -- main.stdout -- version: "v1.22.33" becomes empty: "" +becomes string with spaces: " foo bar " should be kept: "kept_before", "kept_after" no longer unset: "garble_replaced"