You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/reflect.go

452 lines
13 KiB
Go

package main
import (
"fmt"
"go/types"
"path/filepath"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/tools/go/ssa"
)
// Record all instances of reflection use, and don't obfuscate types which are used in reflection.
func (tf *transformer) recordReflection(ssaPkg *ssa.Package) {
if reflectSkipPkg[ssaPkg.Pkg.Path()] {
return
}
lenPrevKnownReflectAPIs := len(cachedOutput.KnownReflectAPIs)
// find all unchecked APIs to add them to checkedAPIs after the pass
notCheckedAPIs := make(map[string]bool)
for _, knownAPI := range maps.Keys(cachedOutput.KnownReflectAPIs) {
if !tf.reflectCheckedAPIs[knownAPI] {
notCheckedAPIs[knownAPI] = true
}
}
tf.ignoreReflectedTypes(ssaPkg)
// all previously unchecked APIs have now been checked add them to checkedAPIs,
// to avoid checking them twice
maps.Copy(tf.reflectCheckedAPIs, notCheckedAPIs)
// if a new reflectAPI is found we need to Re-evaluate all functions which might be using that API
if len(cachedOutput.KnownReflectAPIs) > lenPrevKnownReflectAPIs {
tf.recordReflection(ssaPkg)
}
}
// find all functions, methods and interface declarations of a package and record their
// reflection use
func (tf *transformer) ignoreReflectedTypes(ssaPkg *ssa.Package) {
for _, memb := range ssaPkg.Members {
switch x := memb.(type) {
case *ssa.Type:
// methods aren't package members only their reciever types are
// so some logic is required to find the methods a type has
method := func(mset *types.MethodSet) {
for i, n := 0, mset.Len(); i < n; i++ {
at := mset.At(i)
if m := ssaPkg.Prog.MethodValue(at); m != nil {
tf.checkFunction(m)
} else {
m := at.Obj().(*types.Func)
// handle interface declarations
tf.checkInterfaceMethod(m)
}
}
}
// yes, finding all methods really only works with both calls
mset := ssaPkg.Prog.MethodSets.MethodSet(x.Type())
method(mset)
mset = ssaPkg.Prog.MethodSets.MethodSet(types.NewPointer(x.Type()))
method(mset)
case *ssa.Function:
// these not only include top level functions, but also synthetic
// functions like the initialization of global variables
tf.checkFunction(x)
}
}
}
// Exported methods with unnamed structs as paramters may be "used" in interface declarations
// elsewhere, these interfaces will break if any method uses reflection on the same parameter.
//
// Therefore never obfuscate unnamed structs which are used as a method parameter
// and treat them like a parameter which is actually used in reflection.
//
// See "UnnamedStructMethod" in the reflect.txtar test for an example.
func (tf *transformer) checkMethodSignature(reflectParams map[int]bool, sig *types.Signature) {
if sig.Recv() == nil {
return
}
params := sig.Params()
for i := 0; i < params.Len(); i++ {
if reflectParams[i] {
continue
}
ignore := false
param := params.At(i)
switch x := param.Type().(type) {
case *types.Struct:
ignore = true
case *types.Array:
if _, ok := x.Elem().(*types.Struct); ok {
ignore = true
}
case *types.Slice:
if _, ok := x.Elem().(*types.Struct); ok {
ignore = true
}
}
if ignore {
reflectParams[i] = true
tf.recursivelyRecordAsNotObfuscated(param.Type())
}
}
}
// Checks the signature of an interface method for potential reflection use.
func (tf *transformer) checkInterfaceMethod(m *types.Func) {
reflectParams := make(map[int]bool)
maps.Copy(reflectParams, cachedOutput.KnownReflectAPIs[m.FullName()])
sig := m.Type().(*types.Signature)
if m.Exported() {
tf.checkMethodSignature(reflectParams, sig)
}
if len(reflectParams) > 0 {
cachedOutput.KnownReflectAPIs[m.FullName()] = reflectParams
/* fmt.Printf("cachedOutput.KnownReflectAPIs: %v\n", cachedOutput.KnownReflectAPIs) */
}
}
// Checks all callsites in a function declaration for use of reflection.
func (tf *transformer) checkFunction(fun *ssa.Function) {
/* if fun != nil && fun.Synthetic != "loaded from gc object file" {
// fun.WriteTo crashes otherwise
fun.WriteTo(os.Stdout)
} */
f, _ := fun.Object().(*types.Func)
reflectParams := make(map[int]bool)
if f != nil {
maps.Copy(reflectParams, cachedOutput.KnownReflectAPIs[f.FullName()])
if f.Exported() {
tf.checkMethodSignature(reflectParams, fun.Signature)
}
}
/* fmt.Printf("f: %v\n", f)
fmt.Printf("fun: %v\n", fun) */
for _, block := range fun.Blocks {
for _, inst := range block.Instrs {
/* fmt.Printf("inst: %v, t: %T\n", inst, inst) */
call, ok := inst.(*ssa.Call)
if !ok {
continue
}
callName := call.Call.Value.String()
if m := call.Call.Method; m != nil {
callName = call.Call.Method.FullName()
}
if tf.reflectCheckedAPIs[callName] {
// only check apis which were not already checked
continue
}
/* fmt.Printf("callName: %v\n", callName) */
// record each call argument passed to a function parameter which is used in reflection
knownParams := cachedOutput.KnownReflectAPIs[callName]
for knownParam := range knownParams {
if len(call.Call.Args) <= knownParam {
continue
}
arg := call.Call.Args[knownParam]
/* fmt.Printf("flagging arg: %v\n", arg) */
visited := make(map[ssa.Value]bool)
reflectedParam := tf.recordArgReflected(arg, visited)
if reflectedParam == nil {
continue
}
pos := slices.Index(fun.Params, reflectedParam)
if pos < 0 {
continue
}
/* fmt.Printf("recorded param: %v func: %v\n", pos, fun) */
reflectParams[pos] = true
}
}
}
if len(reflectParams) > 0 {
cachedOutput.KnownReflectAPIs[f.FullName()] = reflectParams
/* fmt.Printf("cachedOutput.KnownReflectAPIs: %v\n", cachedOutput.KnownReflectAPIs) */
}
}
// recordArgReflected finds the type(s) of a function argument, which is being used in reflection
// and excludes these types from obfuscation
// It also checks if this argument has any relation to a function paramter and returns it if found.
func (tf *transformer) recordArgReflected(val ssa.Value, visited map[ssa.Value]bool) *ssa.Parameter {
// make sure we visit every val only once, otherwise there will be infinite recursion
if visited[val] {
return nil
}
/* fmt.Printf("val: %v %T %v\n", val, val, val.Type()) */
visited[val] = true
switch val := val.(type) {
case *ssa.IndexAddr:
for _, ref := range *val.Referrers() {
if store, ok := ref.(*ssa.Store); ok {
tf.recordArgReflected(store.Val, visited)
}
}
return tf.recordArgReflected(val.X, visited)
case *ssa.Slice:
return tf.recordArgReflected(val.X, visited)
case *ssa.MakeInterface:
return tf.recordArgReflected(val.X, visited)
case *ssa.UnOp:
return tf.recordArgReflected(val.X, visited)
case *ssa.FieldAddr:
return tf.recordArgReflected(val.X, visited)
case *ssa.Alloc:
/* fmt.Printf("recording val %v \n", *val.Referrers()) */
tf.recursivelyRecordAsNotObfuscated(val.Type())
for _, ref := range *val.Referrers() {
if idx, ok := ref.(*ssa.IndexAddr); ok {
tf.recordArgReflected(idx, visited)
}
}
// relatedParam needs to revisit nodes so create an empty map
visited := make(map[ssa.Value]bool)
// check if the found alloc gets tainted by function parameters
return relatedParam(val, visited)
case *ssa.Const:
tf.recursivelyRecordAsNotObfuscated(val.Type())
case *ssa.Global:
tf.recursivelyRecordAsNotObfuscated(val.Type())
// TODO: this might need similar logic to *ssa.Alloc, however
// reassigning a function param to a global variable and then reflecting
// it is probably unlikely to occur
case *ssa.Parameter:
// this only finds the parameters who want to be found,
// otherwise relatedParam is used for more in depth analysis
tf.recursivelyRecordAsNotObfuscated(val.Type())
return val
}
return nil
}
// relatedParam checks if a route to a function paramter can be constructed
// from a ssa.Value, and returns the paramter if it found one.
func relatedParam(val ssa.Value, visited map[ssa.Value]bool) *ssa.Parameter {
// every val should only be visited once to prevent infinite recursion
if visited[val] {
return nil
}
/* fmt.Printf("related val: %v %T %v\n", val, val, val.Type()) */
visited[val] = true
switch x := val.(type) {
case *ssa.Parameter:
// a paramter has been found
return x
case *ssa.UnOp:
if param := relatedParam(x.X, visited); param != nil {
return param
}
case *ssa.FieldAddr:
/* fmt.Printf("addr: %v\n", x)
fmt.Printf("addr.X: %v %T\n", x.X, x.X) */
if param := relatedParam(x.X, visited); param != nil {
return param
}
}
refs := val.Referrers()
if refs == nil {
return nil
}
for _, ref := range *refs {
/* fmt.Printf("ref: %v %T\n", ref, ref) */
var param *ssa.Parameter
switch ref := ref.(type) {
case *ssa.FieldAddr:
param = relatedParam(ref, visited)
case *ssa.UnOp:
param = relatedParam(ref, visited)
case *ssa.Store:
if param := relatedParam(ref.Val, visited); param != nil {
return param
}
param = relatedParam(ref.Addr, visited)
}
if param != nil {
return param
}
}
return nil
}
// recursivelyRecordAsNotObfuscated calls recordAsNotObfuscated on any named
// types and fields under typ.
//
// Only the names declared in the current package are recorded. This is to ensure
// that reflection detection only happens within the package declaring a type.
// Detecting it in downstream packages could result in inconsistencies.
func (tf *transformer) recursivelyRecordAsNotObfuscated(t types.Type) {
switch t := t.(type) {
case *types.Named:
obj := t.Obj()
// TODO: the transformer is only needed in this function, there is
// probably a way to do this with only the ssa information.
if obj.Pkg() == nil || obj.Pkg() != tf.pkg {
return // not from the specified package
}
if recordedAsNotObfuscated(obj) {
return // prevent endless recursion
}
recordAsNotObfuscated(obj)
// Record the underlying type, too.
tf.recursivelyRecordAsNotObfuscated(t.Underlying())
case *types.Struct:
for i := 0; i < t.NumFields(); i++ {
field := t.Field(i)
// This check is similar to the one in *types.Named.
// It's necessary for unnamed struct types,
// as they aren't named but still have named fields.
if field.Pkg() == nil || field.Pkg() != tf.pkg {
return // not from the specified package
}
// Record the field itself, too.
recordAsNotObfuscated(field)
tf.recursivelyRecordAsNotObfuscated(field.Type())
}
case interface{ Elem() types.Type }:
// Get past pointers, slices, etc.
tf.recursivelyRecordAsNotObfuscated(t.Elem())
}
}
// TODO: consider caching recordedObjectString via a map,
// if that shows an improvement in our benchmark
func recordedObjectString(obj types.Object) objectString {
pkg := obj.Pkg()
if obj, ok := obj.(*types.Var); ok && obj.IsField() {
// For exported fields, "pkgpath.Field" is not unique,
// because two exported top-level types could share "Field".
//
// Moreover, note that not all fields belong to named struct types;
// an API could be exposing:
//
// var usedInReflection = struct{Field string}
//
// For now, a hack: assume that packages don't declare the same field
// more than once in the same line. This works in practice, but one
// could craft Go code to break this assumption.
// Also note that the compiler's object files include filenames and line
// numbers, but not column numbers nor byte offsets.
// TODO(mvdan): give this another think, and add tests involving anon types.
pos := fset.Position(obj.Pos())
return fmt.Sprintf("%s.%s - %s:%d", pkg.Path(), obj.Name(),
filepath.Base(pos.Filename), pos.Line)
}
// Names which are not at the top level cannot be imported,
// so we don't need to record them either.
// Note that this doesn't apply to fields, which are never top-level.
if pkg.Scope() != obj.Parent() {
return ""
}
// For top-level exported names, "pkgpath.Name" is unique.
return pkg.Path() + "." + obj.Name()
}
// recordAsNotObfuscated records all the objects whose names we cannot obfuscate.
// An object is any named entity, such as a declared variable or type.
//
// As of June 2022, this only records types which are used in reflection.
// TODO(mvdan): If this is still the case in a year's time,
// we should probably rename "not obfuscated" and "cannot obfuscate" to be
// directly about reflection, e.g. "used in reflection".
func recordAsNotObfuscated(obj types.Object) {
if obj.Pkg().Path() != curPkg.ImportPath {
panic("called recordedAsNotObfuscated with a foreign object")
}
objStr := recordedObjectString(obj)
if objStr == "" {
// If the object can't be described via a qualified string,
// do we need to record it at all?
return
}
cachedOutput.KnownCannotObfuscate[objStr] = struct{}{}
}
func recordedAsNotObfuscated(obj types.Object) bool {
objStr := recordedObjectString(obj)
if objStr == "" {
return false
}
_, ok := cachedOutput.KnownCannotObfuscate[objStr]
return ok
}