golang / go

The Go programming language

Home Page:https://go.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

cmd/compile: panic on conversion to anonymous interface when 2 versions of library are built

jirfag opened this issue · comments

What version of Go are you using (go version)?

$ go version
go version go1.11.4 darwin/amd64

Does this issue reproduce with the latest release?

yes it does

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/denis/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/denis/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/nl/54f5x38s4m53mkzzj92zsj340000gn/T/go-build581161643=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

Run

git clone https://github.com/golangci/test4.git
go run main.go

and got

panic: interface conversion: *ssa.Call is not interface { ssa.setNum(int) }: missing method setNum

goroutine 1 [running]:
github.com/golangci/test2/ssa.Panics(0x105cde0, 0xc000064020, 0x2)
        /Users/denis/go/pkg/mod/github.com/golangci/test2@v0.0.0-20190108121323-b241e05eddb2/ssa/func.go:6 +0x82
main.main()
        /Users/denis/go/src/github.com/golangci/test4/main.go:14 +0xa0
exit status 2

What did you expect to see?

No panic with anonymous interface cast.

What did you see instead?

Panic.

Unless I'm missing something, this is a run-time panic, not a compiler panic. Any reason you assigned this to cmd/compile?

Also, were you able to reproduce this with a smaller package or Go program?

sorry, suggest more appropriate component, please. I set cmd/compile because it looks like a compiler bug and #24547 had such component.

I will try to reproduce it with a smaller program.

I could reproduce it with much smaller program

sorry, suggest more appropriate component, please.

Oh, I don't know if the compiler has a bug here - certainly possible, but I was wondering if you had dug up the cause.

If you can reproduce the crash with a smaller self-contained program, please share it. That will make debugging the problem faster.

I have already changed the description of the issue with the smaller test case:

git clone https://github.com/golangci/test4.git
go run main.go

Sorry, but that repo is a 404. Can you just share the file on play.golang.org?

sorry, it was private, I made the repo a public. There are a few packages needed, I can't share it on play.golang.org

It does seem like something is very wrong. I've been able to minimize your test case to a single module with about half the amount of code, but still involving two packages with very similar types and the same package names.

$ git clone https://github.com/mvdan/go-issue-29612
$ cd go-issue-29612
$ go build
$ ./foo
Works succeeded
panic: interface conversion: *ssa.T is not interface { ssa.foo() }: missing method foo

goroutine 1 [running]:
test.tld/foo/p2/ssa.Panics(0x45ecc0, 0x4dce30)
        /home/mvdan/foo/p2/ssa/ssa.go:21 +0x52
main.main()
        /home/mvdan/foo/main.go:16 +0x88
$ # remove ssa1 from main.go; see the comment there
$ go build
$ ./foo
Works succeeded
Panics succeeded

This weird behavior happens on both 1.11.4 and go version devel +73fb3c38a6 Mon Jan 7 14:13:33 2019 +0000 linux/amd64. My best guess without any digging is that either the compiler or the runtime get confused by the very similar types in the very similar pakages, and there's a conflict or misplacement somewhere.

The fact that removing the first ssa package from main.go makes the second ssa package work fine tells me that there's no bug in either of the ssa packages.

/cc @randall77 @ianlancetaylor @dr2chase for some input

Broken in 1.11 too, and we're late in the cycle, so I'm leaving this for 1.13 for now. Perhaps it's reconsidered to be critical enough (or simple enough to fix) to include in 1.12.

Interesting. This has been broken since at least Go 1.1. It works with gccgo.

@mvdan it's a runtime bug.

The interface type interface { ssa.foo() } both appears in test.tld/foo/p1 and test.tld/foo/p2. Why doing (*itab).init(), with type interface { ssa.foo() } of test.tld/foo/p2, the code get method foo from test.tld/foo/p1:

name := inter.typ.nameOff(i.name)
iname := name.name()
ipkg := name.pkgPath()
if ipkg == "" {
	ipkg = inter.pkgpath.name()
}

Here name.pkgPath() returns test.tld/foo/p1. Because the interface methods are shared in *itab, the method foo() we see in test.tld/foo/p2 has package path test.tld/foo/p1.

