Skip to content

Commit fede6c4

Browse files
committed
initial support for build caching
As per the discussion in golang/go#41145, it turns out that we don't need special support for build caching in -toolexec. We can simply modify the behavior of "[...]/compile -V=full" and "[...]/link -V=full" so that they include garble's own version and options in the printed build ID. The part of the build ID that matters is the last, since it's the "content ID" which is used to work out whether there is a need to redo the action (build) or not. Since cmd/go parses the last word in the output as "buildID=...", we simply add "+garble buildID=_/_/_/${hash}". The slashes let us imitate a full binary build ID, but we assume that the other components such as the action ID are not necessary, since the only reader here is cmd/go and it only consumes the content ID. The reported content ID includes the tool's original content ID, garble's own content ID from the built binary, and the garble options which modify how we obfuscate code. If any of the three changes, we should use a different build cache key. GOPRIVATE also affects caching, since a different GOPRIVATE value means that we might have to garble a different set of packages.
1 parent 3970bb9 commit fede6c4

File tree

3 files changed

+136
-44
lines changed

3 files changed

+136
-44
lines changed

README.md

+1-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ information about the original source code as possible.
1616

1717
The tool is designed to be:
1818

19-
* Coupled with `cmd/go`, to support both `GOPATH` and modules with ease
19+
* Coupled with `cmd/go`, to support modules and build caching
2020
* Deterministic and reproducible, given the same initial source code
2121
* Reversible given the original source, to un-garble panic stack traces
2222

@@ -44,9 +44,6 @@ packages to garble, set `GOPRIVATE`, documented at `go help module-private`.
4444
Most of these can improve with time and effort. The purpose of this section is
4545
to document the current shortcomings of this tool.
4646

47-
* Build caching is not supported, so large projects will likely be slow to
48-
build. See [golang/go#41145](https://github.com/golang/go/issues/41145).
49-
5047
* Exported methods and fields are never garbled at the moment, since they could
5148
be required by interfaces and reflection. This area is a work in progress.
5249

import_obfuscation.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ func hashImport(pkg string, garbledImports map[string]string) string {
296296
return garbledPkg
297297
}
298298

299-
garbledPkg := hashWith(buildInfo.imports[pkg].buildID, pkg)
299+
garbledPkg := hashWith(buildInfo.imports[pkg].actionID, pkg)
300300
garbledImports[pkg] = garbledPkg
301301

302302
return garbledPkg

main.go

+134-39
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ var (
9999
})}
100100

