internal/literals: add benchmark to measure the run-time overhead

pull/679/head
pagran 1 year ago committed by GitHub
parent 89facf1648
commit 9a8608f061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,8 @@ import (
"io"
"os/exec"
"strings"
"mvdan.cc/garble/internal/literals"
)
const buildIDSeparator = "/"
@ -144,6 +146,9 @@ func appendFlags(w io.Writer, forBuildHash bool) {
io.WriteString(w, " -seed=")
io.WriteString(w, flagSeed.String())
}
if literals.TestObfuscator != "" && forBuildHash {
io.WriteString(w, literals.TestObfuscator)
}
}
// buildIDComponentLength is the number of bytes each build ID component takes,

@ -31,7 +31,8 @@ const minSize = 8
const maxSize = 2 << 10 // KiB
// Obfuscate replaces literals with obfuscated anonymous functions.
func Obfuscate(obfRand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File {
func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File {
obfRand := newObfRand(rand, file)
pre := func(cursor *astutil.Cursor) bool {
switch node := cursor.Node().(type) {
case *ast.GenDecl:
@ -122,7 +123,7 @@ func Obfuscate(obfRand *mathrand.Rand, file *ast.File, info *types.Info, linkStr
// be used to replace it.
//
// If the input node cannot be obfuscated nil is returned.
func handleCompositeLiteral(obfRand *mathrand.Rand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node {
func handleCompositeLiteral(obfRand *obfRand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node {
if len(node.Elts) < minSize || len(node.Elts) > maxSize {
return nil
}
@ -216,18 +217,18 @@ func withPos(node ast.Node, pos token.Pos) ast.Node {
return node
}
func obfuscateString(obfRand *mathrand.Rand, data string) *ast.CallExpr {
obfuscator := obfuscators[obfRand.Intn(len(obfuscators))]
block := obfuscator.obfuscate(obfRand, []byte(data))
func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator()
block := obfuscator.obfuscate(obfRand.Rand, []byte(data))
block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data"))))
return ah.LambdaCall(ast.NewIdent("string"), block)
}
func obfuscateByteSlice(obfRand *mathrand.Rand, isPointer bool, data []byte) *ast.CallExpr {
obfuscator := obfuscators[obfRand.Intn(len(obfuscators))]
block := obfuscator.obfuscate(obfRand, data)
func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator()
block := obfuscator.obfuscate(obfRand.Rand, data)
if isPointer {
block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{
@ -243,9 +244,9 @@ func obfuscateByteSlice(obfRand *mathrand.Rand, isPointer bool, data []byte) *as
return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block)
}
func obfuscateByteArray(obfRand *mathrand.Rand, isPointer bool, data []byte, length int64) *ast.CallExpr {
obfuscator := obfuscators[obfRand.Intn(len(obfuscators))]
block := obfuscator.obfuscate(obfRand, data)
func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length int64) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator()
block := obfuscator.obfuscate(obfRand.Rand, data)
arrayType := &ast.ArrayType{
Len: ah.IntLit(int(length)),

@ -15,14 +15,19 @@ type obfuscator interface {
obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt
}
// obfuscators contains all types which implement the obfuscator Interface
var obfuscators = []obfuscator{
simple{},
swap{},
split{},
shuffle{},
// seed{}, TODO: re-enable once https://go.dev/issue/47631 is fixed in Go 1.20
}
var (
// Obfuscators contains all types which implement the obfuscator Interface
Obfuscators = []obfuscator{
simple{},
swap{},
split{},
shuffle{},
// seed{}, TODO: re-enable once https://go.dev/issue/47631 is fixed in Go 1.20
}
TestObfuscator string
testPkgToObfuscatorMap map[string]obfuscator
)
func genRandIntSlice(obfRand *mathrand.Rand, max, count int) []int {
indexes := make([]int, count)
@ -66,3 +71,20 @@ func operatorToReversedBinaryExpr(t token.Token, x, y ast.Expr) *ast.BinaryExpr
return expr
}
type obfRand struct {
*mathrand.Rand
testObfuscator obfuscator
}
func (r *obfRand) nextObfuscator() obfuscator {
if r.testObfuscator != nil {
return r.testObfuscator
}
return Obfuscators[r.Intn(len(Obfuscators))]
}
func newObfRand(rand *mathrand.Rand, file *ast.File) *obfRand {
testObf := testPkgToObfuscatorMap[file.Name.Name]
return &obfRand{rand, testObf}
}

@ -0,0 +1,31 @@
//go:build garble_testing
package literals
import (
"os"
"strconv"
"strings"
)
func init() {
obfMapEnv := os.Getenv("GARBLE_TEST_LITERALS_OBFUSCATOR_MAP")
if obfMapEnv == "" {
panic("literals obfuscator map required for testing build")
}
testPkgToObfuscatorMap = make(map[string]obfuscator)
// Parse obfuscator mapping: pkgName1=obfIndex1,pkgName2=obfIndex2
pairs := strings.Split(obfMapEnv, ",")
for _, pair := range pairs {
keyValue := strings.SplitN(pair, "=", 2)
pkgName := keyValue[0]
obfIndex, err := strconv.Atoi(keyValue[1])
if err != nil {
panic(err)
}
testPkgToObfuscatorMap[pkgName] = Obfuscators[obfIndex]
}
TestObfuscator = obfMapEnv
}

@ -0,0 +1,158 @@
// This script generate benchmarks for performance analysis of individual obfuscator literals.
// Note that only the speed of obfuscated methods is measured, initialization cost or build speed are not measured.
package main
import (
_ "embed"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"mvdan.cc/garble/internal/literals"
)
const (
minDataLen = 8
maxDataLen = 64
stepDataLen = 4
dataCountPerLen = 10
moduleName = "test/literals"
garbleSeed = "o9WDTZ4CN4w"
// For benchmarking individual obfuscators, we use package=obfuscator mapping
// and add a prefix to package name to make sure there are no collisions with system packages.
packagePrefix = "literals_bench_"
)
func generateRunSrc() string {
var sb strings.Builder
sb.WriteString(`
var alwaysFalseFlag = false
func noop(i interface{}) {
if alwaysFalseFlag {
println(i)
}
}
func Run() {
`)
dataStr := strings.Repeat("X", maxDataLen)
dataBytes := make([]string, maxDataLen)
for i := 0; i < len(dataBytes); i++ {
dataBytes[i] = strconv.Itoa(i)
}
for dataLen := minDataLen; dataLen <= maxDataLen; dataLen += stepDataLen {
for y := 0; y < dataCountPerLen; y++ {
fmt.Fprintf(&sb, "\tnoop(%q)\n", dataStr[:dataLen])
fmt.Fprintf(&sb, "\tnoop([]byte{%s})\n", strings.Join(dataBytes[:dataLen], ", "))
}
}
sb.WriteString("}\n")
return sb.String()
}
func buildTestGarble(tdir string) string {
garbleBin := filepath.Join(tdir, "garble")
if runtime.GOOS == "windows" {
garbleBin += ".exe"
}
output, err := exec.Command("go", "build", "-tags", "garble_testing", "-o="+garbleBin).CombinedOutput()
if err != nil {
log.Fatalf("garble build failed: %v\n%s", err, string(output))
}
return garbleBin
}
func handle(err error) {
if err != nil {
panic(err)
}
}
func writeDateFile(tdir, obfName, src string) {
pkgName := packagePrefix + obfName
var sb strings.Builder
fmt.Fprintf(&sb, "package %s\n\n", pkgName)
sb.WriteString(src)
dir := filepath.Join(tdir, pkgName)
handle(os.MkdirAll(dir, 0o777))
handle(os.WriteFile(filepath.Join(dir, "data.go"), []byte(sb.String()), 0o777))
}
func writeTestFile(dir, obfName string) {
var sb strings.Builder
sb.WriteString(`package main
import "testing"
`)
pkgName := packagePrefix + obfName
fmt.Fprintf(&sb, "import %q\n", moduleName+"/"+pkgName)
fmt.Fprintf(&sb, `func Benchmark%s(b *testing.B) {
for i := 0; i < b.N; i++ {
%s.Run()
}
}
`, strings.ToUpper(obfName[:1])+obfName[1:], pkgName)
handle(os.WriteFile(filepath.Join(dir, obfName+"_test.go"), []byte(sb.String()), 0o777))
}
func main() {
tdir, err := os.MkdirTemp("", "literals-bench*")
if err != nil {
log.Fatalf("create temp directory failed: %v", err)
}
defer os.RemoveAll(tdir)
if err := os.WriteFile(filepath.Join(tdir, "go.mod"), []byte("module "+moduleName), 0o777); err != nil {
log.Fatalf("write go.mod failed: %v", err)
}
runSrc := generateRunSrc()
writeTest := func(name string) {
writeDateFile(tdir, name, runSrc)
writeTestFile(tdir, name)
}
var packageToObfuscatorIndex []string
for i, obf := range literals.Obfuscators {
obfName := reflect.TypeOf(obf).Name()
writeTest(obfName)
packageToObfuscatorIndex = append(packageToObfuscatorIndex, fmt.Sprintf(packagePrefix+"%s=%d", obfName, i))
}
writeTest("all")
garbleBin := buildTestGarble(tdir)
args := append([]string{"-seed", garbleSeed, "-literals", "test", "-bench"}, os.Args[1:]...)
cmd := exec.Command(garbleBin, args...)
cmd.Env = append(os.Environ(),
// Explicitly specify package for obfuscation to avoid affecting testing package.
"GOGARBLE="+moduleName,
"GARBLE_TEST_LITERALS_OBFUSCATOR_MAP="+strings.Join(packageToObfuscatorIndex, ","),
)
cmd.Dir = tdir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
log.Fatalf("run garble test failed: %v", err)
}
}
Loading…
Cancel
Save