Files
garble/testdata/script/reflect.txtar
T
Paul eef2eae50b fix reflection handling for foreign types and aliases (#1032)
Preserve reflected names for foreign named types, alias-backed unnamed structs,
and foreign struct fields by hashing names with the declaring package or struct
identity as appropriate. Extend the reflect script coverage to assert the
foreign type and field names that must now remain visible.

Fixes #996.

Co-Autored by: 0xKismetDev <131729061+0xKismetDev@users.noreply.github.com>
2026-04-18 17:52:09 +02:00

703 lines
16 KiB
Plaintext

exec garble build
exec ./main
cmp stdout main.stdout
! binsubstr main$exe 'garble_main.go' 'test/main' 'importedpkg.','IndirectObfuscated' 'IndirectNamedWithoutReflect' 'AliasIndirectNamedWithReflect' 'AliasIndirectNamedWithoutReflect' 'FmtTypeField' 'LocalObfuscated'
binsubstr main$exe 'ReflectInDefined' 'ExportedField2' 'unexportedField2' 'IndirectUnobfuscated' 'IndirectNamedWithReflect' 'ForeignNamedType' 'DownstreamField' 'SiblingField'
[short] stop # no need to verify this with -short
# Check that the program works as expected without garble.
go build
exec ./main
cmp stdout main.stdout
-- go.mod --
module test/main
go 1.23
-- garble_main.go --
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/gob"
"encoding/json"
"fmt"
"math/big"
"os"
"reflect"
"strings"
"sync"
"text/template"
"unsafe"
"test/main/importedpkg"
"test/main/importedpkg2"
)
var Sink interface{}
func main() {
// Fields still work fine when they are not obfuscated.
fmt.Println(importedpkg.ReflectInDefinedVar.ExportedField2)
fmt.Println(importedpkg.ReflectInDefined{ExportedField2: 5})
// Type names are not obfuscated either, when reflection is used.
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOf(2))
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOfIndirect(4))
fmt.Println(reflect.TypeOf(importedpkg2.ForeignNamedType{}).Name())
// More complex use of reflect.
v := importedpkg.ReflectValueOfVar
printfWithoutPackage("%#v\n", v)
method := reflect.ValueOf(&v).MethodByName("ExportedMethodName")
if method.IsValid() {
fmt.Println(method.Call(nil))
} else {
fmt.Println("method not found")
}
// Use of a common library using reflect, encoding/json.
enc, _ := json.Marshal(EncodingT{Foo: 3})
fmt.Println(string(enc))
// Another common library, text/template.
tmpl := template.Must(template.New("").Parse("Hello {{.Name}}."))
_ = tmpl.Execute(os.Stdout, struct{ Name string }{Name: "Dave"})
fmt.Println() // Always print a newline.
_ = genericRenderTemplate("name: {{.Name}} value: {{.Value}} port: {{.Port}}", GenericTemplateData{
Name: "test",
Value: "example",
Port: 8080,
})
fmt.Println() // Always print a newline.
// Another complex case, involving embedding and another package.
outer := &importedpkg.EmbeddingOuter{}
outer.InnerField = 3
enc, _ = json.Marshal(outer)
fmt.Println(string(enc))
// An edge case; the struct type is defined in a different package.
// Note that the struct type is unnamed, but it still has named fields.
// We only use reflection on it here, not the declaring package.
// As such, we should obfuscate the field name.
// Simply using the field name here used to cause build failures.
_ = reflect.TypeOf(importedpkg.UnnamedWithDownstreamReflect{})
fmt.Printf("%#v\n", importedpkg.UnnamedWithDownstreamReflect{
DownstreamField: "downstream",
})
// An edge case; the struct type is defined in package importedpkg2.
// importedpkg uses reflect.TypeOf on ReflectInDefined, which embeds
// ReflectInSiblingImport, so all reachable fields (including SiblingField)
// must keep real names for reflection
sibling := importedpkg2.ReflectInSiblingImport{
SiblingField: "sibling",
}
st := reflect.TypeOf(sibling)
sf2 := reflect.ValueOf(sibling).Field(0)
fmt.Printf("%s:%q\n", st.Field(0).Name, sf2.String())
// Foreign generic wrapper (cf. cloudflare-go param.Field): all fields reachable
// from reflect.TypeOf must keep real names, including for dynamic FieldByName.
wrapped := importedpkg2.FieldByNameWrapper[string]{Value: "wrapped", Present: true}
wt := reflect.TypeOf(wrapped)
for _, name := range []string{"Value", "Present"} {
sf, ok := wt.FieldByName(name)
if !ok {
panic("FieldByNameWrapper missing field " + name)
}
if sf.Name != name {
panic("unexpected StructField name")
}
}
dynField := "Value"
sf, ok := wt.FieldByName(dynField)
if !ok || sf.Type.Kind() != reflect.String {
panic("dynamic FieldByName on wrapper failed")
}
fmt.Printf("wrapper: %v %v\n",
reflect.ValueOf(wrapped).FieldByName("Value").Interface(),
reflect.ValueOf(wrapped).FieldByName("Present").Bool(),
)
// Using type aliases as both regular fields, and embedded fields.
var emb EmbeddingIndirect
emb.With.IndirectUnobfuscated = "indirect-with"
emb.With.DuplicateFieldName = 3
emb.Without.IndirectObfuscated = "indirect-without"
emb.Without.DuplicateFieldName = 4
fmt.Printf("%v\n", emb)
printfWithoutPackage("%#v\n", emb.With)
// TODO: don't obfuscate the embedded field name here
// printfWithoutPackage("%#v\n", importedpkg.ReflectEmbeddingAlias{})
indirectReflection(IndirectReflection{})
fmt.Println(FmtType{})
// Variadic functions are a bit tricky as the number of parameters is variable.
// We want to notice indirect uses of reflection via all variadic arguments.
_ = importedpkg.VariadicReflect(0, 1, 2, 3)
_ = importedpkg.VariadicReflect(0)
variadic := VariadicReflection{ReflectionField: "variadic"}
_ = importedpkg.VariadicReflect("foo", 1, variadic, false)
printfWithoutPackage("%#v\n", variadic)
testx509()
testGoSpew()
// Very complex reflection used by gorm
user := StatUser{}
find(&user)
// Similar to gorm with composite literals instead of direct assignments
userComp := StatCompUser{}
findComp(&userComp)
x := UnnamedStructInterface(importedpkg.ReflectUnnamedStruct(0))
x.UnnamedStructMethod(struct{ UnnamedStructField string }{UnnamedStructField: "field value"})
// Local names not used in reflection should not be in the final binary,
// even if they are embedded in a struct and become a field name.
type unexportedLocalObfuscated struct{ LocalObfuscatedA int }
type ExportedLocalObfuscated struct{ LocalObfuscatedB int }
type EmbeddingObfuscated struct {
unexportedLocalObfuscated
ExportedLocalObfuscated
}
// Ensure the types are kept in the binary. Use an anonymous type too.
_ = fmt.Sprintf("%#v", EmbeddingObfuscated{})
_ = fmt.Sprintf("%#v", struct{ ExportedLocalObfuscated }{})
// reflection can see all type names, even local ones, so they cannot be obfuscated.
{
type TypeOfNamedField struct{ NamedReflectionField int }
type TypeOfEmbeddedField struct{ EmbeddedReflectionField int }
type TypeOfParent struct {
ReflectionField TypeOfNamedField
TypeOfEmbeddedField
}
t := reflect.TypeOf(TypeOfParent{})
fmt.Println("TypeOfParent's own name:", t.Name())
namedField, _ := t.FieldByName("ReflectionField")
namedFieldField, _ := namedField.Type.FieldByName("NamedReflectionField")
fmt.Println("TypeOfParent named:",
namedField.Type.Name(),
namedFieldField.Name,
)
embedField, _ := t.FieldByName("TypeOfEmbeddedField")
embedFieldField, _ := embedField.Type.FieldByName("EmbeddedReflectionField")
fmt.Println("TypeOfParent embedded:",
embedField.Type.Name(),
embedFieldField.Name,
)
}
y := UnnamedStructFields{}
y.unexportedGoGoProto = new(struct {
mu sync.Mutex
extensionMap map[int32]EncodingT
})
fmt.Println(reflect.TypeOf(Connection{}))
}
type EmbeddingIndirect struct {
// With field names, to test selectors above.
With importedpkg.AliasIndirectNamedWithReflect
Without importedpkg.AliasIndirectNamedWithoutReflect
// Embedding used to crash garble, too.
importedpkg.AliasIndirectNamedWithReflect
}
func printfWithoutPackage(format string, v any) {
s := fmt.Sprintf(format, v)
if _, without, found := strings.Cut(s, "."); found {
s = without
}
fmt.Print(s)
}
type EncodingT struct {
Foo int
}
type RecursiveStruct struct {
*RecursiveStruct
list []RecursiveStruct
}
// This could crash or hang if we don't deal with loops.
var _ = reflect.TypeOf(RecursiveStruct{})
type GenericTemplateData struct {
Name string
Value string
Port int
}
func genericRenderTemplate[T any](src string, data T) error {
tmpl, err := template.New("").Parse(src)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, data)
}
type IndirectReflection struct {
ReflectionField string
}
func indirectReflection(v any) {
fmt.Println(reflect.TypeOf(v).Field(0).Name)
}
type VariadicReflection struct {
ReflectionField string
}
type FmtType struct {
FmtTypeField int
}
// copied from github.com/davecgh/go-spew, which reaches into reflect's internals
func testGoSpew() {
flagValOffset := func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
type flag uintptr
flagField := func(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
type t0 int
var t struct {
A t0
t0
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
flagvA := *flagField(&vA)
flagva := *flagField(&va)
flagvt0 := *flagField(&vt0)
if flagvA&flagva&flagvt0 == 0 {
panic("reflect.Value read-only flag has changed semantics")
}
type T0 int
var T struct {
A T0
T0
a T0
}
vA = reflect.ValueOf(T).FieldByName("A")
va = reflect.ValueOf(T).FieldByName("a")
vt0 = reflect.ValueOf(T).FieldByName("T0")
flagvA = *flagField(&vA)
flagva = *flagField(&va)
flagvt0 = *flagField(&vt0)
if flagvA&flagva&flagvt0 == 0 {
panic("reflect.Value read-only flag has changed semantics")
}
}
// encoding/x509 uses encoding/asn1, which uses reflect.
// In one place it depends on field names; that used to be broken by garble.
func testx509() {
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
panic(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{Organization: []string{"Acme Co"}},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
panic(err)
}
_, err = x509.ParseCertificate(derBytes)
if err != nil {
panic(err)
}
}
type pingMsg struct {
Data string `sshtype:"192"`
}
type pongMsg struct {
Data string `sshtype:"193"`
}
// golang.org/x/crypto/ssh converts a reflected type to another type
func testSSH() {
var msg = pingMsg{
Data: "data",
}
json.Marshal(msg)
_ = pongMsg(msg)
}
// variations similar to ssh
type reflectedMsg struct {
Data string
}
type convertedMsg struct {
Data string
}
func reflectConvert() {
msg := reflectedMsg(convertedMsg{})
json.Marshal(msg)
}
type reflectedMsg2 struct {
Data string
}
type convertedMsg2 struct {
Data string
}
func unrelatedConvert() {
// only discoverable by rechecking the package
_ = convertedMsg2(reflectedMsg2{})
}
func reflectUnrelatedConv() {
var msg = reflectedMsg2{
Data: "data",
}
json.Marshal(msg)
}
type StatUser struct {
Id int64 `gorm:"primaryKey"`
User_Id int64
}
type StatCompUser struct {
Id int64 `gorm:"primaryKey"`
User_Id int64
}
type Transaction struct {
Statement Statement
}
type Statement struct {
Dest interface{}
Model string
}
func find(dest interface{}) {
tx := Transaction{}
tx.Statement.Dest = dest
execute(tx)
}
func findComp(dest interface{}) {
tx := Transaction{
Statement: Statement{
Dest: dest,
},
}
execute(tx)
}
func execute(db Transaction) {
stmt := db.Statement
v := reflect.TypeOf(stmt.Dest)
fmt.Println(v)
}
type UnnamedStructInterface interface {
UnnamedStructMethod(struct{ UnnamedStructField string })
}
// Some projects declare types with unnamed struct fields,
// and the entire type is used via reflection and cannot be obfuscated.
// However, when assigning to these fields, the use of inline anonymous struct types
// confused garble, and it did not obfuscate those inline structs as well.
// That resulted in "cannot use X as Y value in assignment" build errors.
var _ = reflect.TypeOf(UnnamedStructFields{})
type UnnamedStructFields struct {
// As seen in github.com/gogo/protobuf/proto.
unexportedGoGoProto *struct {
mu sync.Mutex
extensionMap map[int32]EncodingT
}
}
func gobStruct() {
type gobAlias struct {
Security []map[string]struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make([]map[string]struct {
List []string
Pad bool
}, 0, len([]string{}))
}
func gobMap() {
type gobAlias struct {
Security map[string]struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make(map[string]struct {
List []string
Pad bool
}, len([]string{}))
}
func gobChan() {
type gobAlias struct {
Security chan struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make(chan struct {
List []string
Pad bool
}, len([]string{}))
}
type Connection struct {
MaxLen struct {
Varchar int
}
}
// NewConnection create a new connection from databaseURL string
func NewConnection() *Connection {
return &Connection{
MaxLen: struct {
Varchar int
}{
Varchar: 0x7FFF,
},
}
}
func closure() {
type gobAlias struct {
Security func() struct {
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
outer := func() func() struct{ Pad bool } {
return func() struct{ Pad bool } {
return struct{ Pad bool }{Pad: true}
}
}
alias.Security = outer()
}
-- importedpkg/imported.go --
package importedpkg
import (
"fmt"
"reflect"
"test/main/importedpkg/indirect"
"test/main/importedpkg2"
)
type ReflectTypeOf int
var _ = reflect.TypeOf(ReflectTypeOf(0))
type ReflectTypeOfIndirect int
var _ = reflect.TypeOf(new([]*ReflectTypeOfIndirect))
type ReflectValueOf struct {
ExportedField string
unexportedField string
}
func (r *ReflectValueOf) ExportedMethodName() string { return "method: " + r.ExportedField }
var ReflectValueOfVar = ReflectValueOf{ExportedField: "abc"}
var _ = reflect.TypeOf(ReflectValueOfVar)
type ReflectInDefined struct {
ExportedField2 int
unexportedField2 int
importedpkg2.ReflectInSiblingImport
}
var ReflectInDefinedVar = ReflectInDefined{ExportedField2: 9000}
var _ = reflect.TypeOf(ReflectInDefinedVar)
var _ = reflect.TypeOf([]*struct{ EmbeddingOuter }{})
type EmbeddingOuter struct {
EmbeddingInner
Anon struct {
AnonField int
}
}
type EmbeddingInner struct {
InnerField int
}
type UnnamedWithDownstreamReflect = struct {
DownstreamField string
}
type (
AliasIndirectNamedWithReflect = indirect.IndirectNamedWithReflect
AliasIndirectNamedWithoutReflect = indirect.IndirectNamedWithoutReflect
)
var _ = reflect.TypeOf(ReflectEmbeddingAlias{})
type ReflectEmbeddingAlias struct {
ReflectEmbeddedAlias
}
type ReflectEmbeddedAlias = ReflectEmbeddingNamed
type ReflectEmbeddingNamed struct{}
func VariadicReflect(x any, ys ...any) int {
_ = reflect.TypeOf(x)
for _, y := range ys {
_ = reflect.TypeOf(y)
}
return len(ys)
}
type ReflectUnnamedStruct int
func (ReflectUnnamedStruct) UnnamedStructMethod(s struct{ UnnamedStructField string }) {
fmt.Println(reflect.TypeOf(s))
}
-- importedpkg2/imported2.go --
package importedpkg2
type ReflectInSiblingImport struct {
SiblingField string
}
type ForeignNamedType struct{}
// FieldByNameWrapper is a generic struct in another package; callers may use
// reflect.FieldByName (including with a non-constant name) on values of this type.
type FieldByNameWrapper[T any] struct {
Value T
Present bool
}
-- importedpkg/indirect/indirect.go --
package indirect
import "reflect"
var _ = reflect.TypeOf(IndirectNamedWithReflect{})
type IndirectNamedWithReflect struct {
IndirectUnobfuscated string
DuplicateFieldName int
}
type IndirectNamedWithoutReflect struct {
IndirectObfuscated string
DuplicateFieldName int
}
-- main.stdout --
9000
{5 0 {}}
ReflectTypeOf
ReflectTypeOfIndirect
ForeignNamedType
ReflectValueOf{ExportedField:"abc", unexportedField:""}
[method: abc]
{"Foo":3}
Hello Dave.
name: test value: example port: 8080
{"InnerField":3,"Anon":{"AnonField":0}}
struct { DownstreamField string }{DownstreamField:"downstream"}
SiblingField:"sibling"
wrapper: wrapped true
{{indirect-with 3} {indirect-without 4} { 0}}
IndirectNamedWithReflect{IndirectUnobfuscated:"indirect-with", DuplicateFieldName:3}
ReflectionField
{0}
VariadicReflection{ReflectionField:"variadic"}
*main.StatUser
*main.StatCompUser
struct { UnnamedStructField string }
TypeOfParent's own name: TypeOfParent
TypeOfParent named: TypeOfNamedField NamedReflectionField
TypeOfParent embedded: TypeOfEmbeddedField EmbeddedReflectionField
main.Connection