diff --git a/internal/literals/fuzz_test.go b/internal/literals/fuzz_test.go index 5855580..bad5b3d 100644 --- a/internal/literals/fuzz_test.go +++ b/internal/literals/fuzz_test.go @@ -45,7 +45,7 @@ func FuzzObfuscate(f *testing.F) { f.Add("long_enough_string", initialRandSeed) f.Add("binary_\x00\x01\x02", initialRandSeed) f.Add("whitespace \n\t\t", initialRandSeed) - f.Add(strings.Repeat("x", (2<<10)+1), initialRandSeed) // past maxSize + f.Add(strings.Repeat("x", (2<<10)+1), initialRandSeed) // past MaxSize tdir := f.TempDir() var tdirCounter atomic.Int64 diff --git a/internal/literals/literals.go b/internal/literals/literals.go index a3f2e45..58bcbea 100644 --- a/internal/literals/literals.go +++ b/internal/literals/literals.go @@ -21,10 +21,12 @@ import ( // moderate, this also decreases the likelihood for performance slowdowns. const MinSize = 8 -// maxSize is the upper limit of the size of string-like literals -// which we will obfuscate with any of the available obfuscators. -// Beyond that we apply only a subset of obfuscators which are guaranteed to run efficiently. -const maxSize = 2 << 10 // KiB +// MaxSize is the upper limit of the size of string-like literals we will obfuscate. +const MaxSize = 2 << 10 // 2 KiB + +// MaxSizeExpensive is the upper limit for using expensive obfuscators (split, seed). +// Above this size, only cheap obfuscators are used. +const MaxSizeExpensive = 256 const ( // minStringJunkBytes defines the minimum number of junk bytes to prepend or append during string obfuscation. @@ -83,7 +85,7 @@ func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkString if typeAndValue.Type == types.Typ[types.String] && typeAndValue.Value != nil { value := constant.StringVal(typeAndValue.Value) - if len(value) < MinSize { + if len(value) < MinSize || len(value) > MaxSize { return true } @@ -143,7 +145,7 @@ func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkString // // If the input node cannot be obfuscated nil is returned. func handleCompositeLiteral(obfRand *obfRand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { - if len(node.Elts) < MinSize { + if len(node.Elts) < MinSize || len(node.Elts) > MaxSize { return nil } @@ -357,9 +359,11 @@ func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length in } func getNextObfuscator(obfRand *obfRand, size int) obfuscator { - if size <= maxSize { - return obfRand.nextObfuscator() - } else { - return obfRand.nextLinearTimeObfuscator() + if size < MinSize || size > MaxSize { + panic(fmt.Sprintf("getNextObfuscator called with size %d outside [%d, %d]", size, MinSize, MaxSize)) } + if size <= MaxSizeExpensive { + return obfRand.nextObfuscator() + } + return obfRand.nextCheapObfuscator() } diff --git a/internal/literals/obfuscators.go b/internal/literals/obfuscators.go index ba1b747..368fc3a 100644 --- a/internal/literals/obfuscators.go +++ b/internal/literals/obfuscators.go @@ -60,20 +60,20 @@ type obfuscator interface { } var ( - simpleObfuscator = simple{} - - // Obfuscators contains all types which implement the obfuscator Interface + // Obfuscators contains all types which implement the obfuscator Interface. Obfuscators = []obfuscator{ - simpleObfuscator, + simple{}, swap{}, split{}, shuffle{}, seed{}, } - // LinearTimeObfuscators contains all types which implement the obfuscator Interface and can safely be used on large literals - LinearTimeObfuscators = []obfuscator{ - simpleObfuscator, + // CheapObfuscators contains obfuscators safe to use on large literals. + // The expensive obfuscators scale poorly, so they are excluded here. + CheapObfuscators = []obfuscator{ + simple{}, + swap{}, } TestObfuscator string @@ -268,11 +268,11 @@ func (r *obfRand) nextObfuscator() obfuscator { return Obfuscators[r.Intn(len(Obfuscators))] } -func (r *obfRand) nextLinearTimeObfuscator() obfuscator { +func (r *obfRand) nextCheapObfuscator() obfuscator { if r.testObfuscator != nil { return r.testObfuscator } - return LinearTimeObfuscators[r.Intn(len(LinearTimeObfuscators))] + return CheapObfuscators[r.Intn(len(CheapObfuscators))] } func newObfRand(rand *mathrand.Rand, file *ast.File, nameFunc NameProviderFunc) *obfRand { diff --git a/main_test.go b/main_test.go index dd4a344..59785a6 100644 --- a/main_test.go +++ b/main_test.go @@ -26,6 +26,7 @@ import ( "github.com/rogpeppe/go-internal/testscript" ah "mvdan.cc/garble/internal/asthelper" + "mvdan.cc/garble/internal/literals" ) var proxyURL string @@ -248,23 +249,10 @@ func bincmp(ts *testscript.TestScript, neg bool, args []string) { var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano())) -func generateStringLit(minSize int) *ast.BasicLit { - buffer := make([]byte, minSize) - _, err := testRand.Read(buffer) - if err != nil { - panic(err) - } +const uniqueLitString = "garble_unique_string" - return ah.StringLit(string(buffer) + "a_unique_string_that_is_part_of_all_extra_literals") -} - -// generateLiterals creates a new source code file with a few random literals inside. -// All literals contain the string "a_unique_string_that_is_part_of_all_extra_literals" -// so we can later check if they are all obfuscated by looking for this substring. -// The code is designed such that the Go compiler does not optimize away the literals, -// which would destroy the test. -// This is achieved by defining a global variable `var x = ""` and an `init` function -// which appends all literals to `x`. +// generateLiterals creates a source file with random string literals appended +// to a global var in init, preventing the compiler from optimizing them away. func generateLiterals(ts *testscript.TestScript, neg bool, args []string) { if neg { ts.Fatalf("unsupported: ! generate-literals") @@ -290,29 +278,32 @@ func generateLiterals(ts *testscript.TestScript, neg bool, args []string) { var statements []ast.Stmt - // Assignments which append 100 random small literals to x: `x += "the_small_random_literal"` + // 100 literals up to MaxSize, all containing uniqueLitString. for range 100 { + randSize := testRand.Intn(literals.MaxSize - len(uniqueLitString) + 1) + buffer := make([]byte, randSize) + testRand.Read(buffer) statements = append( statements, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("x")}, Tok: token.ADD_ASSIGN, - Rhs: []ast.Expr{generateStringLit(1 + testRand.Intn(255))}, + Rhs: []ast.Expr{ah.StringLit(string(buffer) + uniqueLitString)}, }, ) } - // Assignments which append 5 random huge literals to x: `x += "the_huge_random_literal"` - // We add huge literals to make sure we obfuscate them fast. - // 5 * 128KiB is large enough that it would take a very, very long time - // to obfuscate those literals if too complex obfuscators are used. + // 5 huge literals past MaxSize, without uniqueLitString; not obfuscated. for range 5 { + size := literals.MaxSize + 1 + testRand.Intn(128<<10) + buffer := make([]byte, size) + testRand.Read(buffer) statements = append( statements, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("x")}, Tok: token.ADD_ASSIGN, - Rhs: []ast.Expr{generateStringLit(128 << 10)}, + Rhs: []ast.Expr{ah.StringLit(string(buffer))}, }, ) } diff --git a/testdata/script/literals.txtar b/testdata/script/literals.txtar index 5670ecb..21f2724 100644 --- a/testdata/script/literals.txtar +++ b/testdata/script/literals.txtar @@ -27,13 +27,13 @@ generate-literals extra_literals.go # ensure we find the extra literals in an unobfuscated build go build -binsubstr main$exe 'a_unique_string_that_is_part_of_all_extra_literals' +binsubstr main$exe 'garble_unique_string' # ensure we don't find the extra literals in an obfuscated build exec garble -literals -debugdir=debug1 build exec ./main$exe cmp stderr main.stderr -! binsubstr main$exe 'a_unique_string_that_is_part_of_all_extra_literals' +! binsubstr main$exe 'garble_unique_string' # Check obfuscators.