Skip to content

compiler, runtime: implement recoverable divide-by-zero panic #4758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ TEST_PACKAGES_FAST = \
# debug/plan9obj requires os.ReadAt, which is not yet supported on windows
# image requires recover(), which is not yet supported on wasi
# io/ioutil requires os.ReadDir, which is not yet supported on windows or wasi
# math/bits: needs panic()/recover()
# mime: fail on wasi; neds panic()/recover()
# mime/multipart: needs wasip1 syscall.FDFLAG_NONBLOCK
# mime/quotedprintable requires syscall.Faccessat
Expand All @@ -382,8 +383,11 @@ TEST_PACKAGES_LINUX := \
crypto/hmac \
debug/dwarf \
debug/plan9obj \
encoding/binary \
go/constant \
image \
io/ioutil \
math/bits \
mime \
mime/multipart \
mime/quotedprintable \
Expand All @@ -403,6 +407,9 @@ TEST_PACKAGES_WINDOWS := \
compress/flate \
crypto/des \
crypto/hmac \
encoding/binary \
go/constant \
math/bits \
strconv \
text/template/parse \
$(nil)
Expand Down
22 changes: 11 additions & 11 deletions compiler/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (b *builder) createLookupBoundsCheck(arrayLen, index llvm.Value) {

// Now do the bounds check: index >= arrayLen
outOfBounds := b.CreateICmp(llvm.IntUGE, index, arrayLen, "")
b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic")
b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic", true)
}

// createSliceBoundsCheck emits a bounds check before a slicing operation to make
Expand Down Expand Up @@ -74,7 +74,7 @@ func (b *builder) createSliceBoundsCheck(capacity, low, high, max llvm.Value, lo
outOfBounds3 := b.CreateICmp(llvm.IntUGT, max, capacity, "slice.maxcap")
outOfBounds := b.CreateOr(outOfBounds1, outOfBounds2, "slice.lowmax")
outOfBounds = b.CreateOr(outOfBounds, outOfBounds3, "slice.lowcap")
b.createRuntimeAssert(outOfBounds, "slice", "slicePanic")
b.createRuntimeAssert(outOfBounds, "slice", "slicePanic", false)
}

// createSliceToArrayPointerCheck adds a check for slice-to-array pointer
Expand All @@ -86,7 +86,7 @@ func (b *builder) createSliceToArrayPointerCheck(sliceLen llvm.Value, arrayLen i
// > run-time panic occurs.
arrayLenValue := llvm.ConstInt(b.uintptrType, uint64(arrayLen), false)
isLess := b.CreateICmp(llvm.IntULT, sliceLen, arrayLenValue, "")
b.createRuntimeAssert(isLess, "slicetoarray", "sliceToArrayPointerPanic")
b.createRuntimeAssert(isLess, "slicetoarray", "sliceToArrayPointerPanic", false)
}

// createUnsafeSliceStringCheck inserts a runtime check used for unsafe.Slice
Expand Down Expand Up @@ -118,7 +118,7 @@ func (b *builder) createUnsafeSliceStringCheck(name string, ptr, len llvm.Value,
lenIsNotZero := b.CreateICmp(llvm.IntNE, len, zero, "")
assert := b.CreateAnd(ptrIsNil, lenIsNotZero, "")
assert = b.CreateOr(assert, lenOutOfBounds, "")
b.createRuntimeAssert(assert, name, "unsafeSlicePanic")
b.createRuntimeAssert(assert, name, "unsafeSlicePanic", false)
}

// createChanBoundsCheck creates a bounds check before creating a new channel to
Expand Down Expand Up @@ -155,7 +155,7 @@ func (b *builder) createChanBoundsCheck(elementSize uint64, bufSize llvm.Value,

// Do the check for a too large (or negative) buffer size.
bufSizeTooBig := b.CreateICmp(llvm.IntUGE, bufSize, maxBufSize, "")
b.createRuntimeAssert(bufSizeTooBig, "chan", "chanMakePanic")
b.createRuntimeAssert(bufSizeTooBig, "chan", "chanMakePanic", false)
}

// createNilCheck checks whether the given pointer is nil, and panics if it is.
Expand Down Expand Up @@ -199,7 +199,7 @@ func (b *builder) createNilCheck(inst ssa.Value, ptr llvm.Value, blockPrefix str
isnil := b.CreateICmp(llvm.IntEQ, ptr, nilptr, "")

// Emit the nil check in IR.
b.createRuntimeAssert(isnil, blockPrefix, "nilPanic")
b.createRuntimeAssert(isnil, blockPrefix, "nilPanic", false)
}

// createNegativeShiftCheck creates an assertion that panics if the given shift value is negative.
Expand All @@ -212,7 +212,7 @@ func (b *builder) createNegativeShiftCheck(shift llvm.Value) {

// isNegative = shift < 0
isNegative := b.CreateICmp(llvm.IntSLT, shift, llvm.ConstInt(shift.Type(), 0, false), "")
b.createRuntimeAssert(isNegative, "shift", "negativeShiftPanic")
b.createRuntimeAssert(isNegative, "shift", "negativeShiftPanic", false)
}

