tinygo icon indicating copy to clipboard operation
tinygo copied to clipboard

compiler crash on interface assert

Open awmorgan opened this issue 1 year ago • 4 comments

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)*$ 

awmorgan avatar Jun 12 '23 15:06 awmorgan

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) 

awmorgan avatar Jun 12 '23 19:06 awmorgan

/cc @aykevl

dgryski avatar Jun 14 '23 17:06 dgryski

@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) 

awmorgan avatar Jun 18 '23 15:06 awmorgan

hit this issue as well.

dapchen avatar Nov 02 '23 04:11 dapchen

Completed with v0.31.0 so now closing. Thank you!

deadprogram avatar Feb 28 '24 07:02 deadprogram

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?

marco6 avatar Mar 05 '24 14:03 marco6