support go:linkname directives pointing at methods

This is not common, but it is done by a few projects.
Namely, github.com/goccy/go-json reached into reflect's guts,
which included a number of methods:

	internal/runtime/rtype.go
	11://go:linkname rtype_Align reflect.(*rtype).Align
	19://go:linkname rtype_FieldAlign reflect.(*rtype).FieldAlign
	27://go:linkname rtype_Method reflect.(*rtype).Method
	35://go:linkname rtype_MethodByName reflect.(*rtype).MethodByName
	[...]

Add tests for such go:linkname directives pointing at methods.
Note that there are two possible symbol string variants;
"pkg/path.(*Receiver).method" for methods with pointer receivers,
and "pkg/path.Receiver.method" for the rest.

We can't assume that the presence of two dots means a method either.
For example, a package path may be "pkg/path.with.dots",
and so "pkg/path.with.dots.SomeFunc" is the function "SomeFunc"
rather than the method "SomeFunc" on a type "dots".
To account for this ambiguity, rather than splitting on the last dot
like we used to, try to find a package path prefix by splitting on an
increasing number of first dots.

This can in theory still be ambiguous. For example,
we could have the package "pkg/path" expose the method "foo.bar",
and the package "pkg/path.foo" expose the func "bar".
Then, the symbol string "pkg/path.foo.bar" could mean either of them.
However, this seems extremely unlikely to happen in practice,
and I'm not sure that Go's toolchain would support it either.

I also noticed that goccy/go-json still failed to build after the fix.
The reason was that the type reflect.rtype wasn't being obfuscated.
We could, and likely should, teach our assembly and linkname
transformers about which names we chose not to obfuscate due to the use
of reflection. However, in this particular case, reflect's own types
can be obfuscated safely, so just do that.

Fixes #656.
pull/667/head
Daniel Martí 1 year ago
parent 09a17375e3
commit 2ee9cf7a43

