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
pull/635/head
pagran 2 years ago committed by GitHub
parent 417bcf27bb
commit 22c177f088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
}

@ -1,14 +1,14 @@
From de93a968f1bb3500088b30cbdce439e6a0d95e58 Mon Sep 17 00:00:00 2001
From 444ff9310865ff5e4b367d28edfd386ec795524e Mon Sep 17 00:00:00 2001
From: pagran <pagran@protonmail.com>
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")
+ }

@ -0,0 +1,62 @@
From aad38f7aa37d00c723c3540bd8a907b92353d97d Mon Sep 17 00:00:00 2001
From: pagran <pagran@protonmail.com>
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

@ -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)
}

@ -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)

Loading…
Cancel
Save