Skip to content

Commit 66b9f08

Browse files
committed
Fix coroutine stack handling after hook yields
Preserve the coroutine stack when a debug hook yields, so Lua stack is not truncated by `StackGuard`. Fixes #723
1 parent 8eddf85 commit 66b9f08

6 files changed

Lines changed: 177 additions & 38 deletions

File tree

src/state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ impl Lua {
782782
pub fn remove_hook(&self) {
783783
let lua = self.lock();
784784
unsafe {
785-
ffi::lua_sethook(lua.state(), None, 0, 0);
785+
lua.remove_thread_hook(lua.state());
786786
}
787787
}
788788

src/state/extra.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub(crate) struct ExtraData {
8181
pub(super) hook_callback: Option<crate::types::HookCallback>,
8282
#[cfg(not(feature = "luau"))]
8383
pub(super) hook_triggers: crate::debug::HookTriggers,
84+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
85+
pub(super) hook_removed_while_yielded: bool,
8486
#[cfg(any(feature = "lua55", feature = "lua54"))]
8587
pub(super) warn_callback: Option<crate::types::WarnCallback>,
8688
#[cfg(feature = "luau")]
@@ -187,6 +189,8 @@ impl ExtraData {
187189
hook_callback: None,
188190
#[cfg(not(feature = "luau"))]
189191
hook_triggers: Default::default(),
192+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
193+
hook_removed_while_yielded: false,
190194
#[cfg(any(feature = "lua55", feature = "lua54"))]
191195
warn_callback: None,
192196
#[cfg(feature = "luau")]

src/state/raw.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ impl RawLua {
427427
if event == ffi::LUA_HOOKCOUNT || event == ffi::LUA_HOOKLINE {
428428
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
429429
if ffi::lua_isyieldable(state) != 0 {
430+
if ffi::lua_gethook(state).is_none() {
431+
let extra = ExtraData::get(state);
432+
(*extra).hook_removed_while_yielded = true;
433+
}
430434
ffi::lua_yield(state, 0);
431435
}
432436
#[cfg(any(feature = "lua52", feature = "lua51", feature = "luajit"))]
@@ -518,6 +522,41 @@ impl RawLua {
518522
Ok(())
519523
}
520524

525+
#[cfg(not(feature = "luau"))]
526+
#[inline]
527+
pub(crate) unsafe fn remove_thread_hook(&self, thread_state: *mut ffi::lua_State) {
528+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
529+
if ffi::lua_status(thread_state) == ffi::LUA_YIELD && Self::has_hook_yielded_frame(thread_state) {
530+
(*self.extra.get()).hook_removed_while_yielded = true;
531+
}
532+
ffi::lua_sethook(thread_state, None, 0, 0);
533+
}
534+
535+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
536+
unsafe fn has_hook_yielded_frame(thread_state: *mut ffi::lua_State) -> bool {
537+
let mut ar = mem::zeroed::<ffi::lua_Debug>();
538+
ffi::lua_getstack(thread_state, 0, &mut ar) != 0
539+
&& ffi::lua_getinfo(thread_state, cstr!("S"), &mut ar) != 0
540+
&& !ar.what.is_null()
541+
&& CStr::from_ptr(ar.what).to_bytes() != b"C"
542+
}
543+
544+
pub(crate) unsafe fn is_hook_yielded(&self, thread_state: *mut ffi::lua_State) -> bool {
545+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
546+
{
547+
if ffi::lua_gethook(thread_state).is_none() && !(*self.extra.get()).hook_removed_while_yielded {
548+
return false;
549+
}
550+
Self::has_hook_yielded_frame(thread_state)
551+
}
552+
553+
#[cfg(not(any(feature = "lua55", feature = "lua54", feature = "lua53")))]
554+
{
555+
let _ = thread_state;
556+
false
557+
}
558+
}
559+
521560
/// See [`Lua::create_string`]
522561
pub(crate) unsafe fn create_string(&self, s: &[u8]) -> Result<LuaString> {
523562
let state = self.state();

src/thread.rs

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,6 @@ enum ThreadStatusInner {
185185
}
186186

187187
impl ThreadStatusInner {
188-
#[cfg(feature = "async")]
189-
#[inline(always)]
190-
fn is_resumable(self) -> bool {
191-
matches!(self, ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_))
192-
}
193-
194188
#[inline(always)]
195189
fn is_yielded(self) -> bool {
196190
matches!(self, ThreadStatusInner::Yielded(_))
@@ -329,7 +323,7 @@ impl Thread {
329323
{
330324
let lua = self.0.lua.lock();
331325
check_thread_reentrancy(self.state(), &lua)?;
332-
let mut pushed_nargs = self.resumable_nargs(&lua)?;
326+
let (mut pushed_nargs, mut hook_yielded) = self.resumable_state(&lua)?;
333327

334328
let state = lua.state();
335329
let thread_state = self.state();
@@ -341,18 +335,24 @@ impl Thread {
341335
if exec_thread_event(&lua, on_resume, thread_state, || {
342336
ThreadEvent::Resume(self.clone())
343337
})? {
344-
pushed_nargs = self.resumable_nargs(&lua)?;
338+
(pushed_nargs, hook_yielded) = self.resumable_state(&lua)?;
345339
}
346340

347-
let nargs = args.push_into_stack_multi(&lua)?;
348-
if nargs > 0 {
349-
check_stack(thread_state, nargs)?;
350-
ffi::lua_xmove(state, thread_state, nargs);
351-
pushed_nargs += nargs;
341+
if !hook_yielded {
342+
let nargs = args.push_into_stack_multi(&lua)?;
343+
if nargs > 0 {
344+
check_stack(thread_state, nargs)?;
345+
ffi::lua_xmove(state, thread_state, nargs);
346+
pushed_nargs += nargs;
347+
}
352348
}
353349

354-
let _thread_sg = StackGuard::with_top(thread_state, 0);
350+
let mut thread_sg = StackGuard::with_top(thread_state, 0);
355351
let (status, nresults) = self.resume_inner(&lua, pushed_nargs)?;
352+
if status.is_yielded() && self.is_hook_yielded(&lua) {
353+
debug_assert_eq!(nresults, 0);
354+
thread_sg.keep(ffi::lua_gettop(thread_state));
355+
}
356356

357357
check_stack(state, nresults + 1)?;
358358
ffi::lua_xmove(thread_state, state, nresults);
@@ -478,15 +478,24 @@ impl Thread {
478478
}
479479
}
480480

481-
/// Returns the number of pending arguments on the thread stack if the thread is resumable.
481+
/// Returns the pending argument count and whether the thread was interrupted by a hook.
482482
#[inline]
483-
fn resumable_nargs(&self, lua: &RawLua) -> Result<c_int> {
483+
fn resumable_state(&self, lua: &RawLua) -> Result<(c_int, bool)> {
484484
match self.status_inner(lua) {
485-
ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => Ok(nargs),
485+
ThreadStatusInner::New(nargs) => Ok((nargs, false)),
486+
ThreadStatusInner::Yielded(nargs) => {
487+
let hook_yielded = self.is_hook_yielded(lua);
488+
Ok((if hook_yielded { 0 } else { nargs }, hook_yielded))
489+
}
486490
_ => Err(Error::CoroutineUnresumable),
487491
}
488492
}
489493

494+
/// Distinguishes a hook interruption from a normal yield.
495+
fn is_hook_yielded(&self, lua: &RawLua) -> bool {
496+
unsafe { lua.is_hook_yielded(self.state()) }
497+
}
498+
490499
/// Returns `true` if this thread is resumable (meaning it can be resumed by calling
491500
/// [`Thread::resume`]).
492501
#[inline(always)]
@@ -548,9 +557,9 @@ impl Thread {
548557
#[cfg(not(feature = "luau"))]
549558
#[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))]
550559
pub fn remove_hook(&self) {
551-
let _lua = self.0.lua.lock();
560+
let lua = self.0.lua.lock();
552561
unsafe {
553-
ffi::lua_sethook(self.state(), None, 0, 0);
562+
lua.remove_thread_hook(self.state());
554563
}
555564
}
556565

@@ -681,19 +690,19 @@ impl Thread {
681690
{
682691
let lua = self.0.lua.lock();
683692
check_thread_reentrancy(self.state(), &lua)?;
684-
if !self.status_inner(&lua).is_resumable() {
685-
return Err(Error::CoroutineUnresumable);
686-
}
693+
let (_, hook_yielded) = self.resumable_state(&lua)?;
687694

688695
let state = lua.state();
689696
let thread_state = self.state();
690697
unsafe {
691698
let _sg = StackGuard::new(state);
692699

693-
let nargs = args.push_into_stack_multi(&lua)?;
694-
if nargs > 0 {
695-
check_stack(thread_state, nargs)?;
696-
ffi::lua_xmove(state, thread_state, nargs);
700+
if !hook_yielded {
701+
let nargs = args.push_into_stack_multi(&lua)?;
702+
if nargs > 0 {
703+
check_stack(thread_state, nargs)?;
704+
ffi::lua_xmove(state, thread_state, nargs);
705+
}
697706
}
698707

699708
Ok(AsyncThread {
@@ -794,7 +803,7 @@ impl<R> Drop for AsyncThread<R> {
794803
{
795804
unsafe {
796805
let mut status = self.thread.status_inner(&lua);
797-
if matches!(status, ThreadStatusInner::Yielded(0)) {
806+
if matches!(status, ThreadStatusInner::Yielded(0)) && !self.thread.is_hook_yielded(&lua) {
798807
// The thread is dropped while yielded, resume it with the "terminate" signal
799808
ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0);
800809
if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) {
@@ -820,32 +829,37 @@ impl<R: FromLuaMulti> Stream for AsyncThread<R> {
820829
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
821830
let lua = self.thread.0.lua.lock();
822831
check_thread_reentrancy(self.thread.state(), &lua)?;
823-
let mut nargs = match self.thread.resumable_nargs(&lua) {
824-
Ok(nargs) => nargs,
832+
let mut nargs = match self.thread.resumable_state(&lua) {
833+
Ok((nargs, _)) => nargs,
825834
Err(_) => return Poll::Ready(None),
826835
};
827836

828837
let state = lua.state();
829838
let thread_state = self.thread.state();
830839
unsafe {
831840
let _sg = StackGuard::new(state);
832-
let _thread_sg = StackGuard::with_top(thread_state, 0);
841+
let mut thread_sg = StackGuard::with_top(thread_state, 0);
833842
let _wg = WakerGuard::new(&lua, cx.waker());
834843

835844
// If the resume callback runs, it may touch this thread, so re-read the argument count
836845
let on_resume = lua.thread_event_triggers().on_resume;
837846
if exec_thread_event(&lua, on_resume, thread_state, || {
838847
ThreadEvent::Resume(self.thread.clone())
839848
})? {
840-
nargs = match self.thread.resumable_nargs(&lua) {
841-
Ok(nargs) => nargs,
849+
nargs = match self.thread.resumable_state(&lua) {
850+
Ok((nargs, _)) => nargs,
842851
Err(_) => return Poll::Ready(None),
843852
};
844853
}
845854

846855
let (status, nresults) = (self.thread).resume_inner(&lua, nargs)?;
856+
let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua);
857+
if hook_yielded {
858+
debug_assert_eq!(nresults, 0);
859+
thread_sg.keep(ffi::lua_gettop(thread_state));
860+
}
847861

848-
if status.is_yielded() && nresults == 1 && is_poll_pending(thread_state) {
862+
if status.is_yielded() && !hook_yielded && nresults == 1 && is_poll_pending(thread_state) {
849863
// Exec thread yield callback
850864
let on_yield = lua.thread_event_triggers().on_yield;
851865
exec_thread_event(&lua, on_yield, thread_state, || {
@@ -878,27 +892,32 @@ impl<R: FromLuaMulti> Future for AsyncThread<R> {
878892
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
879893
let lua = self.thread.0.lua.lock();
880894
check_thread_reentrancy(self.thread.state(), &lua)?;
881-
let mut nargs = self.thread.resumable_nargs(&lua)?;
895+
let (mut nargs, _) = self.thread.resumable_state(&lua)?;
882896

883897
let state = lua.state();
884898
let thread_state = self.thread.state();
885899
unsafe {
886900
let _sg = StackGuard::new(state);
887-
let _thread_sg = StackGuard::with_top(thread_state, 0);
901+
let mut thread_sg = StackGuard::with_top(thread_state, 0);
888902
let _wg = WakerGuard::new(&lua, cx.waker());
889903

890904
// If the resume callback runs, it may touch this thread, so re-read the argument count
891905
let on_resume = lua.thread_event_triggers().on_resume;
892906
if exec_thread_event(&lua, on_resume, thread_state, || {
893907
ThreadEvent::Resume(self.thread.clone())
894908
})? {
895-
nargs = self.thread.resumable_nargs(&lua)?;
909+
(nargs, _) = self.thread.resumable_state(&lua)?;
896910
}
897911

898912
let (status, nresults) = self.thread.resume_inner(&lua, nargs)?;
913+
let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua);
914+
if hook_yielded {
915+
debug_assert_eq!(nresults, 0);
916+
thread_sg.keep(ffi::lua_gettop(thread_state));
917+
}
899918

900919
if status.is_yielded() {
901-
let pending = nresults == 1 && is_poll_pending(thread_state);
920+
let pending = !hook_yielded && nresults == 1 && is_poll_pending(thread_state);
902921

903922
// Exec thread yield callback
904923
let on_yield = lua.thread_event_triggers().on_yield;

tests/async.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,49 @@ async fn test_async_hook() -> Result<()> {
687687
Ok(())
688688
}
689689

690+
#[tokio::test]
691+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
692+
async fn test_async_hook_yield_preserves_stack() -> Result<()> {
693+
use std::future::{Future, poll_fn};
694+
use std::sync::atomic::{AtomicBool, Ordering};
695+
use std::task::Poll;
696+
697+
let lua = Lua::new();
698+
699+
let thread = lua.create_thread(
700+
lua.load(
701+
r#"
702+
local x = 40
703+
local y = 2
704+
return x + y
705+
"#,
706+
)
707+
.into_function()?,
708+
)?;
709+
710+
let yielded = Arc::new(AtomicBool::new(false));
711+
let yielded2 = yielded.clone();
712+
thread.set_hook(mlua::HookTriggers::EVERY_LINE, move |lua, debug| {
713+
if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) {
714+
lua.remove_hook();
715+
return Ok(mlua::VmState::Yield);
716+
}
717+
Ok(mlua::VmState::Continue)
718+
})?;
719+
720+
let mut thread = Box::pin(thread.into_async::<i32>(())?);
721+
poll_fn(|cx| {
722+
assert!(thread.as_mut().poll(cx).is_pending());
723+
Poll::Ready(())
724+
})
725+
.await;
726+
assert!(yielded.load(Ordering::Relaxed));
727+
lua.gc_collect()?;
728+
assert_eq!(thread.await?, 42);
729+
730+
Ok(())
731+
}
732+
690733
#[test]
691734
fn test_async_yield_with() -> Result<()> {
692735
let lua = Lua::new();

tests/hooks.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,40 @@ fn test_hook_yield() -> Result<()> {
294294
Ok(())
295295
}
296296

297+
#[test]
298+
#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))]
299+
fn test_hook_yield_preserves_stack() -> Result<()> {
300+
let lua = Lua::new();
301+
302+
let func = lua
303+
.load(
304+
r#"
305+
local x = { value = 40 }
306+
local y = 2
307+
return x.value + y
308+
"#,
309+
)
310+
.into_function()?;
311+
let co = lua.create_thread(func)?;
312+
313+
let yielded = Arc::new(std::sync::atomic::AtomicBool::new(false));
314+
let yielded2 = yielded.clone();
315+
co.set_hook(HookTriggers::EVERY_LINE, move |_lua, debug| {
316+
if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) {
317+
return Ok(VmState::Yield);
318+
}
319+
Ok(VmState::Continue)
320+
})?;
321+
322+
co.resume::<()>(())?;
323+
assert!(yielded.load(Ordering::Relaxed));
324+
co.remove_hook();
325+
lua.gc_collect()?;
326+
assert_eq!(co.resume::<i32>(())?, 42);
327+
328+
Ok(())
329+
}
330+
297331
#[test]
298332
fn test_global_hook() -> Result<()> {
299333
let lua = Lua::new();

0 commit comments

Comments
 (0)