From 22c177f08884a8048871e8bcb5b8dc9ac0efb6cf Mon Sep 17 00:00:00 2001 From: pagran Date: Mon, 9 Jan 2023 12:34:30 +0100 Subject: [PATCH] remove all unexported func names with -tiny via the linker The patch to the linker does this when generating the pclntab, which is the binary section containing func names. When `-tiny` is being used, we look for unexported funcs, and set their names to the offset `0` - a shared empty string. We also avoid including the original name in the binary, which saves a significant amount of space. The following stats were collected on GOOS=linux, which show that `-tiny` is now about 4% smaller: go build 1203067 garble build 782336 (old) garble -tiny build 688128 (new) garble -tiny build 659456 --- internal/linker/linker.go | 44 +++++++------ .../patches/0001-add-custom-magic-value.patch | 10 +-- ...dd-unexported-function-name-removing.patch | 62 +++++++++++++++++++ main.go | 3 + testdata/script/tiny.txtar | 30 ++++++++- 5 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 internal/linker/patches/0002-add-unexported-function-name-removing.patch diff --git a/internal/linker/linker.go b/internal/linker/linker.go index e1d081a..2509433 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -16,14 +16,14 @@ import ( "os/exec" "path/filepath" "runtime" - "strings" "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/rogpeppe/go-internal/lockedfile" ) const ( - MagicValueEnv = "GARBLE_LINKER_MAGIC" + MagicValueEnv = "GARBLE_LINK_MAGIC" + TinyEnv = "GARBLE_LINK_TINY" cacheDirName = "garble" versionExt = ".version" @@ -36,10 +36,10 @@ var ( linkerPatchesFS embed.FS ) -func loadLinkerPatches() (string, map[string]string, error) { +func loadLinkerPatches() (version string, modFiles map[string]bool, patches [][]byte, err error) { + modFiles = make(map[string]bool) versionHash := sha256.New() - patches := make(map[string]string) - err := fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error { + err = fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -64,23 +64,17 @@ func loadLinkerPatches() (string, map[string]string, error) { if file.IsNew || file.IsDelete || file.IsCopy || file.IsRename { panic("only modification patch is supported") } - patches[file.OldName] = string(patchBytes) + modFiles[file.OldName] = true } + patches = append(patches, patchBytes) return nil }) if err != nil { - return "", nil, err + return } - return base64.RawStdEncoding.EncodeToString(versionHash.Sum(nil)), patches, nil -} - -// TODO(pagran): Remove git dependency in future -// more information in README.md -func applyPatch(workingDir, patch string) error { - cmd := exec.Command("git", "-C", workingDir, "apply") - cmd.Stdin = strings.NewReader(patch) - return cmd.Run() + version = base64.RawStdEncoding.EncodeToString(versionHash.Sum(nil)) + return } func copyFile(src, target string) error { @@ -111,9 +105,11 @@ func fileExists(path string) bool { return !stat.IsDir() } -func applyPatches(srcDir, workingDir string, patches map[string]string) (map[string]string, error) { +// TODO(pagran): Remove git dependency in future +// more information in README.md +func applyPatches(srcDir, workingDir string, modFiles map[string]bool, patches [][]byte) (map[string]string, error) { mod := make(map[string]string) - for fileName, patch := range patches { + for fileName := range modFiles { oldPath := filepath.Join(srcDir, fileName) newPath := filepath.Join(workingDir, fileName) mod[oldPath] = newPath @@ -121,10 +117,12 @@ func applyPatches(srcDir, workingDir string, patches map[string]string) (map[str if err := copyFile(oldPath, newPath); err != nil { return nil, err } + } - if err := applyPatch(workingDir, patch); err != nil { - return nil, fmt.Errorf("apply patch for %s failed: %v", fileName, err) - } + cmd := exec.Command("git", "-C", workingDir, "apply") + cmd.Stdin = bytes.NewReader(bytes.Join(patches, []byte("\n"))) + if err := cmd.Run(); err != nil { + return nil, err } return mod, nil } @@ -199,7 +197,7 @@ func buildLinker(workingDir string, overlay map[string]string, outputLinkPath st } func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), error) { - patchesVer, patches, err := loadLinkerPatches() + patchesVer, modFiles, patches, err := loadLinkerPatches() if err != nil { panic(fmt.Errorf("cannot retrieve linker patches: %v", err)) } @@ -235,7 +233,7 @@ func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), erro srcDir := filepath.Join(goRoot, baseSrcSubdir) workingDir := filepath.Join(tempDir, "linker-src") - overlay, err := applyPatches(srcDir, workingDir, patches) + overlay, err := applyPatches(srcDir, workingDir, modFiles, patches) if err != nil { return "", nil, err } diff --git a/internal/linker/patches/0001-add-custom-magic-value.patch b/internal/linker/patches/0001-add-custom-magic-value.patch index a4c71fa..6afa596 100644 --- a/internal/linker/patches/0001-add-custom-magic-value.patch +++ b/internal/linker/patches/0001-add-custom-magic-value.patch @@ -1,14 +1,14 @@ -From de93a968f1bb3500088b30cbdce439e6a0d95e58 Mon Sep 17 00:00:00 2001 +From 444ff9310865ff5e4b367d28edfd386ec795524e Mon Sep 17 00:00:00 2001 From: pagran -Date: Sun, 8 Jan 2023 14:12:51 +0100 -Subject: [PATCH 1/1] add custom magic value +Date: Mon, 9 Jan 2023 10:32:32 +0100 +Subject: [PATCH] add custom magic value --- cmd/link/internal/ld/pcln.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cmd/link/internal/ld/pcln.go b/cmd/link/internal/ld/pcln.go -index 34ab86cf12..1ec237ffc8 100644 +index 34ab86cf12..b89a4d650c 100644 --- a/cmd/link/internal/ld/pcln.go +++ b/cmd/link/internal/ld/pcln.go @@ -249,6 +249,19 @@ func (state *pclntab) generatePCHeader(ctxt *Link) { @@ -17,7 +17,7 @@ index 34ab86cf12..1ec237ffc8 100644 } + + // Use garble prefix in variable names to minimize collision risk -+ garbleMagicStr := os.Getenv("GARBLE_LINKER_MAGIC") ++ garbleMagicStr := os.Getenv("GARBLE_LINK_MAGIC") + if garbleMagicStr == "" { + panic("[garble] magic value must be set") + } diff --git a/internal/linker/patches/0002-add-unexported-function-name-removing.patch b/internal/linker/patches/0002-add-unexported-function-name-removing.patch new file mode 100644 index 0000000..434cc67 --- /dev/null +++ b/internal/linker/patches/0002-add-unexported-function-name-removing.patch @@ -0,0 +1,62 @@ +From aad38f7aa37d00c723c3540bd8a907b92353d97d Mon Sep 17 00:00:00 2001 +From: pagran +Date: Mon, 9 Jan 2023 10:27:41 +0100 +Subject: [PATCH] add unexported function name removing + +--- + cmd/link/internal/ld/pcln.go | 27 +++++++++++++++++++++++++++ + 1 file changed, 27 insertions(+) + +diff --git a/cmd/link/internal/ld/pcln.go b/cmd/link/internal/ld/pcln.go +index 1ec237ffc8..e1bea2032c 100644 +--- a/cmd/link/internal/ld/pcln.go ++++ b/cmd/link/internal/ld/pcln.go +@@ -321,10 +321,19 @@ func (state *pclntab) generateFuncnametab(ctxt *Link, funcs []loader.Sym) map[lo + return name[:i], "[...]", name[j+1:] + } + ++ garbleIsRemove := os.Getenv("GARBLE_LINK_TINY") == "true" ++ + // Write the null terminated strings. + writeFuncNameTab := func(ctxt *Link, s loader.Sym) { + symtab := ctxt.loader.MakeSymbolUpdater(s) ++ if garbleIsRemove { ++ symtab.AddStringAt(0, "") ++ } ++ + for s, off := range nameOffsets { ++ if garbleIsRemove && off == 0 { ++ continue ++ } + a, b, c := nameParts(ctxt.loader.SymName(s)) + o := int64(off) + o = symtab.AddStringAt(o, a) +@@ -335,7 +344,25 @@ func (state *pclntab) generateFuncnametab(ctxt *Link, funcs []loader.Sym) map[lo + + // Loop through the CUs, and calculate the size needed. + var size int64 ++ ++ if garbleIsRemove { ++ size = 1 // first byte is reserved for empty string used for all non-exportable method names ++ } ++ garbleIsUnexported := func(s loader.Sym) bool { ++ name, _, _ := nameParts(ctxt.loader.SymName(s)) ++ if name[len(name)-1] == '.' { ++ return true ++ } ++ c := name[strings.LastIndexByte(name, '.')+1] ++ return 'a' <= c && c <= 'z' ++ } ++ + walkFuncs(ctxt, funcs, func(s loader.Sym) { ++ if garbleIsRemove && garbleIsUnexported(s) { ++ nameOffsets[s] = 0 // redirect name to empty string ++ return ++ } ++ + nameOffsets[s] = uint32(size) + a, b, c := nameParts(ctxt.loader.SymName(s)) + size += int64(len(a) + len(b) + len(c) + 1) // NULL terminate +-- +2.38.1.windows.1 + diff --git a/main.go b/main.go index 95775ff..7c688b3 100644 --- a/main.go +++ b/main.go @@ -449,6 +449,9 @@ func mainErr(args []string) error { executablePath = modifiedLinkPath os.Setenv(linker.MagicValueEnv, strconv.FormatUint(uint64(magicValue()), 10)) + if flagTiny { + os.Setenv(linker.TinyEnv, "true") + } log.Printf("replaced linker with: %s", executablePath) } diff --git a/testdata/script/tiny.txtar b/testdata/script/tiny.txtar index 4693d27..32ea496 100644 --- a/testdata/script/tiny.txtar +++ b/testdata/script/tiny.txtar @@ -11,7 +11,8 @@ stderr '^caller: \?\? 1$' # position info is removed stderr '^recovered: ya like jazz?' ! stderr '^init runtime' # GODEBUG prints are hidden, like inittrace=1 ! stderr 'panic: oh noes' # panics are hidden - +stderr 'funcExported false funcUnexported true' +stderr 'funcStructExported false funcStructUnexported true' [short] stop # no need to verify this with -short @@ -21,6 +22,8 @@ garble build stderr '^caller: [0-9a-zA-Z_]+\.go [1-9]' stderr '^recovered: ya like jazz?' stderr 'panic: oh noes' +stderr 'funcExported false funcUnexported false' +stderr 'funcStructExported false funcStructUnexported false' -- go.mod -- module test/main @@ -28,9 +31,32 @@ go 1.19 -- garble_main.go -- package main -import "runtime" +import ( + "reflect" + "runtime" +) + +type testStruct struct {} + +func (testStruct) unexportedFunc() { println("dummy") } + +func (testStruct) ExportedFunc() { println("dummy") } + +func ExportedFunc() { println("dummy") } + +func unexportedFunc() { println("dummy") } + +func isEmptyFuncName(i interface{}) bool { + name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + return len(name) == 0 +} func main() { + println("funcExported", isEmptyFuncName(ExportedFunc), "funcUnexported", isEmptyFuncName(unexportedFunc)) + + var s testStruct + println("funcStructExported", isEmptyFuncName(s.ExportedFunc), "funcStructUnexported", isEmptyFuncName(s.unexportedFunc)) + var v any = "tada" println(v)