// createDivideByZeroCheck asserts that y is not zero. If it is, a runtime panic
Expand All @@ -225,12 +225,12 @@ func (b *builder) createDivideByZeroCheck(y llvm.Value) {

// isZero = y == 0
isZero := b.CreateICmp(llvm.IntEQ, y, llvm.ConstInt(y.Type(), 0, false), "")
b.createRuntimeAssert(isZero, "divbyzero", "divideByZeroPanic")
b.createRuntimeAssert(isZero, "divbyzero", "divideByZeroPanic", true)
}

// createRuntimeAssert is a common function to create a new branch on an assert
// bool, calling an assert func if the assert value is true (1).
func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string) {
func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string, isInvoke bool) {
// Check whether we can resolve this check at compile time.
if !assert.IsAConstantInt().IsNil() {
val := assert.ZExtValue()
Expand All @@ -245,17 +245,17 @@ func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc
// current insert position.
faultBlock := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw")
nextBlock := b.insertBasicBlock(blockPrefix + ".next")
b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes

// Now branch to the out-of-bounds or the regular block.
b.CreateCondBr(assert, faultBlock, nextBlock)

// Fail: the assert triggered so panic.
b.SetInsertPointAtEnd(faultBlock)
b.createRuntimeCall(assertFunc, nil, "")
b.createRuntimeCallCommon(assertFunc, nil, "", isInvoke)
b.CreateUnreachable()

// Ok: assert didn't trigger so continue normally.
b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes
b.SetInsertPointAtEnd(nextBlock)
}

Expand Down
9 changes: 7 additions & 2 deletions compiler/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,20 +688,25 @@ func (c *compilerContext) getGlobalInfo(g *ssa.Global) globalInfo {
// Check for //go: pragmas, which may change the link name (among others).
doc := c.astComments[info.linkName]
if doc != nil {
info.parsePragmas(doc)
info.parsePragmas(doc, g)
}
return info
}

// Parse //go: pragma comments from the source. In particular, it parses the
// //go:extern pragma on globals.
func (info *globalInfo) parsePragmas(doc *ast.CommentGroup) {
func (info *globalInfo) parsePragmas(doc *ast.CommentGroup, g *ssa.Global) {
for _, comment := range doc.List {
if !strings.HasPrefix(comment.Text, "//go:") {
continue
}
parts := strings.Fields(comment.Text)
switch parts[0] {
case "//go:linkname":
if len(parts) == 3 && g.Name() == parts[1] {
info.linkName = parts[2]
info.extern = true
}
case "//go:extern":
info.extern = true
if len(parts) == 2 {
Expand Down
6 changes: 6 additions & 0 deletions compiler/testdata/pragma.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package main

import _ "unsafe"

// Use the go:linkname mechanism to link this global to a different package.
// This is used in math/bits.
//
//go:linkname linknamedGlobal runtime.testLinknamedGlobal
var linknamedGlobal int

// Creates an external global with name extern_global.
//
//go:extern extern_global
Expand Down
1 change: 1 addition & 0 deletions compiler/testdata/pragma.ll
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source_filename = "pragma.go"
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-wasi"

@runtime.testLinknamedGlobal = external global i32, align 4
@extern_global = external global [0 x i8], align 1
@main.alignedGlobal = hidden global [4 x i32] zeroinitializer, align 32
@main.alignedGlobal16 = hidden global [4 x i32] zeroinitializer, align 16
Expand Down
18 changes: 18 additions & 0 deletions src/runtime/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,23 @@ package runtime
type Error interface {
error

// Method to indicate this is indeed a runtime error.
RuntimeError()
}

type runtimeError struct {
msg string
}

func (r runtimeError) Error() string {
return r.msg
}

// Purely here to satisfy the Error interface.
func (r runtimeError) RuntimeError() {}

var (
divideError error = runtimeError{"runtime error: integer divide by zero"}
lookupError error = runtimeError{"runtime error: index out of range"}
overflowError error = runtimeError{"runtime error: integer overflow"}
)
4 changes: 2 additions & 2 deletions src/runtime/panic.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func nilMapPanic() {

// Panic when trying to access an array or slice out of bounds.
func lookupPanic() {
runtimePanicAt(returnAddress(0), "index out of range")
_panic(lookupError)
}

// Panic when trying to slice a slice out of bounds.
Expand Down Expand Up @@ -220,7 +220,7 @@ func negativeShiftPanic() {

// Panic when there is a divide by zero.
func divideByZeroPanic() {
runtimePanicAt(returnAddress(0), "divide by zero")
_panic(divideError)
}

func blockingPanic() {
Expand Down
24 changes: 24 additions & 0 deletions testdata/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func main() {
println("\n# defer panic")
deferPanic()

println("\n# runtime panics")
runtimePanicDivByZero(1, 0)
runtimePanicLookup([]int{1, 2, 3}, 10)

println("\n# runtime.Goexit")
runtimeGoexit()
}
Expand Down Expand Up @@ -114,6 +118,26 @@ func deferPanic() {
println("defer panic")
}

func runtimePanicDivByZero(a, b int) int {
defer func() {
if err := recover(); err != nil {
println("recovered:", err)
}
}()

return a / b
}

func runtimePanicLookup(slice []int, index int) int {
defer func() {
if err := recover(); err != nil {
println("recovered:", err)
}
}()

return slice[index]
}

func runtimeGoexit() {
wg.Add(1)
go func() {
Expand Down
Loading