@ -1077,33 +1077,73 @@ func (tf *transformer) transformLinkname(localName, newName string) (string, str
return localName, newName
}
// If the package path has multiple dots, split on the last one.
lastDotIdx := strings.LastIndex(newName, ".")
pkgPath, foreignName := newName[:lastDotIdx], newName[lastDotIdx+1:]
lpkg, err := listPackage(pkgPath)
if err != nil {
if errors.Is(err, ErrNotFound) {
pkgSplit := 0
var lpkg *listedPackage
var foreignName string
for {
i := strings.Index(newName[pkgSplit:], ".")
if i < 0 {
// We couldn't find a prefix that matched a known package.
// Probably a made up name like above, but with a dot.
return localName, newName
}
pkgSplit += i
pkgPath := newName[:pkgSplit]
pkgSplit++ // skip over the dot
var err error
lpkg, err = listPackage(pkgPath)
if err == nil {
foreignName = newName[pkgSplit:]
break
}
if errors.Is(err, ErrNotFound) {
// No match; find the next dot.
continue
}
if errors.Is(err, ErrNotDependency) {
fmt.Fprintf(os.Stderr,
"//go:linkname refers to %s - add `import _ %q` so garble can find the package",
"//go:linkname refers to %s - add `import _ %q` for garble to find the package",
newName, pkgPath)
return localName, newName
}
panic(err) // shouldn't happen
}
if lpkg.ToObfuscate && !compilerIntrinsicsFuncs[lpkg.ImportPath+"."+foreignName] {
// The name exists and was obfuscated; obfuscate the new name.
newForeignName := hashWithPackage(lpkg, foreignName)
newPkgPath := pkgPath
if pkgPath != "main" {
newPkgPath = lpkg.obfuscatedImportPath()
if !lpkg.ToObfuscate || compilerIntrinsicsFuncs[lpkg.ImportPath+"."+foreignName] {
// We're not obfuscating that package or name.
return localName, newName
}
var newForeignName string
if receiver, name, ok := strings.Cut(foreignName, "."); ok {
if strings.HasPrefix(receiver, "(*") {
// pkg/path.(*Receiver).method
receiver = strings.TrimPrefix(receiver, "(*")
receiver = strings.TrimSuffix(receiver, ")")
receiver = "(*" + hashWithPackage(lpkg, receiver) + ")"
} else {
// pkg/path.Receiver.method
receiver = hashWithPackage(lpkg, receiver)
}
newName = newPkgPath + "." + newForeignName
// Exported methods are never obfuscated.
//
// TODO: we're duplicating the logic behind these decisions.
// How can we more easily reuse the same logic?
if !token.IsExported(name) {
name = hashWithPackage(lpkg, name)
}
newForeignName = receiver + "." + name
} else {
// pkg/path.function
newForeignName = hashWithPackage(lpkg, foreignName)
}
newPkgPath := lpkg.ImportPath
if newPkgPath != "main" {
newPkgPath = lpkg.obfuscatedImportPath()
}
newName = newPkgPath + "." + newForeignName
return localName, newName
}
@ -1435,7 +1475,7 @@ func (tf *transformer) prefillObjectMaps(files []*ast.File) error {
path, name := fullName[:i], fullName[i+1:]
// -X represents the main package as "main", not its import path.
if path != curPkg.ImportPath && !(path == "main" && curPkg.Name == "main") {
if path != curPkg.ImportPath && (path != "main" || curPkg.Name != "main") {
return // not the current package
}
@ -2016,8 +2056,10 @@ func (tf *transformer) recursivelyRecordAsNotObfuscated(t types.Type) {
switch t := t.(type) {
case *types.Named:
obj := t.Obj()
if obj.Pkg() == nil || obj.Pkg() != tf.pkg {
if pkg := obj.Pkg(); pkg == nil || pkg != tf.pkg {
return // not from the specified package
} else if pkg.Path() == "reflect" {
return // reflect's own types can always be obfuscated
}
if recordedAsNotObfuscated(obj) {
return // prevent endless recursion

@ -2,7 +2,9 @@ garble build
exec ./main
cmp stderr main.stderr
# TODO: why is 'obfuscatedMethod' present?
! binsubstr main$exe 'obfuscatedFunc' 'ObfuscatedFunc'
binsubstr main$exe 'UnobfuscatedMethod'
[short] stop # no need to verify this with -short
@ -33,8 +35,9 @@ package main
import (
_ "os/exec"
"reflect"
_ "strings"
_ "unsafe"
"unsafe"
_ "big.chungus/meme"
"test/main/imported"
@ -52,6 +55,26 @@ func interfaceEqual(a, b any) bool
//go:linkname obfuscatedFunc test/main/imported.ObfuscatedFuncImpl
func obfuscatedFunc() string
// A linkname to an external obfuscated method.
//go:linkname obfuscatedMethod test/main/imported.Receiver.obfuscatedMethod
func obfuscatedMethod(imported.Receiver) string
// A linkname to an external unobfuscated method.
//go:linkname unobfuscatedMethod test/main/imported.Receiver.UnobfuscatedMethod
func unobfuscatedMethod(imported.Receiver) string
// A linkname to an external obfuscated pointer method, with an extra parameter.
//go:linkname obfuscatedPointerMethod test/main/imported.(*Receiver).obfuscatedPointerMethod
func obfuscatedPointerMethod(*imported.Receiver, string) string
// Similar to the above, but for std, plus having to define a type.
// Some libraries do abuse reflect in this way, unfortunately.
type rtype struct{}
//go:linkname rtype_ptrTo reflect.(*rtype).ptrTo
func rtype_ptrTo(*rtype) *rtype
//go:linkname rtype_NumMethod reflect.(*rtype).NumMethod
func rtype_NumMethod(*rtype) int
// A linkname to an entirely made up name, implemented elsewhere.
//go:linkname renamedFunc madeup.newName
func renamedFunc() string
@ -64,7 +87,23 @@ func tagline() string
func main() {
println(byteIndex("01234", '3'))
println(interfaceEqual("Sephiroth", 7))
println(obfuscatedFunc())
r := imported.Receiver{Field: "field value"}
println(obfuscatedMethod(r))
println(unobfuscatedMethod(r))
println(obfuscatedPointerMethod(&r, "another value"))
typ := reflect.TypeOf(new(error)).Elem()
type emptyInterface struct {
_ *rtype
ptr unsafe.Pointer
}
rtyp := (*rtype)(((*emptyInterface)(unsafe.Pointer(&typ))).ptr)
println("rtype_ptrTo non-nil", rtype_ptrTo(rtyp) != nil)
println("rtype_NumMethod", rtype_NumMethod(rtyp))
println(renamedFunc())
println(tagline())
println(imported.ByteIndex("01234", '3'))
@ -82,6 +121,22 @@ func ObfuscatedFuncImpl() string {
return "obfuscated func"
}
type Receiver struct{
Field string
}
func (r Receiver) obfuscatedMethod() string {
return "obfuscated method: " + r.Field
}
func (r *Receiver) obfuscatedPointerMethod(extra string) string {
return "obfuscated pointer method: " + r.Field + " plus " + extra
}
func (r Receiver) UnobfuscatedMethod() string {
return "unobfuscated method: " + r.Field
}
//go:linkname renamedFunc madeup.newName
func renamedFunc() string {
return "renamed func"
@ -105,6 +160,11 @@ func chungify() string {
3
false
obfuscated func
obfuscated method: field value
unobfuscated method: field value
obfuscated pointer method: field value plus another value
rtype_ptrTo non-nil true
rtype_NumMethod 1
renamed func
featuring Dante from the Devil May Cry series
3

Loading…
Cancel
Save