From cf1912a9f8d89dcbf9aa6bf5ca2ea4eef489cc85 Mon Sep 17 00:00:00 2001 From: PerryLink <255665900+PerryLink@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:17:13 +0000 Subject: [PATCH] fix: use an absolute stack index for the xpcall error handler --- lua.go | 5 ++++- xpcall_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 xpcall_test.go diff --git a/lua.go b/lua.go index 1d1740f..5ffc17a 100644 --- a/lua.go +++ b/lua.go @@ -397,7 +397,10 @@ func (l *State) ProtectedCallWithContinuation(argCount, resultCount, errorFuncti l.checkResults(argCount, resultCount) if errorFunction != 0 { apiCheckStackIndex(errorFunction, l.indexToValue(errorFunction)) - errorFunction = l.AbsIndex(errorFunction) + // The error handler is addressed relative to this frame, but it is + // called later from whichever frame raises the error, so remember its + // absolute stack position. + errorFunction = l.callInfo.function + l.AbsIndex(errorFunction) } f := l.top - (argCount + 1) diff --git a/xpcall_test.go b/xpcall_test.go new file mode 100644 index 0000000..1982a64 --- /dev/null +++ b/xpcall_test.go @@ -0,0 +1,42 @@ +package lua + +import "testing" + +// TestXPCallInvokesErrorHandler checks that xpcall calls the error handler +// passed as its second argument. The handler lives in the caller's frame, so +// the VM has to remember its absolute stack position while the protected +// function runs. +func TestXPCallInvokesErrorHandler(t *testing.T) { + l := NewState() + OpenLibraries(l) + // Put a non-callable value in the first stack slot. A stale frame-relative + // error-handler index resolves to this slot, so the bug surfaces as + // "error in error handling" instead of an endless recursion into the + // outermost chunk. + l.PushNumber(0) + err := DoString(l, ` + local ok, msg = xpcall(function() error("boom") end, + function(m) return "handled: " .. m end) + assert(ok == false, "xpcall reported success for a failing function") + assert(msg:sub(1, 9) == "handled: ", "handler result: " .. tostring(msg)) + assert(msg:sub(-4) == "boom", "handler result: " .. tostring(msg)) + `) + if err != nil { + t.Fatal(err) + } +} + +// TestXPCallReturnsResults checks that a successful xpcall still returns the +// protected function's results and true. +func TestXPCallReturnsResults(t *testing.T) { + l := NewState() + OpenLibraries(l) + err := DoString(l, ` + local ok, a, b = xpcall(function() return 1, 2 end, function() return "unused" end) + assert(ok == true, "xpcall reported failure for a successful function") + assert(a == 1 and b == 2, "results: " .. tostring(a) .. ", " .. tostring(b)) + `) + if err != nil { + t.Fatal(err) + } +}