From 7305cd97a48d752951a35193bedde16a0399058b Mon Sep 17 00:00:00 2001 From: Rodrigo Delduca Date: Thu, 3 Sep 2026 09:40:17 -0300 Subject: [PATCH 1/3] fix(coroutine): include Go stack in recovered panics --- coroutine_panic_stack_test.go | 35 +++++++++++++++++++++++++++++++++++ vm.go | 7 ++++++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 coroutine_panic_stack_test.go diff --git a/coroutine_panic_stack_test.go b/coroutine_panic_stack_test.go new file mode 100644 index 000000000..b46b2254a --- /dev/null +++ b/coroutine_panic_stack_test.go @@ -0,0 +1,35 @@ +package lua + +import ( + "context" + "strings" + "testing" +) + +func TestResumePanicIncludesGoStack(t *testing.T) { + L := NewState(Options{IncludeGoStackTrace: true}) + defer L.Close() + + L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int { + panic("coroutine panic sentinel") + })) + if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil { + t.Fatal(err) + } + + co := L.NewThreadWithContext(context.Background()) + fn := L.GetGlobal("run_panic").(*LFunction) + state, _, err := L.Resume(co, fn) + if state != ResumeError { + t.Fatalf("resume state = %v, want ResumeError", state) + } + if err == nil { + t.Fatal("resume error is nil") + } + if !strings.Contains(err.Error(), "coroutine panic sentinel") { + t.Fatalf("resume error = %q, want panic message", err) + } + if !strings.Contains(err.Error(), "TestResumePanicIncludesGoStack") { + t.Fatalf("resume error = %q, want Go stack", err) + } +} diff --git a/vm.go b/vm.go index 2dbba2dff..36c668f5a 100644 --- a/vm.go +++ b/vm.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "runtime/debug" "strings" "sync" ) @@ -3135,7 +3136,11 @@ func threadRun(L *LState) { SetErrorMetatable(L, e) } } else { - lv = LString(fmt.Sprint(rcv)) + message := fmt.Sprint(rcv) + if L.Options.IncludeGoStackTrace { + message += "\n" + string(debug.Stack()) + } + lv = LString(message) } // Check if there's a protected frame that should catch this error From 172fe50cac62ec59aaf556910f482a81cf23d9ae Mon Sep 17 00:00:00 2001 From: Rodrigo Delduca Date: Thu, 3 Sep 2026 09:44:40 -0300 Subject: [PATCH 2/3] feat(panic): report recovered coroutine panics --- coroutine_panic_stack_test.go | 11 ++++++++++- state.go | 1 + vm.go | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/coroutine_panic_stack_test.go b/coroutine_panic_stack_test.go index b46b2254a..e107a1ad5 100644 --- a/coroutine_panic_stack_test.go +++ b/coroutine_panic_stack_test.go @@ -7,7 +7,13 @@ import ( ) func TestResumePanicIncludesGoStack(t *testing.T) { - L := NewState(Options{IncludeGoStackTrace: true}) + var logged string + L := NewState(Options{ + IncludeGoStackTrace: true, + PanicHandler: func(_ *LState, message string) { + logged = message + }, + }) defer L.Close() L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int { @@ -32,4 +38,7 @@ func TestResumePanicIncludesGoStack(t *testing.T) { if !strings.Contains(err.Error(), "TestResumePanicIncludesGoStack") { t.Fatalf("resume error = %q, want Go stack", err) } + if logged != err.Error() { + t.Fatalf("panic handler message = %q, want returned error", logged) + } } diff --git a/state.go b/state.go index 24370b2dd..50c379b44 100644 --- a/state.go +++ b/state.go @@ -108,6 +108,7 @@ type Options struct { SkipOpenLibs bool // Tells whether a Go stacktrace should be included in a Lua stacktrace when panics occur. IncludeGoStackTrace bool + PanicHandler func(*LState, string) // If `MinimizeStackMemory` is set, the call stack will be automatically grown or shrank up to a limit of // `CallStackSize` in order to minimize memory usage. This does incur a slight performance penalty. MinimizeStackMemory bool diff --git a/vm.go b/vm.go index 36c668f5a..b8e59c330 100644 --- a/vm.go +++ b/vm.go @@ -3140,6 +3140,9 @@ func threadRun(L *LState) { if L.Options.IncludeGoStackTrace { message += "\n" + string(debug.Stack()) } + if L.Options.PanicHandler != nil { + L.Options.PanicHandler(L, message) + } lv = LString(message) } From 4d15e597625b2648a66f7cf8d19ab365f569c410 Mon Sep 17 00:00:00 2001 From: Rodrigo Delduca Date: Thu, 3 Sep 2026 10:06:51 -0300 Subject: [PATCH 3/3] fix(panic): contain reporter failures --- coroutine_panic_stack_test.go | 35 +++++++++++++++++++++++++++++++++++ vm.go | 14 +++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/coroutine_panic_stack_test.go b/coroutine_panic_stack_test.go index e107a1ad5..5f699382d 100644 --- a/coroutine_panic_stack_test.go +++ b/coroutine_panic_stack_test.go @@ -42,3 +42,38 @@ func TestResumePanicIncludesGoStack(t *testing.T) { t.Fatalf("panic handler message = %q, want returned error", logged) } } + +func TestResumePanicHandlerPanicKeepsOriginalError(t *testing.T) { + L := NewState(Options{ + PanicHandler: func(*LState, string) { + panic("panic handler sentinel") + }, + }) + defer L.Close() + + L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int { + panic("coroutine panic sentinel") + })) + if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil { + t.Fatal(err) + } + + co := L.NewThreadWithContext(context.Background()) + fn := L.GetGlobal("run_panic").(*LFunction) + state, _, err := L.Resume(co, fn) + if state != ResumeError { + t.Fatalf("resume state = %v, want ResumeError", state) + } + if err == nil || !strings.Contains(err.Error(), "coroutine panic sentinel") { + t.Fatalf("resume error = %v, want original panic", err) + } + if strings.Contains(err.Error(), "panic handler sentinel") { + t.Fatalf("resume error = %q, want no handler panic", err) + } + if !co.Dead { + t.Fatal("coroutine is not dead") + } + if status := L.Status(co); status != "dead" { + t.Fatalf("coroutine status = %q, want dead", status) + } +} diff --git a/vm.go b/vm.go index b8e59c330..5e9963f1d 100644 --- a/vm.go +++ b/vm.go @@ -3140,9 +3140,7 @@ func threadRun(L *LState) { if L.Options.IncludeGoStackTrace { message += "\n" + string(debug.Stack()) } - if L.Options.PanicHandler != nil { - L.Options.PanicHandler(L, message) - } + reportPanic(L, message) lv = LString(message) } @@ -3171,6 +3169,16 @@ func threadRun(L *LState) { L.mainLoop(L, nil) } +func reportPanic(L *LState, message string) { + if L.Options.PanicHandler == nil { + return + } + defer func() { + _ = recover() + }() + L.Options.PanicHandler(L, message) +} + // handleProtectedError searches for a protected (pcall) frame and handles the error. // Returns true if error was handled, false if it should propagate. func handleProtectedError(L *LState, errValue LValue, _ interface{}) bool {