Files
tinygo/src/internal/task/task_asyncify.go
T
Jake Bailey 16fc1ea2bb runtime: make fatal failures unrecoverable
Match the Go runtime by terminating for deadlocks, stack overflows,
runtime and GC invariants, invalid lock operations, and platform
initialization failures instead of routing them through panic/recover.

Keep language-level runtime errors and unsupported user operations
recoverable. Add crash coverage that verifies fatal errors bypass deferred
recover calls.
2026-07-25 07:14:00 +02:00

139 lines
4.1 KiB
Go

//go:build scheduler.asyncify
package task
import (
"unsafe"
)
// Stack canary, to detect a stack overflow. The number is a random number
// generated by random.org. The bit fiddling dance is necessary because
// otherwise Go wouldn't allow the cast to a smaller integer size.
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
// state is a structure which holds a reference to the state of the task.
// When the task is suspended, the stack pointers are saved here.
type state struct {
// entry is the entry function of the task.
// This is needed every time the function is invoked so that asyncify knows what to rewind.
entry uintptr
// args are a pointer to a struct holding the arguments of the function.
args unsafe.Pointer
// stackState is the state of the stack while unwound.
stackState
launched bool
}
// stackState is the saved state of a stack while unwound.
// The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two.
type stackState struct {
// asyncify is the stack pointer of the asyncify stack.
// This starts from the bottom and grows upwards.
asyncifysp unsafe.Pointer
// asyncify is stack pointer of the C stack.
// This starts from the top and grows downwards.
csp unsafe.Pointer
// Pointer to the first (lowest address) of the stack. It must never be
// overwritten. It can be checked from time to time to see whether a stack
// overflow happened in the past.
canaryPtr *uintptr
}
// start creates and starts a new goroutine with the given function and arguments.
// The new goroutine is immediately started.
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
t := &Task{}
addLiveTask(t)
t.state.initialize(fn, args, stackSize)
scheduleTask(t)
}
//export tinygo_launch
func (*state) launch()
//go:linkname align runtime.align
func align(p uintptr) uintptr
// initialize the state and prepare to call the specified function with the specified argument bundle.
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
// Save the entry call.
s.entry = fn
s.args = args
// Create a stack.
stack := runtime_alloc(stackSize, nil)
// Set up the stack canary, a random number that should be checked when
// switching from the task back to the scheduler. The stack canary pointer
// points to the first word of the stack. If it has changed between now and
// the next stack switch, there was a stack overflow.
s.canaryPtr = (*uintptr)(stack)
*s.canaryPtr = stackCanary
// Calculate stack base addresses.
s.asyncifysp = unsafe.Add(stack, unsafe.Sizeof(uintptr(0)))
s.csp = unsafe.Add(stack, stackSize)
}
// currentTask is the current running task, or nil if currently in the scheduler.
var currentTask *Task
// Current returns the current active task.
func Current() *Task {
return currentTask
}
// Pause suspends the current task and returns to the scheduler.
// This function may only be called when running on a goroutine stack, not when running on the system stack.
func Pause() {
if *currentTask.state.canaryPtr != stackCanary {
runtimeFatal("stack overflow")
}
currentTask.state.unwind()
}
//export tinygo_unwind
func (*stackState) unwind()
// Resume the task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
// The current task must be saved and restored because this can nest on WASM with JS.
prevTask := currentTask
if prevTask == nil {
// Save the system stack pointer.
saveStackPointer()
}
t.gcData.swap()
currentTask = t
if !t.state.launched {
t.state.launch()
t.state.launched = true
} else {
t.state.rewind()
}
currentTask = prevTask
t.gcData.swap()
if uintptr(t.state.asyncifysp) > uintptr(t.state.csp) {
runtimeFatal("stack overflow")
}
}
//go:linkname saveStackPointer runtime.saveStackPointer
func saveStackPointer()
//export tinygo_rewind
func (*state) rewind()
// OnSystemStack returns whether the caller is running on the system stack.
func OnSystemStack() bool {
// If there is not an active goroutine, then this must be running on the system stack.
return Current() == nil
}