diff --git a/interp/interp_test.go b/interp/interp_test.go index e7e37a76dd..b5dbccdfde 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -20,6 +20,7 @@ func TestInterp(t *testing.T) { "revert", "store", "alloc", + "slicedata", } { name := name // make local to this closure t.Run(name, func(t *testing.T) { diff --git a/interp/memory.go b/interp/memory.go index 3c777d08ef..ff86240fd4 100644 --- a/interp/memory.go +++ b/interp/memory.go @@ -299,7 +299,8 @@ func (mv *memoryView) put(index uint32, obj object) { } // Load the value behind the given pointer. Returns nil if the pointer points to -// an external global. +// an external global or if the load is out of bounds of the object (in which +// case the caller defers the load to runtime). func (mv *memoryView) load(p pointerValue, size uint32) value { if checks && mv.hasExternalStore(p) { panic("interp: load from object with external store") @@ -312,8 +313,13 @@ func (mv *memoryView) load(p pointerValue, size uint32) value { if p.offset() == 0 && size == obj.size { return obj.buffer.clone() } - if checks && p.offset()+size > obj.size { - panic("interp: load out of bounds") + if p.offset()+size > obj.size { + // The load is out of bounds of the object. This can happen for valid + // Go programs, for example when dereferencing the pointer returned by + // unsafe.SliceData on a zero-capacity slice (which points to a + // zero-sized object). Return nil so the caller defers this load to + // runtime instead of crashing the compiler. + return nil } v := obj.buffer.asRawValue(mv.r) loadedBuf := v.buf[p.offset() : p.offset()+size] diff --git a/interp/testdata/slicedata.ll b/interp/testdata/slicedata.ll new file mode 100644 index 0000000000..7bda301f84 --- /dev/null +++ b/interp/testdata/slicedata.ll @@ -0,0 +1,23 @@ +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64--linux" + +; Reproduction of https://github.com/tinygo-org/tinygo/issues/4214. +; Dereferencing the pointer returned by unsafe.SliceData on a zero-capacity +; slice produces a load that is out of bounds of a zero-sized object. The +; interp must defer this load to runtime instead of crashing the compiler. + +@main.zeroSized = global {} zeroinitializer +@main.v = global i64 0 + +define void @runtime.initAll() unnamed_addr { +entry: + call void @main.init(ptr undef) + ret void +} + +define internal void @main.init(ptr %context) unnamed_addr { +entry: + %val = load i64, ptr @main.zeroSized + store i64 %val, ptr @main.v + ret void +} diff --git a/interp/testdata/slicedata.out.ll b/interp/testdata/slicedata.out.ll new file mode 100644 index 0000000000..8a6ccf4f56 --- /dev/null +++ b/interp/testdata/slicedata.out.ll @@ -0,0 +1,12 @@ +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64--linux" + +@main.zeroSized = local_unnamed_addr global {} zeroinitializer +@main.v = local_unnamed_addr global i64 0 + +define void @runtime.initAll() unnamed_addr { +entry: + %val = load i64, ptr @main.zeroSized, align 8 + store i64 %val, ptr @main.v, align 8 + ret void +}