Skip to content

Commit

Permalink
support obfuscating the syscall package
Browse files Browse the repository at this point in the history
One more package that further unblocks obfuscating the runtime.
The issue was the TODO we already had about go:linkname directives with
just one argument, which are used in the syscall package.

While here, factor out the obfuscation of linkname directives into
transformLinkname, as it was starting to get a bit complex.
We now support debug logging as well, while still being able to use
"early returns" for some cases where we bail out.

We also need listPackage to treat all runtime sub-packages like it does
runtime itself, as `runtime/internal/syscall` linknames into `syscall`
without it being a dependency as well.

Finally, add a regression test that, without the fix,
properly spots that the syscall package was not obfuscated:

	FAIL: testdata/script/gogarble.txtar:41: unexpected match for ["syscall.RawSyscall6"] in out

Updates #193.
  • Loading branch information
mvdan committed Oct 11, 2022
1 parent e71cb69 commit 7c28663
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 52 deletions.
112 changes: 64 additions & 48 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,9 +888,7 @@ func transformCompile(args []string) ([]string, error) {
}

// Uncomment for some quick debugging. Do not delete.
// if curPkg.ToObfuscate {
// fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, filename, src)
// }
// fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, filename, src)

if path, err := writeTemp(filename, src); err != nil {
return nil, err
Expand Down Expand Up @@ -933,62 +931,80 @@ func (tf *transformer) handleDirectives(comments []*ast.CommentGroup) {
if !strings.HasPrefix(comment.Text, "//go:linkname ") {
continue
}

// We can have either just one argument:
//
// //go:linkname localName
//
// Or two arguments, where the second may refer to a name in a
// different package:
//
// //go:linkname localName newName
// //go:linkname localName pkg.newName
fields := strings.Fields(comment.Text)
if len(fields) != 3 {
// TODO: the 2nd argument is optional, handle when it's not present
continue
localName := fields[1]
newName := ""
if len(fields) == 3 {
newName = fields[2]
}
// This directive has two arguments: "go:linkname localName newName"

// obfuscate the local name, if the current package is obfuscated
if curPkg.ToObfuscate {
fields[1] = hashWithPackage(curPkg, fields[1])
localName, newName = tf.transformLinkname(localName, newName)
fields[1] = localName
if len(fields) == 3 {
fields[2] = newName
}

// If the new name is of the form "pkgpath.Name", and
// we've obfuscated "Name" in that package, rewrite the
// directive to use the obfuscated name.
newName := fields[2]
dotCnt := strings.Count(newName, ".")
if dotCnt < 1 {
// cgo-generated code uses linknames to made up symbol names,
// which do not have a package path at all.
// Replace the comment in case the local name was obfuscated.
comment.Text = strings.Join(fields, " ")
continue
}
switch newName {
case "main.main", "main..inittask", "runtime..inittask":
// The runtime uses some special symbols with "..".
// We aren't touching those at the moment.
continue
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
log.Printf("linkname %q changed to %q", comment.Text, strings.Join(fields, " "))
}
comment.Text = strings.Join(fields, " ")
}
}
}

// If the package path has multiple dots, split on the
// last one.
lastDotIdx := strings.LastIndex(newName, ".")
pkgPath, name := newName[:lastDotIdx], newName[lastDotIdx+1:]
func (tf *transformer) transformLinkname(localName, newName string) (string, string) {
// obfuscate the local name, if the current package is obfuscated
if curPkg.ToObfuscate {
localName = hashWithPackage(curPkg, localName)
}
if newName == "" {
return localName, ""
}
// If the new name is of the form "pkgpath.Name", and we've obfuscated
// "Name" in that package, rewrite the directive to use the obfuscated name.
dotCnt := strings.Count(newName, ".")
if dotCnt < 1 {
// cgo-generated code uses linknames to made up symbol names,
// which do not have a package path at all.
// Replace the comment in case the local name was obfuscated.
return localName, newName
}
switch newName {
case "main.main", "main..inittask", "runtime..inittask":
// The runtime uses some special symbols with "..".
// We aren't touching those at the moment.
return localName, newName
}

lpkg, err := listPackage(pkgPath)
if err != nil {
// Probably a made up name like above, but with a dot.
comment.Text = strings.Join(fields, " ")
continue
}
if lpkg.ToObfuscate {
// The name exists and was obfuscated; obfuscate
// the new name.
newName := hashWithPackage(lpkg, name)
newPkgPath := pkgPath
if pkgPath != "main" {
newPkgPath = lpkg.obfuscatedImportPath()
}
fields[2] = newPkgPath + "." + newName
}
// If the package path has multiple dots, split on the last one.
lastDotIdx := strings.LastIndex(newName, ".")
pkgPath, foreignName := newName[:lastDotIdx], newName[lastDotIdx+1:]

comment.Text = strings.Join(fields, " ")
lpkg, err := listPackage(pkgPath)
if err != nil {
// Probably a made up name like above, but with a dot.
return localName, newName
}
if lpkg.ToObfuscate {
// The name exists and was obfuscated; obfuscate the new name.
newForeignName := hashWithPackage(lpkg, foreignName)
newPkgPath := pkgPath
if pkgPath != "main" {
newPkgPath = lpkg.obfuscatedImportPath()
}
newName = newPkgPath + "." + newForeignName
}
return localName, newName
}

// processImportCfg parses the importcfg file passed to a compile or link step.
Expand Down
6 changes: 2 additions & 4 deletions shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,6 @@ func appendListedPackages(packages []string, withDeps bool) error {
//
// TODO: investigate and resolve each one of these
var cannotObfuscate = map[string]bool{
// "//go:linkname must refer to declared function or variable"
"syscall": true,

// "unknown pc" crashes on windows in the cgo test otherwise
"runtime/cgo": true,

Expand Down Expand Up @@ -333,7 +330,8 @@ func listPackage(path string) (*listedPackage, error) {
// This is due to how it linkname-implements std packages,
// such as sync/atomic or reflect, without importing them in any way.
// If ListedPackages lacks such a package we fill it with "std".
if curPkg.ImportPath == "runtime" {
// Note that this is also allowed for runtime sub-packages.
if curPkg.ImportPath == "runtime" || strings.HasPrefix(curPkg.ImportPath, "runtime/") {
if ok {
return pkg, nil
}
Expand Down
10 changes: 10 additions & 0 deletions testdata/script/gogarble.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ garble build std

# Link a binary importing net/http, which will catch whether or not we
# support ImportMap when linking.
# Also ensure we are obfuscating low-level std packages.
garble build -o=out ./stdimporter
! binsubstr out 'http.ListenAndServe' 'debug.WriteHeapDump' 'time.Now' 'syscall.Listen'

# The same low-level std packages appear in plain sight in regular builds.
go build -o=out_regular ./stdimporter
binsubstr out_regular 'http.ListenAndServe' 'debug.WriteHeapDump' 'time.Now' 'syscall.Listen'

# Also check that a full rebuild is reproducible, via a new GOCACHE.
# This is slow, but necessary to uncover bugs hidden by the build cache.
Expand Down Expand Up @@ -73,11 +79,15 @@ package main
import (
"net/http"
"runtime/debug"
"time"
"syscall"
)

func main() {
http.ListenAndServe("", nil)
// debug.WriteHeapDump is particularly interesting,
// as it is implemented by runtime via a linkname.
debug.WriteHeapDump(1)
time.Now()
syscall.Listen(0, 1)
}

0 comments on commit 7c28663

Please sign in to comment.