tinygo
tinygo copied to clipboard
compiler crash on interface assert
I originally noticed this issue while investigating another issue: https://github.com/tinygo-org/tinygo/issues/3771#issuecomment-1585869068
this is a small reproducer:
/Volumes/git/tinygo/x (dev)*$ uname -v
Darwin Kernel Version 20.6.0: Thu Mar 9 20:39:26 PST 2023; root:xnu-7195.141.49.700.6~1/RELEASE_X86_64
/Volumes/git/tinygo/x (dev)*$
/Volumes/git/tinygo/x (dev)*$ go version
go version go1.20.5 darwin/amd64
/Volumes/git/tinygo/x (dev)*$
/Volumes/git/tinygo/x (dev)*$ tinygo version
tinygo version 0.28.1 darwin/amd64 (using go version go1.20.5 and LLVM version 15.0.7)
/Volumes/git/tinygo/x (dev)*$
/Volumes/git/tinygo/x (dev)*$ cat main.go
package main
func main() {
// _ = any(0).(interface{ x() }) // this is ok
}
func init() {
_ = any(0).(interface{ x() }) // this crashes the compiler
}
/Volumes/git/tinygo/x (dev)*$ tinygo build .
panic: interp: offset out of range
goroutine 66 [running]:
github.com/tinygo-org/tinygo/interp.pointerValue.addOffset(...)
/Volumes/git/tinygo/interp/memory.go:523
github.com/tinygo-org/tinygo/interp.(*runner).run(0xc0010c0b40, 0xc000f75db0, {0xc00072f110, 0x1, 0x0?}, 0xc00111ba10, {0xc0007160d0, 0x8})
/Volumes/git/tinygo/interp/interpreter.go:413 +0x78c8
github.com/tinygo-org/tinygo/interp.(*runner).run(0xc0010c0b40, 0xc000f75d60, {0x0, 0x0, 0x10001ba50?}, 0x0, {0x10041ef36, 0x4})
/Volumes/git/tinygo/interp/interpreter.go:512 +0x7d45
github.com/tinygo-org/tinygo/interp.RunFunc({0xc0001ea120?}, 0xc001503ce0?, 0x40?)
/Volumes/git/tinygo/interp/interp.go:238 +0x3a5
github.com/tinygo-org/tinygo/builder.Build.func3(0xc001115560)
/Volumes/git/tinygo/builder/build.go:437 +0xcf9
github.com/tinygo-org/tinygo/builder.runJob(0xc001115560, 0x0?)
/Volumes/git/tinygo/builder/jobs.go:222 +0x4f
created by github.com/tinygo-org/tinygo/builder.runJobs
/Volumes/git/tinygo/builder/jobs.go:123 +0x5be
/Volumes/git/tinygo/x (dev)*$
The out of range panic is because addOffset
is being passed a -8 from -int64(r.pointerSize)
case strings.HasSuffix(callFn.name, ".$typeassert"):
if r.debug {
fmt.Fprintln(os.Stderr, indent+"interface assert:", operands[1:])
}
// Load various values for the interface implements check below.
typecodePtr, err := operands[1].asPointer(r)
if err != nil {
return nil, mem, r.errorAt(inst, err)
}
methodSetPtr, err := mem.load(typecodePtr.addOffset(-int64(r.pointerSize)), r.pointerSize).asPointer(r)
if err != nil {
return nil, mem, r.errorAt(inst, err)
}
here is some info from debugging this:
/Volumes/git/tinygo/x (dev)*$ dlv exec ../tinygo -- build .
Type 'help' for list of commands.
(dlv) c
> [unrecovered-panic] runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1145 (hits goroutine(23):1 total:1) (PC: 0x100040400)
Warning: debugging optimized function
runtime.curg._panic.arg: interface {}(string) "interp: offset out of range"
1140: // fatalpanic implements an unrecoverable panic. It is like fatalthrow, except
1141: // that if msgs != nil, fatalpanic also prints panic messages and decrements
1142: // runningPanicDefers once main is blocked from exiting.
1143: //
1144: //go:nosplit
=>1145: func fatalpanic(msgs *_panic) {
1146: pc := getcallerpc()
1147: sp := getcallersp()
1148: gp := getg()
1149: var docrash bool
1150: // Switch to the system stack to avoid any stack growth, which
(dlv) bt
0 0x0000000100040400 in runtime.fatalpanic
at /usr/local/go/src/runtime/panic.go:1145
1 0x000000010003fb8c in runtime.gopanic
at /usr/local/go/src/runtime/panic.go:987
2 0x000000010049f20f in github.com/tinygo-org/tinygo/interp.pointerValue.addOffset
at /Volumes/git/tinygo/interp/memory.go:523
3 0x00000001004940d1 in github.com/tinygo-org/tinygo/interp.(*runner).run
at /Volumes/git/tinygo/interp/interpreter.go:413
4 0x000000010049602e in github.com/tinygo-org/tinygo/interp.(*runner).run
at /Volumes/git/tinygo/interp/interpreter.go:512
5 0x000000010048a45b in github.com/tinygo-org/tinygo/interp.RunFunc
at /Volumes/git/tinygo/interp/interp.go:238
6 0x00000001004ed9fb in github.com/tinygo-org/tinygo/builder.Build.func3
at /Volumes/git/tinygo/builder/build.go:437
7 0x0000000100500b3b in github.com/tinygo-org/tinygo/builder.runJob
at /Volumes/git/tinygo/builder/jobs.go:222
8 0x0000000100500199 in github.com/tinygo-org/tinygo/builder.runJobs.func2
at /Volumes/git/tinygo/builder/jobs.go:123
9 0x0000000100074be1 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1598
(dlv) frame 2
> [unrecovered-panic] runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1145 (hits goroutine(23):1 total:1) (PC: 0x100040400)
Warning: debugging optimized function
Frame 2: /Volumes/git/tinygo/interp/memory.go:523 (PC: 10049f20f)
518: // offset to the pointer. It also checks that the offset doesn't overflow the
519: // maximum offset size (which is 4GB).
520: func (v pointerValue) addOffset(offset int64) pointerValue {
521: result := pointerValue{v.pointer + uint64(offset)}
522: if checks && v.index() != result.index() {
=> 523: panic("interp: offset out of range")
524: }
525: return result
526: }
527:
528: func (v pointerValue) len(r *runner) uint32 {
(dlv) locals
result = github.com/tinygo-org/tinygo/interp.pointerValue {pointer: 12884901880}
(dlv) p offset
-8
(dlv) up
> [unrecovered-panic] runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1145 (hits goroutine(23):1 total:1) (PC: 0x100040400)
Warning: debugging optimized function
Frame 3: /Volumes/git/tinygo/interp/interpreter.go:413 (PC: 1004940d1)
408: // Load various values for the interface implements check below.
409: typecodePtr, err := operands[1].asPointer(r)
410: if err != nil {
411: return nil, mem, r.errorAt(inst, err)
412: }
=> 413: methodSetPtr, err := mem.load(typecodePtr.addOffset(-int64(r.pointerSize)), r.pointerSize).asPointer(r)
414: if err != nil {
415: return nil, mem, r.errorAt(inst, err)
416: }
417: methodSet := mem.get(methodSetPtr.index()).llvmGlobal.Initializer()
418: numMethods := int(r.builder.CreateExtractValue(methodSet, 0, "").ZExtValue())
(dlv) locals
mem = github.com/tinygo-org/tinygo/interp.memoryView {r: ("*github.com/tinygo-org/tinygo/interp.runner")(0xc00063e3c0), parent: ("*github.com/tinygo-org/tinygo/interp.memoryView")(0xc00063ac00), objects: map[uint32]github.com/tinygo-org/tinygo/interp.object nil,...+1 more}
locals = []github.com/tinygo-org/tinygo/interp.value len: 7, cap: 7, [...]
runtimeBlocks = map[int]struct {} nil
bb = ("*github.com/tinygo-org/tinygo/interp.basicBlock")(0xc00063ac90)
currentBB = 0
lastBB = -1
operands = []github.com/tinygo-org/tinygo/interp.value len: 2, cap: 2, [...]
startRTInsts = 0
instIndex = 0
inst = github.com/tinygo-org/tinygo/interp.instruction {opcode: Call (45), localIndex: 1, operands: []github.com/tinygo-org/tinygo/interp.value len: 2, cap: 2, [...],...+2 more}
isRuntimeInst = false
(err) = error nil
fnPtr = github.com/tinygo-org/tinygo/interp.pointerValue {pointer: 8589934592}
callFn = ("*github.com/tinygo-org/tinygo/interp.function")(0xc000654230)
err = error nil
typecodePtr = github.com/tinygo-org/tinygo/interp.pointerValue {pointer: 12884901888}
(dlv)
/cc @aykevl
@dgryski @aykevl I did some more investigation on this issue. It appears that the issue is caused by the type not having a method set.
package main
func main() {
}
type myint int
func init() {
// _ = any(0).(any) // compiler crashes
_ = any(myint(0)).(any) // compiler crashes if x() is commented below, but ok if x() is uncommented below
}
// func (i myint) x() {}
The panicking operation is typecodePtr.addOffset(-int64(r.pointerSize))
in this code:
case strings.HasSuffix(callFn.name, ".$typeassert"):
if r.debug {
fmt.Fprintln(os.Stderr, indent+"interface assert:", operands[1:])
}
// Load various values for the interface implements check below.
typecodePtr, err := operands[1].asPointer(r)
if err != nil {
return nil, mem, r.errorAt(inst, err)
}
methodSetPtr, err := mem.load(typecodePtr.addOffset(-int64(r.pointerSize)), r.pointerSize).asPointer(r)
if err != nil {
return nil, mem, r.errorAt(inst, err)
}
methodSet := mem.get(methodSetPtr.index()).llvmGlobal.Initializer()
I think the fix is to check if the type has a method set before trying to get the pointer to the method set that doesn't exist, but I don't know how to do this.
How can I check for a valid method set in this code?
I noticed that methods is an empty string after calling getMethodsString here:
> github.com/tinygo-org/tinygo/compiler.(*compilerContext).getInterfaceImplementsFunc() /Volumes/git/tinygo/compiler/interface.go:775 (PC: 0x1004680cf)
770: if llvmFn.IsNil() {
771: llvmFnType := llvm.FunctionType(c.ctx.Int1Type(), []llvm.Type{c.i8ptrType}, false)
772: llvmFn = llvm.AddFunction(c.mod, fnName, llvmFnType)
773: c.addStandardDeclaredAttributes(llvmFn)
774: methods := c.getMethodsString(assertedType.Underlying().(*types.Interface))
=> 775: llvmFn.AddFunctionAttr(c.ctx.CreateStringAttribute("tinygo-methods", methods))
776: }
777: return llvmFn
778: }
779:
780: // getInvokeFunction returns the thunk to call the given interface method. The
(dlv) p methods
""
(dlv)
hit this issue as well.
Completed with v0.31.0
so now closing. Thank you!
Hi @deadprogram! I think it actually wasn't fixed as the original bug is still there in version 0.31.0. Given a file containing:
// main.go
package main
func init() {
_ = any(0).(interface{ x() }) // this crashes the compiler
}
func main() {}
Running you get:
$ tinygo version
tinygo version 0.31.1 linux/amd64 (using go version go1.21.1 and LLVM version 17.0.1)
$ tinygo build .
# main
<path>/main.go:6:13: interp: offset -8 out of range for object <3>
%0 = call i1 @"interface:{main.x:func:{}{}}.$typeassert"(ptr @"reflect/types.type:basic:int"), !dbg !26
traceback:
<path>/main.go:6:13:
%0 = call i1 @"interface:{main.x:func:{}{}}.$typeassert"(ptr @"reflect/types.type:basic:int"), !dbg !26
<path>:
call void @"main.init#1"(ptr undef), !dbg !26
Right now, only the case of:
_ = any(0).(any) // this crashes the compiler
got fixed. Can we reopen this issue?
Last, I opened some time ago a fix to this issue. Could you take a look?