patch and rebuild cmd/link to modify the magic value in pclntab

This value is hard-coded in the linker and written in a header.
We could rewrite the final binary, like we used to do with import paths,
but that would require once again maintaining libraries to do so.

Instead, we're now modifying the linker to do what we want.
It's not particularly hard, as every Go install has its source code,
and rebuilding a slightly modified linker only takes a few seconds at most.

Thanks to `go build -overlay`, we only need to copy the files we modify,
and right now we're just modifying one file in the toolchain.
We use a git patch, as the change is fairly static and small,
and the patch is easier to understand and maintain.

The other side of this change is in the runtime,
as it also hard-codes the magic value when loading information.
We modify the code via syntax trees in that case, like `-tiny` does,
because the change is tiny (one literal) and the affected lines of code
are modified regularly between major Go releases.

Since rebuilding a slightly modified linker can take a few seconds,
and Go's build cache does not cache linked binaries,
we keep our own cached version of the rebuilt binary in `os.UserCacheDir`.

The feature isn't perfect, and will be improved in the future.
See the TODOs about the added dependency on `git`,
or how we are currently only able to cache one linker binary at once.

Fixes #622.
pull/632/head
pagran 1 year ago committed by GitHub
parent 05d9b4ed26
commit 6ace03322f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -140,6 +140,7 @@ to document the current shortcomings of this tool.
```
* Go plugins are not currently supported; see [#87](https://github.com/burrowers/garble/issues/87).
* Garble requires `git` to patch the linker. That can be avoided once go-gitdiff supports [non-strict patches](https://github.com/bluekeyes/go-gitdiff/issues/30).
### Contributing

@ -3,6 +3,7 @@ module mvdan.cc/garble
go 1.19
require (
github.com/bluekeyes/go-gitdiff v0.7.0
github.com/frankban/quicktest v1.14.3
github.com/google/go-cmp v0.5.8
github.com/rogpeppe/go-internal v1.9.0

@ -1,3 +1,5 @@
github.com/bluekeyes/go-gitdiff v0.7.0 h1:w4SrRFcufU0/tEpWx3VurDBAnWfpxsmwS7yWr14meQk=
github.com/bluekeyes/go-gitdiff v0.7.0/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=

@ -7,6 +7,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"go/token"
"go/types"
@ -189,6 +190,19 @@ func isUpper(b byte) bool { return 'A' <= b && b <= 'Z' }
func toLower(b byte) byte { return b + ('a' - 'A') }
func toUpper(b byte) byte { return b - ('a' - 'A') }
// magicValue returns random magic value based
// on user specified seed or the runtime package's GarbleActionID.
func magicValue() uint32 {
hasher.Reset()
if !flagSeed.present() {
hasher.Write(cache.ListedPackages["runtime"].GarbleActionID)
} else {
hasher.Write(flagSeed.bytes)
}
sum := hasher.Sum(sumBuffer[:0])
return binary.LittleEndian.Uint32(sum)
}
func hashWithPackage(pkg *listedPackage, name string) string {
if !flagSeed.present() {
return hashWithCustomSalt(pkg.GarbleActionID, name)

@ -0,0 +1,250 @@
// Copyright (c) 2022, The Garble Authors.
// See LICENSE for licensing information.
package linker
import (
"bytes"
"crypto/sha256"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/rogpeppe/go-internal/lockedfile"
)
const (
MagicValueEnv = "GARBLE_LINKER_MAGIC"
cacheDirName = "garble"
versionExt = ".version"
garbleCacheDir = "GARBLE_CACHE_DIR"
baseSrcSubdir = "src"
)
var (
//go:embed patches/*.patch
linkerPatchesFS embed.FS
)
func loadLinkerPatches() (string, map[string]string, error) {
versionHash := sha256.New()
patches := make(map[string]string)
err := fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
patchBytes, err := linkerPatchesFS.ReadFile(path)
if err != nil {
return err
}
if _, err := versionHash.Write(patchBytes); err != nil {
return err
}
files, _, err := gitdiff.Parse(bytes.NewReader(patchBytes))
if err != nil {
return err
}
for _, file := range files {
if file.IsNew || file.IsDelete || file.IsCopy || file.IsRename {
panic("only modification patch is supported")
}
patches[file.OldName] = string(patchBytes)
}
return nil
})
if err != nil {
return "", nil, err
}
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()
}
func copyFile(src, target string) error {
targetDir := filepath.Dir(target)
if err := os.MkdirAll(targetDir, 0o777); err != nil {
return err
}
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
targetFile, err := os.Create(target)
if err != nil {
return err
}
defer targetFile.Close()
_, err = io.Copy(targetFile, srcFile)
return err
}
func fileExists(path string) bool {
stat, err := os.Stat(path)
if err != nil {
return false
}
return !stat.IsDir()
}
func applyPatches(srcDir, workingDir string, patches map[string]string) (map[string]string, error) {
mod := make(map[string]string)
for fileName, patch := range patches {
oldPath := filepath.Join(srcDir, fileName)
newPath := filepath.Join(workingDir, fileName)
mod[oldPath] = newPath
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)
}
}
return mod, nil
}
func cachePath(goExe string) (string, error) {
var cacheDir string
if val, ok := os.LookupEnv(garbleCacheDir); ok {
cacheDir = val
} else {
userCacheDir, err := os.UserCacheDir()
if err != nil {
panic(fmt.Errorf("cannot retreive user cache directory: %v", err))
}
cacheDir = userCacheDir
}
cacheDir = filepath.Join(cacheDir, cacheDirName)
if err := os.MkdirAll(cacheDir, 0o777); err != nil {
return "", err
}
// Note that we only keep one patched and built linker in the cache.
// If the user switches between Go versions or garble versions often,
// this may result in rebuilds since we don't keep multiple binaries in the cache.
// We can consider keeping multiple versions of the binary in our cache in the future,
// similar to how GOCACHE works with multiple built versions of the same package.
return filepath.Join(cacheDir, "link"+goExe), nil
}
func getCurrentVersion(goVersion, patchesVer string) string {
return goVersion + " " + patchesVer
}
func checkVersion(linkerPath, goVersion, patchesVer string) (bool, error) {
versionPath := linkerPath + versionExt
version, err := os.ReadFile(versionPath)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return string(version) == getCurrentVersion(goVersion, patchesVer), nil
}
func writeVersion(linkerPath, goVersion, patchesVer string) error {
versionPath := linkerPath + versionExt
return os.WriteFile(versionPath, []byte(getCurrentVersion(goVersion, patchesVer)), 0o777)
}
func buildLinker(workingDir string, overlay map[string]string, outputLinkPath string) error {
file, err := json.Marshal(&struct{ Replace map[string]string }{overlay})
if err != nil {
return err
}
overlayPath := filepath.Join(workingDir, "overlay.json")
if err := os.WriteFile(overlayPath, file, 0o777); err != nil {
return err
}
cmd := exec.Command("go", "build", "-overlay", overlayPath, "-o", outputLinkPath, "cmd/link")
// Explicitly setting GOOS and GOARCH variables prevents conflicts during cross-build
cmd.Env = append(os.Environ(), "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("compiler compile error: %v\n\n%s", err, string(out))
}
return nil
}
func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), error) {
patchesVer, patches, err := loadLinkerPatches()
if err != nil {
panic(fmt.Errorf("cannot retrieve linker patches: %v", err))
}
outputLinkPath, err := cachePath(goExe)
if err != nil {
return "", nil, err
}
mutex := lockedfile.MutexAt(outputLinkPath + ".lock")
unlock, err := mutex.Lock()
if err != nil {
return "", nil, err
}
// If build is successful, mutex unlocking must be on the caller's side
successBuild := false
defer func() {
if !successBuild {
unlock()
}
}()
isCorrectVer, err := checkVersion(outputLinkPath, goVersion, patchesVer)
if err != nil {
return "", nil, err
}
if isCorrectVer && fileExists(outputLinkPath) {
successBuild = true
return outputLinkPath, unlock, nil
}
srcDir := filepath.Join(goRoot, baseSrcSubdir)
workingDir := filepath.Join(tempDir, "linker-src")
overlay, err := applyPatches(srcDir, workingDir, patches)
if err != nil {
return "", nil, err
}
if err := buildLinker(workingDir, overlay, outputLinkPath); err != nil {
return "", nil, err
}
if err := writeVersion(outputLinkPath, goVersion, patchesVer); err != nil {
return "", nil, err
}
successBuild = true
return outputLinkPath, unlock, nil
}

@ -0,0 +1,36 @@
From de93a968f1bb3500088b30cbdce439e6a0d95e58 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
---
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
--- a/cmd/link/internal/ld/pcln.go
+++ b/cmd/link/internal/ld/pcln.go
@@ -249,6 +249,19 @@ func (state *pclntab) generatePCHeader(ctxt *Link) {
if off != size {
panic(fmt.Sprintf("pcHeader size: %d != %d", off, size))
}
+
+ // Use garble prefix in variable names to minimize collision risk
+ garbleMagicStr := os.Getenv("GARBLE_LINKER_MAGIC")
+ if garbleMagicStr == "" {
+ panic("[garble] magic value must be set")
+ }
+ var garbleMagicVal uint32
+ // Use fmt package instead of strconv to avoid importing a new package
+ if _, err := fmt.Sscan(garbleMagicStr, &garbleMagicVal); err != nil {
+ panic(fmt.Errorf("[garble] invalid magic value %s: %v", garbleMagicStr, err))
+ }
+
+ header.SetUint32(ctxt.Arch, 0, garbleMagicVal)
}
state.pcheader = state.addGeneratedSym(ctxt, "runtime.pcheader", size, writeHeader)
--
2.38.1.windows.1

@ -40,6 +40,7 @@ import (
"golang.org/x/mod/semver"
"golang.org/x/tools/go/ast/astutil"
"mvdan.cc/garble/internal/linker"
"mvdan.cc/garble/internal/literals"
)
@ -437,7 +438,22 @@ func mainErr(args []string) error {
} else {
log.Printf("skipping transform on %s with args: %s", tool, strings.Join(transformed, " "))
}
cmd := exec.Command(args[0], transformed...)
executablePath := args[0]
if tool == "link" {
modifiedLinkPath, unlock, err := linker.PatchLinker(cache.GoEnv.GOROOT, cache.GoEnv.GOVERSION, cache.GoEnv.GOEXE, sharedTempDir)
if err != nil {
return fmt.Errorf("cannot get modified linker: %v", err)
}
defer unlock()
executablePath = modifiedLinkPath
os.Setenv(linker.MagicValueEnv, strconv.FormatUint(uint64(magicValue()), 10))
log.Printf("replaced linker with: %s", executablePath)
}
cmd := exec.Command(executablePath, transformed...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
@ -879,10 +895,15 @@ func transformCompile(args []string) ([]string, error) {
for i, file := range files {
basename := filepath.Base(paths[i])
log.Printf("obfuscating %s", basename)
if curPkg.ImportPath == "runtime" && flagTiny {
// strip unneeded runtime code
stripRuntime(basename, file)
tf.removeUnnecessaryImports(file)
if curPkg.ImportPath == "runtime" {
if flagTiny {
// strip unneeded runtime code
stripRuntime(basename, file)
tf.removeUnnecessaryImports(file)
}
if basename == "symtab.go" {
updateMagicValue(file, magicValue())
}
}
tf.handleDirectives(file.Comments)
file = tf.transformGo(file)
@ -2211,7 +2232,7 @@ func flagSetValue(flags []string, name, value string) []string {
func fetchGoEnv() error {
out, err := exec.Command("go", "env", "-json",
// Keep in sync with sharedCache.GoEnv.
"GOOS", "GOMOD", "GOVERSION",
"GOOS", "GOMOD", "GOVERSION", "GOROOT", "GOEXE",
).CombinedOutput()
if err != nil {
// TODO: cover this in the tests.

@ -61,6 +61,11 @@ func TestScript(t *testing.T) {
t.Fatal(err)
}
userCacheDir, err := os.UserCacheDir()
if err != nil {
t.Fatal(err)
}
p := testscript.Params{
Dir: filepath.Join("testdata", "script"),
Setup: func(env *testscript.Env) error {
@ -86,6 +91,7 @@ func TestScript(t *testing.T) {
"gofullversion="+runtime.Version(),
"EXEC_PATH="+execPath,
"GARBLE_CACHE_DIR="+userCacheDir,
)
if os.Getenv("TESTSCRIPT_COVER_DIR") != "" {

@ -5,11 +5,61 @@ package main
import (
"go/ast"
"go/token"
"strconv"
"strings"
ah "mvdan.cc/garble/internal/asthelper"
)
// updateMagicValue updates hardcoded value of hdr.magic
// when verifying header in symtab.go
func updateMagicValue(file *ast.File, magicValue uint32) {
magicUpdated := false
// Find `hdr.magic != 0xfffffff?` in symtab.go and update to random magicValue
updateMagic := func(node ast.Node) bool {
binExpr, ok := node.(*ast.BinaryExpr)
if !ok || binExpr.Op != token.NEQ {
return true
}
selectorExpr, ok := binExpr.X.(*ast.SelectorExpr)
if !ok {
return true
}
if ident, ok := selectorExpr.X.(*ast.Ident); !ok || ident.Name != "hdr" {
return true
}
if selectorExpr.Sel.Name != "magic" {
return true
}
if _, ok := binExpr.Y.(*ast.BasicLit); !ok {
return true
}
binExpr.Y = &ast.BasicLit{
Kind: token.INT,
Value: strconv.FormatUint(uint64(magicValue), 10),
}
magicUpdated = true
return false
}
for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if ok && funcDecl.Name.Name == "moduledataverify1" {
ast.Inspect(funcDecl, updateMagic)
break
}
}
if !magicUpdated {
panic("magic value not updated")
}
}
// stripRuntime removes unnecessary code from the runtime,
// such as panic and fatal error printing, and code that
// prints trace/debug info of the runtime.

@ -51,6 +51,8 @@ type sharedCache struct {
GOMOD string
GOVERSION string
GOROOT string
GOEXE string
}
}

@ -0,0 +1,45 @@
garble build
exec ./main
! cmp stderr main.stderr
[short] stop # no need to verify this with -short
go build
exec ./main
cmp stderr main.stderr
-- go.mod --
module test/main
go 1.19
-- main.go --
package main
import (
"strconv"
"strings"
_ "unsafe"
)
type fakeModuleData struct {
pcHeader *struct {
magic uint32
}
}
//go:linkname activeModules runtime.activeModules
func activeModules() []*fakeModuleData
// genericMagicValue returns magic value without last digit
func genericMagicValue() string {
mod := activeModules()[0]
magicValHex := strings.ToUpper(strconv.FormatUint(uint64(mod.pcHeader.magic), 16))
return "0x" + magicValHex[:len(magicValHex)-1] + "?"
}
func main() {
println(genericMagicValue())
}
-- main.stderr --
0xFFFFFFF?
Loading…
Cancel
Save