101101
buildInfo = struct {
102-
buildID string // from -buildid
102+
actionID []byte // from -buildid
103103
importCfg string // from -importcfg
104104

105105
// TODO: replace part of this with goobj.ParseImportCfg, so that
@@ -218,7 +218,7 @@ func garbledImport(path string) (*types.Package, error) {
218218

219219
type importedPkg struct {
220220
packagefile string
221-
buildID string
221+
actionID []byte
222222

223223
pkg *types.Package
224224
}
@@ -260,7 +260,7 @@ How to install Go: https://golang.org/doc/install
260260
return false
261261
}
262262

263-
rawVersion := string(bytes.TrimPrefix(bytes.TrimSpace(out), []byte("go version ")))
263+
rawVersion := strings.TrimPrefix(strings.TrimSpace(string(out)), "go version ")
264264

265265
tagIdx := strings.IndexByte(rawVersion, ' ')
266266
tag := rawVersion[:tagIdx]
@@ -424,10 +424,13 @@ func mainErr(args []string) error {
424424
}
425425
goArgs := []string{
426426
cmd,
427-
"-a",
428427
"-trimpath",
429428
"-toolexec=" + execPath,
430429
}
430+
if flagDebugDir != "" {
431+
// TODO: don't make -debugdir force rebuilding all packages
432+
goArgs = append(goArgs, "-a")
433+
}
431434
if cmd == "test" {
432435
// vet is generally not useful on garbled code; keep it
433436
// disabled by default.
@@ -455,6 +458,38 @@ func mainErr(args []string) error {
455458
transformed := args[1:]
456459
// log.Println(tool, transformed)
457460
if transform != nil {
461+
if len(args) == 2 && args[1] == "-V=full" {
462+
cmd := exec.Command(args[0], args[1:]...)
463+
out, err := cmd.Output()
464+
if err != nil {
465+
if err, _ := err.(*exec.ExitError); err != nil {
466+
return fmt.Errorf("%v: %s", err, err.Stderr)
467+
}
468+
return err
469+
}
470+
line := string(bytes.TrimSpace(out))
471+
f := strings.Fields(line)
472+
if len(f) < 3 || f[0] != tool || f[1] != "version" || f[2] == "devel" && !strings.HasPrefix(f[len(f)-1], "buildID=") {
473+
return fmt.Errorf("%s -V=full: unexpected output:\n\t%s", args[0], line)
474+
}
475+
var toolID []byte
476+
if f[2] == "devel" {
477+
// On the development branch, use the content ID part of the build ID.
478+
toolID = decodeHash(contentID(f[len(f)-1]))
479+
} else {
480+
// For a release, the output is like: "compile version go1.9.1 X:framepointer".
481+
// Use the whole line.
482+
toolID = []byte(line)
483+
}
484+
485+
out = bytes.TrimSpace(out) // no trailing newline
486+
contentID, err := ownContentID(toolID)
487+
if err != nil {
488+
return fmt.Errorf("cannot obtain garble's own version: %v", err)
489+
}
490+
fmt.Printf("%s +garble buildID=_/_/_/%s\n", line, contentID)
491+
return nil
492+
}
458493
var err error
459494
if transformed, err = transform(transformed); err != nil {
460495
return err
@@ -476,6 +511,38 @@ func mainErr(args []string) error {
476511
return nil
477512
}
478513

514+
const buildIDSeparator = "/"
515+
516+
// actionID returns the action ID half of a build ID, the first element.
517+
func actionID(buildID string) string {
518+
i := strings.Index(buildID, buildIDSeparator)
519+
if i < 0 {
520+
return buildID
521+
}
522+
return buildID[:i]
523+
}
524+
525+
// contentID returns the content ID half of a build ID, the last element.
526+
func contentID(buildID string) string {
527+
return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:]
528+
}
529+
530+
// decodeHash isthe opposite of hashToString, but with a panic for error
531+
// handling since it should never happen.
532+
func decodeHash(str string) []byte {
533+
h, err := base64.RawURLEncoding.DecodeString(str)
534+
if err != nil {
535+
panic(fmt.Sprintf("invalid hash %q: %v", str, err))
536+
}
537+
return h
538+
}
539+
540+
// hashToString encodes the first 120 bits of a sha256 sum in base64, the same
541+
// format used for elements in a build ID.
542+
func hashToString(h []byte) string {
543+
return base64.RawURLEncoding.EncodeToString(h[:15])
544+
}
545+
479546
var transformFuncs = map[string]func([]string) ([]string, error){
480547
"compile": transformCompile,
481548
"link": transformLink,
@@ -484,10 +551,6 @@ var transformFuncs = map[string]func([]string) ([]string, error){
484551
func transformCompile(args []string) ([]string, error) {
485552
var err error
486553
flags, paths := splitFlagsFromFiles(args, ".go")
487-
if len(paths) == 0 {
488-
// Nothing to transform; probably just ["-V=full"].
489-
return args, nil
490-
}
491554

492555
// We will force the linker to drop DWARF via -w, so don't spend time
493556
// generating it.
@@ -542,7 +605,7 @@ func transformCompile(args []string) ([]string, error) {
542605

543606
mathrand.Seed(int64(binary.BigEndian.Uint64(seed)))
544607
} else {
545-
mathrand.Seed(int64(binary.BigEndian.Uint64([]byte(buildInfo.buildID))))
608+
mathrand.Seed(int64(binary.BigEndian.Uint64([]byte(buildInfo.actionID))))
546609
}
547610

548611
info := &types.Info{
@@ -723,12 +786,12 @@ func isPrivate(path string) bool {
723786

724787
// fillBuildInfo initializes the global buildInfo struct via the supplied flags.
725788
func fillBuildInfo(flags []string) error {
726-
buildInfo.buildID = flagValue(flags, "-buildid")
727-
switch buildInfo.buildID {
789+
buildID := flagValue(flags, "-buildid")
790+
switch buildID {
728791
case "", "true":
729792
return fmt.Errorf("could not find -buildid argument")
730793
}
731-
buildInfo.buildID = trimBuildID(buildInfo.buildID)
794+
buildInfo.actionID = decodeHash(actionID(buildID))
732795
buildInfo.importCfg = flagValue(flags, "-importcfg")
733796
if buildInfo.importCfg == "" {
734797
return fmt.Errorf("could not find -importcfg argument")
@@ -755,32 +818,24 @@ func fillBuildInfo(flags []string) error {
755818
continue
756819
}
757820
importPath, objectPath := args[:j], args[j+1:]
758-
fileID, err := buildidOf(objectPath)
821+
buildID, err := buildidOf(objectPath)
759822
if err != nil {
760823
return err
761824
}
762-
// log.Println("buildid:", fileID)
825+
// log.Println("buildid:", buildID)
763826

764827
if len(buildInfo.imports) == 0 {
765828
buildInfo.firstImport = importPath
766829
}
767830
buildInfo.imports[importPath] = importedPkg{
768831
packagefile: objectPath,
769-
buildID: fileID,
832+
actionID: decodeHash(actionID(buildID)),
770833
}
771834
}
772835
// log.Printf("%#v", buildInfo)
773836
return nil
774837
}
775838

776-
func trimBuildID(id string) string {
777-
id = strings.TrimSpace(id)
778-
if i := strings.IndexByte(id, '/'); i > 0 {
779-
id = id[:i]
780-
}
781-
return id
782-
}
783-
784839
func buildidOf(path string) (string, error) {
785840
cmd := exec.Command("go", "tool", "buildid", path)
786841
out, err := cmd.Output()
@@ -790,19 +845,19 @@ func buildidOf(path string) (string, error) {
790845
}
791846
return "", err
792847
}
793-
return trimBuildID(string(bytes.TrimSpace(out))), nil
848+
return string(out), nil
794849
}
795850

796-
func hashWith(salt, value string) string {
851+
func hashWith(salt []byte, name string) string {
797852
const length = 4
798853

799854
d := sha256.New()
800-
io.WriteString(d, salt)
855+
d.Write(salt)
801856
d.Write(seed)
802-
io.WriteString(d, value)
857+
io.WriteString(d, name)
803858
sum := b64.EncodeToString(d.Sum(nil))
804859

805-
if token.IsExported(value) {
860+
if token.IsExported(name) {
806861
return "Z" + sum[:length]
807862
}
808863
return "z" + sum[:length]
@@ -997,12 +1052,12 @@ func transformGo(file *ast.File, info *types.Info, blacklist map[types.Object]st
9971052
default:
9981053
return true // we only want to rename the above
9991054
}
1000-
buildID := buildInfo.buildID
1055+
actionID := buildInfo.actionID
10011056
path := pkg.Path()
10021057
if !isPrivate(path) {
10031058
return true // only private packages are transformed
10041059
}
1005-
if id := buildInfo.imports[path].buildID; id != "" {
1060+
if id := buildInfo.imports[path].actionID; len(id) > 0 {
10061061
garbledPkg, err := garbledImport(path)
10071062
if err != nil {
10081063
panic(err) // shouldn't happen
@@ -1011,12 +1066,12 @@ func transformGo(file *ast.File, info *types.Info, blacklist map[types.Object]st
10111066
if garbledPkg.Scope().Lookup(obj.Name()) != nil {
10121067
return true
10131068
}
1014-
buildID = id
1069+
actionID = id
10151070
}
10161071

10171072
// The exported names cannot be shortened as counter synchronization between packages is not currently implemented
10181073
if token.IsExported(node.Name) {
1019-
node.Name = hashWith(buildID, node.Name)
1074+
node.Name = hashWith(actionID, node.Name)
10201075
return true
10211076
}
10221077

@@ -1031,7 +1086,7 @@ func transformGo(file *ast.File, info *types.Info, blacklist map[types.Object]st
10311086
// orig := node.Name
10321087
privateNameMap[fullName] = name
10331088
node.Name = name
1034-
// log.Printf("%q hashed with %q to %q", orig, buildID, node.Name)
1089+
// log.Printf("%q hashed with %q to %q", orig, actionID, node.Name)
10351090
return true
10361091
}
10371092
return astutil.Apply(file, pre, nil).(*ast.File)
@@ -1077,11 +1132,9 @@ func isTestSignature(sign *types.Signature) bool {
10771132
}
10781133

10791134
func transformLink(args []string) ([]string, error) {
1080-
flags, paths := splitFlagsFromFiles(args, ".a")
1081-
if len(paths) == 0 {
1082-
// Nothing to transform; probably just ["-V=full"].
1083-
return args, nil
1084-
}
1135+
// We can't split by the ".a" extension, because cached object files
1136+
// lack any extension.
1137+
flags, paths := splitFlagsFromArgs(args)
10851138

10861139
if err := fillBuildInfo(flags); err != nil {
10871140
return nil, err
@@ -1117,7 +1170,7 @@ func transformLink(args []string) ([]string, error) {
11171170
// the import config map.
11181171
pkgPath = buildInfo.firstImport
11191172
}
1120-
if id := buildInfo.imports[pkgPath].buildID; id != "" {
1173+
if id := buildInfo.imports[pkgPath].actionID; len(id) > 0 {
11211174
// If the name is not in the map file, it means that the name was not obfuscated or is public
11221175
newName, ok := privateNameMap[pkg+"."+name]
11231176
if !ok {
@@ -1291,3 +1344,45 @@ func flagSetValue(flags []string, name, value string) []string {
12911344
}
12921345
return append(flags, name+"="+value)
12931346
}
1347+
1348+
func ownContentID(toolID []byte) (string, error) {
1349+
// We can't rely on the module version to exist, because it's
1350+
// missing in local builds without 'go get'.
1351+
// For now, use 'go tool buildid' on the binary that's running. Just
1352+
// like Go's own cache, we use hex-encoded sha256 sums.
1353+
// Once https://github.com/golang/go/issues/37475 is fixed, we
1354+
// can likely just use that.
1355+
path, err := os.Executable()
1356+
if err != nil {
1357+
return "", err
1358+
}
1359+
buildID, err := buildidOf(path)
1360+
if err != nil {
1361+
return "", err
1362+
}
1363+
ownID := decodeHash(contentID(buildID))
1364+
1365+
// Join the two content IDs together into a single base64-encoded sha256
1366+
// sum. This includes the original tool's content ID, and garble's own
1367+
// content ID.
1368+
h := sha256.New()
1369+
h.Write(toolID)
1370+
h.Write(ownID)
1371+
1372+
// We also need to add the selected options to the full version string,
1373+
// because all of them result in different output.
1374+
if envGoPrivate != "" {
1375+
fmt.Fprintf(h, " GOPRIVATE=%s", envGoPrivate)
1376+
}
1377+
if envGarbleLiterals {
1378+
fmt.Fprintf(h, " -literals")
1379+
}
1380+
if envGarbleTiny {
1381+
fmt.Fprintf(h, " -tiny")
1382+
}
1383+
if envGarbleSeed != "" {
1384+
fmt.Fprintf(h, " -seed=%x", envGarbleSeed)
1385+
}
1386+
1387+
return hashToString(h.Sum(nil)), nil
1388+
}

0 commit comments

Comments
 (0)