# Note that in this test we use "! bincmp" on plaintext output files, # as a workaround for "cmp" not supporting "! cmp". # TODO: now that obfuscation with -seed is deterministic, # can we just rely on the regular "cmp" with fixed output files? # TODO: consider setting these seeds globally, # so we can reuse them across tests and make better use of the shared build cache. env SEED1=OQg9kACEECQ env SEED2=NruiDmVz6/s # Check the binary with a given base64 encoded seed. exec garble -seed=${SEED1} build exec ./main$exe cmp stderr main.stderr binsubstr main$exe 'teststring' 'imported var value' ! binsubstr main$exe 'ImportedVar' ${SEED1} [short] stop # the extra checks are relatively expensive exec ./main$exe test/main/imported cp stderr importedpkg-seed-static-1 # Also check that the binary is reproducible. # No packages should be rebuilt either, thanks to the build cache. cp main$exe main_seed1$exe rm main$exe exec garble -seed=${SEED1}= build -v ! stderr . bincmp main$exe main_seed1$exe exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 # Even if we use the same seed, the same names in a different package # should still be obfuscated in a different way. exec ./main$exe test/main cp stderr mainpkg-seed-static-1 ! bincmp mainpkg-seed-static-1 importedpkg-seed-static-1 # Using different flags which affect the build, such as -literals or -tiny, # should result in the same obfuscation as long as the seed is constant. # TODO: also test that changing non-garble build parameters, # such as GOARCH or -tags, still results in the same hashing via the seed. exec garble -seed=${SEED1} -literals build exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 exec garble -seed=${SEED1} -tiny build exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 # Also check that a different seed leads to a different binary. # We can't know if caching happens here, because of previous test runs. cp main$exe main_seed2$exe rm main$exe exec garble -seed=${SEED2} build ! bincmp main$exe main_seed2$exe exec ./main$exe test/main/imported cp stderr importedpkg-seed-static-2 ! bincmp importedpkg-seed-static-2 importedpkg-seed-static-1 # Use a random seed, which should always trigger a full build. exec garble -seed=random build -v stderr -count=1 '^-seed chosen at random: .+' stderr -count=1 '^runtime$' stderr -count=1 '^test/main$' exec ./main$exe cmp stderr main.stderr binsubstr main$exe 'teststring' 'imported var value' ! binsubstr main$exe 'ImportedVar' exec ./main$exe test/main/imported cp stderr importedpkg-seed-random-1 ! bincmp importedpkg-seed-random-1 importedpkg-seed-static-1 # Also check that the random binary is not reproducible. cp main$exe main_random$exe rm main$exe exec garble -seed=random build -v stderr . ! bincmp main$exe main_random$exe exec ./main$exe test/main/imported cp stderr importedpkg-seed-random-2 ! bincmp importedpkg-seed-random-2 importedpkg-seed-random-1 # Finally, ensure that our runtime and reflect test code does what we think. go build exec ./main$exe cmp stderr main.stderr exec ./main$exe test/main cmp stderr mainpkg.stderr exec ./main$exe test/main/imported cmp stderr importedpkg.stderr -- go.mod -- module test/main go 1.21 -- main.go -- package main import ( "os" "test/main/imported" ) var teststringVar = "teststring" func main() { mainFunc() } func mainFunc() { if len(os.Args) > 1 { switch os.Args[1] { case "test/main": imported.PrintNames(NamedTypeValue, NamedFunc) case "test/main/imported": imported.PrintNames(imported.NamedType{}, imported.NamedFunc) default: panic("unknown package") } } else { println(teststringVar) println(imported.ImportedVar) // When we're obfuscating, check that the obfuscated name lengths vary. // With sixteen hashed names, and a range of 7 (between 6 and 12), // the chances of fourteen repeats are incredibly minuscule. // If that happens, then our randomness is clearly broken. if hashedNames[0] != "main.hashed_00" { var count [16]int for _, name := range hashedNames { name = name[len("main."):] if len(name) < 6 { panic("ended up with a hashed name that's too short: "+name) } if len(name) > 12 { panic("ended up with a hashed name that's too long: "+name) } count[len(name)]++ if count[len(name)] >= 14 { for _, name := range hashedNames { println(name) } panic("six or more hashed names with the same length") } } } } } // A workaround to fool garble's reflect detection, // because we want it to show us the obfuscated NamedType. var NamedTypeValue any = NamedType{} type NamedType struct { NamedField int } func NamedFunc() string { return imported.CallerFuncName() } var hashedNames = []string{ hashed_00(), hashed_01(), hashed_02(), hashed_03(), hashed_04(), hashed_05(), hashed_06(), hashed_07(), hashed_08(), hashed_09(), hashed_10(), hashed_11(), hashed_12(), hashed_13(), hashed_14(), hashed_15(), } func hashed_00() string { return imported.CallerFuncName() } func hashed_01() string { return imported.CallerFuncName() } func hashed_02() string { return imported.CallerFuncName() } func hashed_03() string { return imported.CallerFuncName() } func hashed_04() string { return imported.CallerFuncName() } func hashed_05() string { return imported.CallerFuncName() } func hashed_06() string { return imported.CallerFuncName() } func hashed_07() string { return imported.CallerFuncName() } func hashed_08() string { return imported.CallerFuncName() } func hashed_09() string { return imported.CallerFuncName() } func hashed_10() string { return imported.CallerFuncName() } func hashed_11() string { return imported.CallerFuncName() } func hashed_12() string { return imported.CallerFuncName() } func hashed_13() string { return imported.CallerFuncName() } func hashed_14() string { return imported.CallerFuncName() } func hashed_15() string { return imported.CallerFuncName() } -- imported/imported.go -- package imported import ( "reflect" "runtime" ) var ImportedVar = "imported var value" type NamedType struct { NamedField int } func NamedFunc() string { return CallerFuncName() } func PrintNames(v any, fn func() string) { typ := reflect.TypeOf(v) println("path:", typ.PkgPath()) println("type:", typ.Name()) println("field:", typ.Field(0).Name) println("func: ", fn()) } func CallerFuncName() string { pc, _, _, _ := runtime.Caller(1) fn := runtime.FuncForPC(pc) return fn.Name() } -- main.stderr -- teststring imported var value -- mainpkg.stderr -- path: main type: NamedType field: NamedField func: main.NamedFunc -- importedpkg.stderr -- path: test/main/imported type: NamedType field: NamedField func: test/main/imported.NamedFunc