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 return localName, newName
} }
// If the package path has multiple dots, split on the last one. pkgSplit := 0
lastDotIdx := strings.LastIndex(newName, ".") var lpkg *listedPackage
pkgPath, foreignName := newName[:lastDotIdx], newName[lastDotIdx+1:] var foreignName string
for {
lpkg, err := listPackage(pkgPath) i := strings.Index(newName[pkgSplit:], ".")
if err != nil { if i < 0 {
if errors.Is(err, ErrNotFound) { // We couldn't find a prefix that matched a known package.
// Probably a made up name like above, but with a dot. // Probably a made up name like above, but with a dot.
return localName, newName 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) { if errors.Is(err, ErrNotDependency) {
fmt.Fprintf(os.Stderr, 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) newName, pkgPath)
return localName, newName return localName, newName
} }
panic(err) // shouldn't happen panic(err) // shouldn't happen
} }
if lpkg.ToObfuscate && !compilerIntrinsicsFuncs[lpkg.ImportPath+"."+foreignName] {
// The name exists and was obfuscated; obfuscate the new name. if !lpkg.ToObfuscate || compilerIntrinsicsFuncs[lpkg.ImportPath+"."+foreignName] {
newForeignName := hashWithPackage(lpkg, foreignName) // We're not obfuscating that package or name.
newPkgPath := pkgPath return localName, newName
if pkgPath != "main" { }
newPkgPath = lpkg.obfuscatedImportPath()
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 return localName, newName
} }
@ -1435,7 +1475,7 @@ func (tf *transformer) prefillObjectMaps(files []*ast.File) error {
path, name := fullName[:i], fullName[i+1:] path, name := fullName[:i], fullName[i+1:]
// -X represents the main package as "main", not its import path. // -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 return // not the current package
} }
@ -2016,8 +2056,10 @@ func (tf *transformer) recursivelyRecordAsNotObfuscated(t types.Type) {
switch t := t.(type) { switch t := t.(type) {
case *types.Named: case *types.Named:
obj := t.Obj() 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 return // not from the specified package
} else if pkg.Path() == "reflect" {
return // reflect's own types can always be obfuscated
} }
if recordedAsNotObfuscated(obj) { if recordedAsNotObfuscated(obj) {
return // prevent endless recursion return // prevent endless recursion

@ -2,7 +2,9 @@ garble build
exec ./main exec ./main
cmp stderr main.stderr cmp stderr main.stderr
# TODO: why is 'obfuscatedMethod' present?
! binsubstr main$exe 'obfuscatedFunc' 'ObfuscatedFunc' ! binsubstr main$exe 'obfuscatedFunc' 'ObfuscatedFunc'
binsubstr main$exe 'UnobfuscatedMethod'
[short] stop # no need to verify this with -short [short] stop # no need to verify this with -short
@ -33,8 +35,9 @@ package main
import ( import (
_ "os/exec" _ "os/exec"
"reflect"
_ "strings" _ "strings"
_ "unsafe" "unsafe"
_ "big.chungus/meme" _ "big.chungus/meme"
"test/main/imported" "test/main/imported"
@ -52,6 +55,26 @@ func interfaceEqual(a, b any) bool
//go:linkname obfuscatedFunc test/main/imported.ObfuscatedFuncImpl //go:linkname obfuscatedFunc test/main/imported.ObfuscatedFuncImpl
func obfuscatedFunc() string 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. // A linkname to an entirely made up name, implemented elsewhere.
//go:linkname renamedFunc madeup.newName //go:linkname renamedFunc madeup.newName
func renamedFunc() string func renamedFunc() string
@ -64,7 +87,23 @@ func tagline() string
func main() { func main() {
println(byteIndex("01234", '3')) println(byteIndex("01234", '3'))
println(interfaceEqual("Sephiroth", 7)) println(interfaceEqual("Sephiroth", 7))
println(obfuscatedFunc()) 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(renamedFunc())
println(tagline()) println(tagline())
println(imported.ByteIndex("01234", '3')) println(imported.ByteIndex("01234", '3'))
@ -82,6 +121,22 @@ func ObfuscatedFuncImpl() string {
return "obfuscated func" 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 //go:linkname renamedFunc madeup.newName
func renamedFunc() string { func renamedFunc() string {
return "renamed func" return "renamed func"
@ -105,6 +160,11 @@ func chungify() string {
3 3
false false
obfuscated func 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 renamed func
featuring Dante from the Devil May Cry series featuring Dante from the Devil May Cry series
3 3

Loading…
Cancel
Save