From b4f4c080d69486f5894e2865f7b23c32b4c80b6d Mon Sep 17 00:00:00 2001 From: Eldo Loguzzo Date: Wed, 7 Oct 2026 15:15:40 -0600 Subject: [PATCH] Issue #545 : yield across pcall or a Go call boundary raises the Lua 5.1 error A coroutine that yielded from a Lua function called by Go code (pcall, xpcall, table.sort, any Go function calling Lua) unwound the nested main loop of that Go call, and the next resume dereferenced a nil frame. LState.nCcalls counts the Go-to-Lua calls (callR) in progress on the thread; PCall restores it when it recovers an error. A yield with a nested call in progress raises "attempt to yield across metamethod/C-call boundary", which the enclosing pcall catches, as in Lua 5.1. Co-Authored-By: Claude Opus 5.5 (1M context) --- _glua-tests/coroutine.lua | 27 +++++++++++++++++++++++++++ _state.go | 5 +++++ _vm.go | 5 +++++ state.go | 5 +++++ value.go | 1 + vm.go | 5 +++++ 6 files changed, 48 insertions(+) diff --git a/_glua-tests/coroutine.lua b/_glua-tests/coroutine.lua index fd6ee73b..98663bd4 100644 --- a/_glua-tests/coroutine.lua +++ b/_glua-tests/coroutine.lua @@ -15,3 +15,30 @@ local ok, msg = pcall(function() co() end) assert(not ok and string.find(msg, "can not resume a dead thread")) + +-- yield across a Go call boundary (pcall, a Go function calling Lua) is an +-- error, as in Lua 5.1; it used to crash with a nil pointer dereference (#545) +local co = coroutine.create(function() + local ok, err = pcall(function() coroutine.yield(1) end) + return ok, err +end) +local ok, a, b = coroutine.resume(co) +assert(ok and a == false and string.find(b, "attempt to yield across metamethod/C-call boundary", 1, true)) +assert(coroutine.status(co) == "dead") + +co = coroutine.create(function() + return pcall(table.sort, {3, 1, 2}, function(x, y) coroutine.yield(); return x < y end) +end) +ok, a, b = coroutine.resume(co) +assert(ok and a == false and string.find(b, "attempt to yield across", 1, true)) + +co = coroutine.create(function() + pcall(function() coroutine.yield() end) + coroutine.yield("again") + return "done" +end) +assert(select(2, coroutine.resume(co)) == "again") +assert(select(2, coroutine.resume(co)) == "done") + +local gen = coroutine.wrap(function() coroutine.yield(1); return 2 end) +assert(gen() == 1 and gen() == 2) diff --git a/_state.go b/_state.go index 6e9febfb..e2006ae6 100644 --- a/_state.go +++ b/_state.go @@ -1048,6 +1048,7 @@ func (ls *LState) callR(nargs, nret, rbase int) { Parent: ls.currentFrame, TailCall: 0, }, lv, meta) + ls.nCcalls++ if ls.G.MainThread == nil { ls.G.MainThread = ls ls.G.CurrentThread = ls @@ -1055,6 +1056,7 @@ func (ls *LState) callR(nargs, nret, rbase int) { } else { ls.mainLoop(ls, ls.currentFrame) } + ls.nCcalls-- if nret != MultRet { ls.reg.SetTop(rbase + nret) } @@ -1817,6 +1819,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { err = nil sp := ls.stack.Sp() base := ls.reg.Top() - nargs - 1 + nccalls := ls.nCcalls oldpanic := ls.Panic ls.Panic = panicWithoutTraceback if errfunc != nil { @@ -1827,6 +1830,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { ls.hasErrorFunc = false rcv := recover() if rcv != nil { + ls.nCcalls = nccalls if _, ok := rcv.(*ApiError); !ok { err = newApiErrorS(ApiErrorPanic, fmt.Sprint(rcv)) if ls.Options.IncludeGoStackTrace { @@ -1856,6 +1860,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { err = rcv.(*ApiError) err.(*ApiError).StackTrace = ls.stackTrace(0) } + ls.nCcalls = nccalls ls.stack.SetSp(sp) ls.currentFrame = ls.stack.Last() ls.reg.SetTop(base) diff --git a/_vm.go b/_vm.go index ee2be040..835e8cd1 100644 --- a/_vm.go +++ b/_vm.go @@ -110,6 +110,11 @@ func callGFunction(L *LState, tailcall bool) bool { } if gfnret < 0 { + if L.nCcalls > 0 && L.Parent != nil { + // The yield would unwind a Go frame (pcall, a metamethod, a Go + // function calling Lua) that cannot be resumed: Lua 5.1 behavior. + L.RaiseError("attempt to yield across metamethod/C-call boundary") + } switchToParentThread(L, L.GetTop(), false, false) return true } diff --git a/state.go b/state.go index 292f93b4..3e3cbf55 100644 --- a/state.go +++ b/state.go @@ -1261,6 +1261,7 @@ func (ls *LState) callR(nargs, nret, rbase int) { Parent: ls.currentFrame, TailCall: 0, }, lv, meta) + ls.nCcalls++ if ls.G.MainThread == nil { ls.G.MainThread = ls ls.G.CurrentThread = ls @@ -1268,6 +1269,7 @@ func (ls *LState) callR(nargs, nret, rbase int) { } else { ls.mainLoop(ls, ls.currentFrame) } + ls.nCcalls-- if nret != MultRet { ls.reg.SetTop(rbase + nret) } @@ -2030,6 +2032,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { err = nil sp := ls.stack.Sp() base := ls.reg.Top() - nargs - 1 + nccalls := ls.nCcalls oldpanic := ls.Panic ls.Panic = panicWithoutTraceback if errfunc != nil { @@ -2040,6 +2043,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { ls.hasErrorFunc = false rcv := recover() if rcv != nil { + ls.nCcalls = nccalls if _, ok := rcv.(*ApiError); !ok { err = newApiErrorS(ApiErrorPanic, fmt.Sprint(rcv)) if ls.Options.IncludeGoStackTrace { @@ -2069,6 +2073,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { err = rcv.(*ApiError) err.(*ApiError).StackTrace = ls.stackTrace(0) } + ls.nCcalls = nccalls ls.stack.SetSp(sp) ls.currentFrame = ls.stack.Last() ls.reg.SetTop(base) diff --git a/value.go b/value.go index 4156e9d5..56b5a84e 100644 --- a/value.go +++ b/value.go @@ -192,6 +192,7 @@ type LState struct { wrapped bool uvcache *Upvalue hasErrorFunc bool + nCcalls int // Go-to-Lua calls (callR) in progress on this thread mainLoop func(*LState, *callFrame) ctx context.Context ctxCancelFn context.CancelFunc diff --git a/vm.go b/vm.go index 97335a75..fc82e182 100644 --- a/vm.go +++ b/vm.go @@ -205,6 +205,11 @@ func callGFunction(L *LState, tailcall bool) bool { } if gfnret < 0 { + if L.nCcalls > 0 && L.Parent != nil { + // The yield would unwind a Go frame (pcall, a metamethod, a Go + // function calling Lua) that cannot be resumed: Lua 5.1 behavior. + L.RaiseError("attempt to yield across metamethod/C-call boundary") + } switchToParentThread(L, L.GetTop(), false, false) return true }