To confirm that behavior, just changing foo() to Foo() or make p2 import before p1:

~/go/src/test.tld/foo(master ✗) cat main.go 
package main

import (
	ssa2 "test.tld/foo/p2/ssa"

	ssa1 "test.tld/foo/p1/ssa"
)

func main() {
	// Remove the ssa1 import and these two lines to magically fix the program.
	v1 := &ssa1.T{}
	_ = v1

	v2 := &ssa2.T{}
	ssa2.Works(v2)
	println("Works succeeded")
	ssa2.Panics(v2)
	println("Panics succeeded")
}
~/go/src/test.tld/foo(master ✗) go-tip run main.go
Works succeeded
Panics succeeded

I'm trying to fix this bug but don't know how, I'm not too familiar with the runtime source code. One solution I'm thinking about, if the type package path (test.tld/foo/p2/ssa) and the interface package path (test.tld/foo/p1/ssa) have the same suffix (ssa), then the type implements the interface

if tname.isExported() || pkgPath == ipkg {

Any idea or suggestion @ianlancetaylor ?

Change https://golang.org/cl/157397 mentions this issue: runtime: fix same anonymous interface conversion panic

I haven't looked but I'm guessing that the problem is that the two distinct interfaces have the same hash. I think the compiler or the runtime needs to include the full package path in the hash of any interface that contains an unexported method.

@ianlancetaylor I checked and the hashes are different.

The fact is that both test.tld/foo/p1/ssa.interface { foo() } and test.tld/foo/p2/ssa.interface { foo() } appears as interface { ssa.foo() } in symtab, so they're treated as the same in itab.

I checked runtime and sounds like it doesn't turn test.tld/foo/p1/ssa.interface { foo() } to interface { ssa.foo() }, so it should be the compiler. Do you know where?

All the type descriptor information is generated in cmd/compile/internal/gc/reflect.go.

@ianlancetaylor I thought we think it too complicated. Just adding a condition that interface is literal or not will fix that problem.

I updated the CL, please take a look.

I don't think the updated CL is correct.

It sounds like the problem is that there are two interfaces that look the same, but come from different packages, and have an unexported method. Because of the unexported method, the two interfaces should be treated as different, but the itab hash table is treating them as the same interface.

If I have described the problem correctly, then that is what we have to fix: one way or another, the two different interfaces must not hash to the same entry in the itab hash table.

Perhaps the reason that they are same entry in the itab hash table is that they are the same entry in the executable's symbol table. That is, perhaps the linker is merging the two types into the same symbol. If that is the case, then that is what we must fix. We must change the symbol names.

@ianlancetaylor: Maybe I missed something, but they are not the same entry in this case, only the methods are the same.
They have diffirent hash inter.typ.hash. As I understand the code, two methods always treated as the same, if it’s unexported, itab use their package path to differentate them.

if tname.isExported() || pkgPath == ipkg {
. So one/a.fooer.foo and two/a.fooer.foo will have the same symbol.

Also it’s only matter for literal interface, named interface does not affect by this change. But literal interface can not be used outside the packge it was declared. That’s why I think add isLiteral to if condition above is enough.

So one/a.fooer.foo and two/a.fooer.foo will have the same symbol.

Can you clarify what you mean by that? What you mean by "the same symbol"?

@ianlancetaylor I mean in this line

t := &xmhdr[j]

will return the same t for both one/a.fooer.foo and two/a.fooer.foo. Then when the code reaches line 216, it checks the method is exported or in the same package (pkgPath == ipkg).

That's why I said if we add one more check for literal interface, we don't affect named interface, because they're cover by two previous conditions:

if tname.isExported() || pkgPath == ipkg

The condition inter.isLiteral() covers the literal interface case. I added your test case in CL https://go-review.googlesource.com/c/go/+/157397/3/src/cmd/go/go_test.go#6208

Maybe I don't understand the code correctly, but for me, all interface with the same signature is shared, and they differentiate by path if not exported.

The type t set at iface.go:209 is type method. It has a package path fetched from the name field (see the comment beginning with "name is an encoded type name with optional extra data" in reflect/type.go). If package one/a defines an interface with method foo, then that package path should be one/a. And if package two/a defines an interface with method foo, then that package path should be two/a. So if line 209 sets the same t for both one/a.fooer and two/a.fooer, then something must be wrong. What is the package path for that method? It should be either one/a or two/a. It can't be both.

@ianlancetaylor In reflect.Type, the comment lines after name is an encoded type name with optional extra data said that:

// If the import path follows, then 4 bytes at the end of
// the data form a nameOff. The import path is only set for concrete
// methods that are defined in a different package than their type.

So for interface method, it doesn't set, right? That's why the import path can be empty, in that case, import path of type is used.

if pkgPath == "" {

@ianlancetaylor I added some debug lines:

println("1: ", ipkg)
if ipkg == "" {
	ipkg = inter.pkgpath.name()
}
println("2: ", ipkg, i, itype, itype.string(), itype.hash)
for ; j < nt; j++ {
	t := &xmhdr[j]
	tname := typ.nameOff(t.name)
	if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
		pkgPath := tname.pkgPath()
		println("3: ", pkgPath)
		if pkgPath == "" {
			pkgPath = typ.nameOff(x.pkgpath).name()
		}
		println("4: ", pkgPath, t, tname.name(), iname, typ.typeOff(t.mtyp).hash)
                ....

And the result:

1:  
2:  one/a 0x4a08c0 0x4963a0 func() 4135763190
3:  
4:  one/a 0x49de20 m m 4135763190
true
1:  
2:  one/a 0x4a08c0 0x4963a0 func() 4135763190
3:  
4:  two/a 0x49e2a0 m m 4135763190
false
1:  
2:  two/a 0x4a09c0 0x4963a0 func() 4135763190
3:  
4:  one/a 0x49de20 m m 4135763190
false
1:  
2:  two/a 0x4a09c0 0x4963a0 func() 4135763190
3:  
4:  two/a 0x49e2a0 m m 4135763190
true

For the program in the issue:

1:  
2:  test.tld/foo/p2/ssa 0x4600c0 0x45a4c0 func() 4135763190
3:  
4:  test.tld/foo/p2/ssa 0x45ed88 foo foo 4135763190
Works succeeded
1:  test.tld/foo/p1/ssa
2:  test.tld/foo/p1/ssa 0x45ee50 0x45a4c0 func() 4135763190
3:  
4:  test.tld/foo/p2/ssa 0x45ed88 foo foo 4135763190
1:  test.tld/foo/p1/ssa
2:  test.tld/foo/p1/ssa 0x45ee50 0x45a4c0 func() 4135763190
3:  
4:  test.tld/foo/p2/ssa 0x45ed88 foo foo 4135763190
1:  
2:   0x45ffc0 0x45aaa0 func() string 516648354
3:  
4:  runtime 0x45f4c8 Error Error 516648354
panic: interface conversion: *ssa.T is not interface { ssa.foo() }: missing method foo

goroutine 1 [running]:
test.tld/foo/p2/ssa.Panics(0x45ed40, 0x4dce10)
	/home/cuonglm/go/src/test.tld/foo/p2/ssa/ssa.go:38 +0x8e
main.main()
	/home/cuonglm/go/src/test.tld/foo/main.go:15 +0x88

So sounds like 2 interfaces treated the same. With literal interface, the name.pkgPath() is set to the first one seen test.tld/foo/p1/ssa, that's why the same literal interface in other package never seen.

Also, inter.pkgpath.name() seems to be set for all named interface, and empty for all literal interface. If that's true, then we can use it for checking interface literal or not instead of parsing interface name like I did in previous CL update.

So sounds like 2 interfaces treated the same. With literal interface, the name.pkgPath() is set to the first one seen test.tld/foo/p1/ssa, that's why the same literal interface in other package never seen.

Thanks, that seems like you have found the bug there. The code in package p2/ssa should not be seeing an interface type that has a pkgPath of p1/ssa.

@rsc @ianlancetaylor do you have any suggestion to fix this issue?

I added a debug line in

fmt.Printf("XXX: %#v - %#v - %#v\n", t, ot, tpkg)

I got:

go-tip run main.go
# test.tld/foo/p1/sas
XXX: interface { ssa.foo() } - 80 - (*types.Pkg)(nil)
XXX: interface {} - 80 - (*types.Pkg)(nil)
# test.tld/foo/p2/ssa
XXX: fooer - 96 - &types.Pkg{Path:"", Name:"ssa", Prefix:"\"\"", Syms:map[string]*types.Sym{"(*T).foo":(*T).foo, ".fp":.fp, ".i0":.i0, ".s0":.s0, ".s1":.s1, ".sink":.sink, ".this":.this, "Error":Error, "Panics":Panics, "T":T, "T.foo":T.foo, "Works":Works, "_":_, "alignme":alignme, "append":append, "bool":bool, "byte":byte, "cap":cap, "cgo":cgo, "close":close, "complex":complex, "complex128":complex128, "complex64":complex64, "copy":copy, "delete":delete, "enabled":enabled, "error":error, "false":false, "float32":float32, "float64":float64, "foo":foo, "fooer":fooer, "fooer.foo":fooer.foo, "imag":imag, "int":int, "int16":int16, "int32":int32, "int64":int64, "int8":int8, "iota":iota, "len":len, "make":make, "needed":needed, "new":new, "nil":nil, "pad":pad, "panic":panic, "print":print, "println":println, "real":real, "recover":recover, "rune":rune, "string":string, "true":true, "uint":uint, "uint16":uint16, "uint32":uint32, "uint64":uint64, "uint8":uint8, "uintptr":uintptr, "v":v}, Pathsym:(*obj.LSym)(0xc000540c40), Height:0, Imported:false, Direct:false}
YYY: &types.Pkg{Path:"", Name:"ssa", Prefix:"\"\"", Syms:map[string]*types.Sym{"(*T).foo":(*T).foo, ".fp":.fp, ".i0":.i0, ".s0":.s0, ".s1":.s1, ".sink":.sink, ".this":.this, "Error":Error, "Panics":Panics, "T":T, "T.foo":T.foo, "Works":Works, "_":_, "alignme":alignme, "append":append, "bool":bool, "byte":byte, "cap":cap, "cgo":cgo, "close":close, "complex":complex, "complex128":complex128, "complex64":complex64, "copy":copy, "delete":delete, "enabled":enabled, "error":error, "false":false, "float32":float32, "float64":float64, "foo":foo, "fooer":fooer, "fooer.foo":fooer.foo, "imag":imag, "int":int, "int16":int16, "int32":int32, "int64":int64, "int8":int8, "iota":iota, "len":len, "make":make, "needed":needed, "new":new, "nil":nil, "pad":pad, "panic":panic, "print":print, "println":println, "real":real, "recover":recover, "rune":rune, "string":string, "true":true, "uint":uint, "uint16":uint16, "uint32":uint32, "uint64":uint64, "uint8":uint8, "uintptr":uintptr, "v":v}, Pathsym:(*obj.LSym)(0xc000540c40), Height:0, Imported:false, Direct:false}
XXX: interface { ssa.foo() } - 80 - (*types.Pkg)(nil)
XXX: interface {} - 80 - (*types.Pkg)(nil)
Works succeeded
panic: interface conversion: *ssa.T is not interface { ssa.foo() }: missing method foo

goroutine 1 [running]:
test.tld/foo/p2/ssa.Panics(0x45ece0, 0x4dce10)
	/home/cuonglm/go/src/test.tld/foo/p2/ssa/ssa.go:22 +0x8e
main.main()
	/home/cuonglm/go/src/test.tld/foo/main.go:13 +0x7c
exit status 2

What is ot? Why does empty interface and anonymous interface have the same ot?

The ot variable holds an offset into the type descriptor that is being built. It's just used to put values at the right place.

Change https://golang.org/cl/165859 mentions this issue: cmd/compile: use full package path to print unexported interface methods

Change https://golang.org/cl/170157 mentions this issue: cmd/compile: use full package path for unexported interface methods, struct fields

Change https://golang.org/cl/195782 mentions this issue: test: add test coverage for type-switch hash collisions