properly record when type aliases are embedded as fields

There are two scenarios when it comes to embedding fields.
The first is easy, and we always handled it well:

	type Named struct { Foo int }

	type T struct { Named }

In this scenario, T ends up with an embedded field named "Named",
and a promoted field named "Foo".

Then there's the form with a type alias:

	type Named struct { Foo int }

	type Alias = Named

	type T struct { Alias }

This case is different: T ends up with an embedded field named "Alias",
and a promoted field named "Foo".
Note how the field gets its name from the referenced type,
even if said type is just an alias to another type.

This poses two problems.
First, we must obfuscate the field T.Alias as the name "Alias",
and not as the name "Named" that the alias points to.
Second, we must be careful of cases where Named and Alias are declared
in different packages, as they will obfuscate the same name differently.

Both of those problems compounded in the reported issue.
The actual reason is that quic-go has a type alias in the form of:

	type ConnectionState = qtls.ConnectionState

In other words, the entire problem boils down to a type alias which
points to a named type in a different package, where both types share
the same name. For example:

	package parent

	import "parent/p1"

	type T struct { p1.SameName }

	[...]

	package p1

	import "parent/p2"

	type SameName = p2.SameName

	[...]

	package p2

	type SameName struct { Foo int }

This broke garble because we had a heuristic to detect when an embedded
field was a type alias:

	// Instead, detect such a "foreign alias embed".
	// If we embed a final named type,
	// but the field name does not match its name,
	// then it must have been done via an alias.
	// We dig out the alias's TypeName via locateForeignAlias.
	if named.Obj().Name() != node.Name {

As the reader can deduce, this heuristic would incorrectly assume that
the snippet above does not embed a type alias, when in fact it does.
When obfuscating the field T.SameName, which uses a type alias,
we would correctly obfuscate the name "SameName",
but we would incorrectly obfuscate it with the package p2, not p1.
This would then result in build errors.

To fix this problem for good, we need to get rid of the heuristic.
Instead, we now mimic what was done for KnownCannotObfuscate,
but for embedded fields which use type aliases.
KnownEmbeddedAliasFields is now filled for each package
and stored in the cache as part of cachedOutput.
We can then detect the "embedded alias" case reliably,
even when the field is declared in an imported package.

On the plus side, we get to remove locateForeignAlias.
We also add a couple of TODOs to record further improvements.
Finally, add a test.

Fixes #466.
pull/484/head
Daniel Martí 2 years ago committed by lu4p
parent 73e91fd8c0
commit 955c24856c

@ -995,8 +995,14 @@ func processImportCfg(flags []string) (newImportCfg string, _ error) {
}
type (
funcFullName = string
funcFullName = string // as per go/types.Func.FullName
objectString = string // as per recordedObjectString
reflectParameterPosition = int
typeName struct {
PkgPath, Name string
}
)
// cachedOutput contains information that will be stored as per garbleExportFile.
@ -1014,13 +1020,22 @@ var cachedOutput = struct {
// weren't obfuscated, so we can obfuscate their local uses accordingly.
//
// TODO: merge cannotObfuscateNames into this directly
KnownCannotObfuscate map[string]struct{}
KnownCannotObfuscate map[objectString]struct{}
// KnownEmbeddedAliasFields records which embedded fields use a type alias.
// They are the only instance where a type alias matters for obfuscation,
// because the embedded field name is derived from the type alias itself,
// and not the type that the alias points to.
// In that way, the type alias is obfuscated as a form of named type,
// bearing in mind that it may be owned by a different package.
KnownEmbeddedAliasFields map[objectString]typeName
}{
KnownReflectAPIs: map[funcFullName][]reflectParameterPosition{
"reflect.TypeOf": {0},
"reflect.ValueOf": {0},
},
KnownCannotObfuscate: map[string]struct{}{},
KnownCannotObfuscate: map[string]struct{}{},
KnownEmbeddedAliasFields: map[string]typeName{},
}
// garbleExportFile returns an absolute path to a build cache entry
@ -1267,10 +1282,6 @@ type transformer struct {
// fieldToStruct helps locate struct types from any of their field
// objects. Useful when obfuscating field names.
fieldToStruct map[*types.Var]*types.Struct
// fieldToAlias helps tell if an embedded struct field object is a type
// alias. Useful when obfuscating field names.
fieldToAlias map[*types.Var]*types.TypeName
}
// newTransformer helps initialize some maps.
@ -1283,7 +1294,6 @@ func newTransformer() *transformer {
},
recordTypeDone: make(map[types.Type]bool),
fieldToStruct: make(map[*types.Var]*types.Struct),
fieldToAlias: make(map[*types.Var]*types.TypeName),
}
}
@ -1303,15 +1313,29 @@ func (tf *transformer) typecheck(files []*ast.File) error {
}
}
for name, obj := range tf.info.Uses {
if obj != nil {
if obj, ok := obj.(*types.TypeName); ok && obj.IsAlias() {
vr, _ := tf.info.Defs[name].(*types.Var)
if vr != nil {
tf.fieldToAlias[vr] = obj
}
}
tf.recordType(obj.Type())
if obj == nil {
continue
}
tf.recordType(obj.Type())
// Record into KnownEmbeddedAliasFields.
obj, ok := obj.(*types.TypeName)
if !ok || !obj.IsAlias() {
continue
}
vr, _ := tf.info.Defs[name].(*types.Var)
if vr == nil || !vr.Embedded() {
continue
}
vrStr := recordedObjectString(vr)
if vrStr == "" {
continue
}
aliasTypeName := typeName{
PkgPath: obj.Pkg().Path(),
Name: obj.Name(),
}
cachedOutput.KnownEmbeddedAliasFields[vrStr] = aliasTypeName
}
for _, tv := range tf.info.Types {
tf.recordType(tv.Type)
@ -1347,12 +1371,7 @@ func (tf *transformer) recordType(t types.Type) {
}
}
func recordedObjectString(obj types.Object) string {
if !obj.Exported() {
// Unexported names will never be used by other packages,
// so we don't need to bother recording them.
return ""
}
func recordedObjectString(obj types.Object) objectString {
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".
@ -1469,11 +1488,23 @@ func (tf *transformer) transformGo(filename string, file *ast.File) *ast.File {
// handle the alias's TypeName instead of treating it as
// the type the alias points to.
//
// Alternatively, if we don't have an alias, we want to
// Alternatively, if we don't have an alias, we still want to
// use the embedded type, not the field.
if tname := tf.fieldToAlias[vr]; tname != nil {
if !tname.IsAlias() {
panic("fieldToAlias recorded a non-alias TypeName?")
vrStr := recordedObjectString(vr)
aliasTypeName, ok := cachedOutput.KnownEmbeddedAliasFields[vrStr]
if ok {
pkg2 := tf.pkg
if aliasTypeName.PkgPath != pkg2.Path() {
// TODO(mvdan): pull this package from tf.pkg.Imports instead?
var err error
pkg2, err = origImporter.ImportFrom(aliasTypeName.PkgPath, parentWorkDir, 0)
if err != nil {
panic(err)
}
}
tname, ok := pkg2.Scope().Lookup(aliasTypeName.Name).(*types.TypeName)
if !ok || !tname.IsAlias() {
panic(fmt.Sprintf("KnownEmbeddedAliasFields pointed %q to a non-alias", vrStr))
}
obj = tname
} else {
@ -1481,24 +1512,7 @@ func (tf *transformer) transformGo(filename string, file *ast.File) *ast.File {
if named == nil {
return true // unnamed type (probably a basic type, e.g. int)
}
// If the field embeds an alias,
// and the field is declared in a dependency,
// fieldToAlias might not tell us about the alias.
// We lack the *ast.Ident for the field declaration,
// so we can't see it in types.Info.Uses.
//
// Instead, detect such a "foreign alias embed".
// If we embed a final named type,
// but the field name does not match its name,
// then it must have been done via an alias.
// We dig out the alias's TypeName via locateForeignAlias.
if named.Obj().Name() != node.Name {
tname := locateForeignAlias(vr.Pkg().Path(), node.Name)
tf.fieldToAlias[vr] = tname // to reuse it later
obj = tname
} else {
obj = named.Obj()
}
obj = named.Obj()
}
pkg = obj.Pkg()
}
@ -1633,44 +1647,6 @@ func (tf *transformer) transformGo(filename string, file *ast.File) *ast.File {
return astutil.Apply(file, pre, post).(*ast.File)
}
// locateForeignAlias finds the TypeName for an alias by the name aliasName,
// which must be declared in one of the dependencies of dependentImportPath.
func locateForeignAlias(dependentImportPath, aliasName string) *types.TypeName {
var found *types.TypeName
lpkg, err := listPackage(dependentImportPath)
if err != nil {
panic(err) // shouldn't happen
}
for _, importedPath := range lpkg.Imports {
pkg2, err := origImporter.ImportFrom(importedPath, parentWorkDir, 0)
if err != nil {
panic(err)
}
tname, ok := pkg2.Scope().Lookup(aliasName).(*types.TypeName)
if ok && tname.IsAlias() {
if found != nil {
// We assume that the alias is declared exactly
// once in the set of direct imports.
// This might not be the case, e.g. if two
// imports declare the same alias name.
//
// TODO: Think how we could solve that
// efficiently, if it happens in practice.
panic(fmt.Sprintf("found multiple TypeNames for %s", aliasName))
}
found = tname
}
}
if found == nil {
// This should never happen.
// If package A embeds an alias declared in a dependency,
// it must show up in the form of "B.Alias",
// so A must import B and B must declare "Alias".
panic(fmt.Sprintf("could not find TypeName for %s", aliasName))
}
return found
}
// recordIgnore adds any named types (including fields) under typ to
// cannotObfuscateNames.
//

@ -144,6 +144,8 @@ type listedPackage struct {
// between garble processes. Use "Garble" as a prefix to ensure no
// collisions with the JSON fields from 'go list'.
// TODO(mvdan): consider filling this iff ToObfuscate==true,
// which will help ensure we don't obfuscate any of their names otherwise.
GarbleActionID []byte
ToObfuscate bool

@ -134,6 +134,12 @@ func main() {
ExternalForeignAlias: nil,
Reader: nil,
}
var emb sub.EmbeddingAlias
_ = emb.EmbeddedAlias
_ = emb.Foo
_ = emb.EmbeddedAliasSameName
_ = emb.Bar
}
-- scopes.go --
@ -198,13 +204,12 @@ func TestBar(*struct{}) {}
// If we obfuscate the alias name, we must obfuscate its use here too.
type EmbeddingAlias struct {
EmbeddedAlias
EmbeddedAliasSameName
}
type EmbeddedAlias = EmbeddedStruct
type EmbeddedAlias = external.NamedExternal
type EmbeddedStruct struct {
Foo int
}
type EmbeddedAliasSameName = external.EmbeddedAliasSameName
// We obfuscate the name foreignAlias, but not the name Reader,
// as it's not declared in a private package.
@ -247,6 +252,14 @@ package external
import "io"
type NamedExternal struct {
Foo int
}
type EmbeddedAliasSameName struct {
Bar int
}
type ExternalForeignAlias = io.Reader
type Reader = io.Reader

Loading…
Cancel
Save