From 264ed0b2a56944d0654a921231bcff64bd42cc10 Mon Sep 17 00:00:00 2001 From: iamrajiv Date: Wed, 10 Jun 2026 03:29:48 +0530 Subject: [PATCH] interp: defer out-of-bounds loads to runtime instead of crashing When the interpreter encountered a load that was out of bounds of the object, it panicked with "interp: load out of bounds", crashing the compiler. 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: package main import "unsafe" var p = unsafe.SliceData([]int{}) var v = *p func main() {} Return nil for an out-of-bounds load, the same as for an external global, so the caller defers the load to runtime instead of crashing. This matches what regular Go does, where the load reads from the runtime zero-base at runtime. Fixes #4214 --- interp/interp_test.go | 1 + interp/memory.go | 12 +++++++++--- interp/testdata/slicedata.ll | 23 +++++++++++++++++++++++ interp/testdata/slicedata.out.ll | 12 ++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 interp/testdata/slicedata.ll create mode 100644 interp/testdata/slicedata.out.ll 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 +}