ruby -e 'Signal.trap("TSTP") { puts "Received a terminal stop signal, but i will sleep instead."; sleep 10 }; loop {puts 1}'
this fails with deadlock; recursive locking (ThreadError) when I send the SIGTSTP via my terminal. This is on Mac OS Monterey (M1). It only happens in 3.0.3 and onward (I tried 3.1.0-preview1 as well, fails there too), when I try 3.0.2, the signal is handled properly.
I couldn't replicate this behavior in OpenBSD/amd64. On OpenBSD/amd64, sending TSTP prints the Received... string twice, 10 seconds part, followed by the loop printing 1. Windows doesn't support TSTP, so no reason to test there. Can anyone replicate this outside of M1 MacOS?
I tested this on several Linux (GNU/Linux and Android) environments. Sending Ctrl-Z from terminal sometimes results in "Received ..." and sometimes "deadlock" but not sticking to one result for each environment. I used the newest released versions of Ruby (2.7.6, 3.0.4, 3.1.2) but not sure what happens for older versions.
This issue can also be reproduced on the master branch. Since it is not specific to TSTP, it can be reproduced with any signal.
$ ./miniruby -ve 'Signal.trap(:TERM) { print "2\r"; sleep 0.01; exit(0) }; spawn("sleep 0.1; kill -TERM #{$$}"); loop {print "1\r"}'
ruby 4.1.0dev (2026-08-12T02:39:28Z origin/master c6e9858440) +PRISM [x86_64-linux]
-e:1:in 'IO#write': deadlock; recursive locking (ThreadError)
from -e:1:in 'Kernel#print'
from -e:1:in 'block in <main>'
from -e:1:in 'IO#write'
from -e:1:in 'Kernel#print'
from -e:1:in 'block in <main>'
from -e:1:in 'Kernel#loop'
from -e:1:in '<main>'
I attached the backtrace captured with GDB.
Summary:
print "1\r" # print in loop{}
->
#44 0x00005555557005d5 in io_binwrite (ptr=0x55555606e810 "1\r", len=2, fptr=0x555555f26c40, nosync=0) at ../../io.c:1939
#43 0x000055555589676c in rb_mutex_synchronize (self=140737345534480, func=0x555555700320 <io_binwrite_string>, arg=140737488338256) at ../../thread_sync.c:629
#42 0x00005555556b408c in rb_ec_ensure (ec_arg=0x555555ebff58, b_proc=0x555555700320 <io_binwrite_string>, data1=140737488338256,
e_proc_arg=0x555555896318 <do_mutex_unlock_safe>, data2_arg=140737488338176) at ../../eval.c:1171
#40 0x000055555570030a in io_binwrite_string_internal (fptr=0x555555f26c40, ptr=0x55555606e810 "1\r", length=2) at ../../io.c:1817
#38 0x00005555556fdf28 in rb_io_blocking_region_wait (io=0x555555f26c40, function=0x5555556ff0db <internal_write_func>, argument=0x7fffffffbac0, events=RUBY_IO_WRITABLE)
at ../../io.c:230
#35 0x000055555589d86e in rb_threadptr_execute_interrupts (th=0x555555ea0780 <_main_thread>, blocking_timing=1) at ../../thread.c:2820
Signal.trap(:TERM)
->
#34 0x000055555584688a in rb_signal_exec (th=0x555555ea0780 <_main_thread>, sig=15) at ../../signal.c:1143
#33 0x00005555558466ef in signal_exec (cmd=140737345533960, sig=15) at ../../signal.c:1081
#32 0x00005555559106d9 in rb_eval_cmd_call_kw (cmd=140737345533960, argc=1, argv=0x7fffffffb778, kw_splat=0) at ../../vm_eval.c:2196
print "2\r" # print in trap{}
->
#3 0x00005555557005d5 in io_binwrite (ptr=0x55555606e8c0 "2\r", len=2, fptr=0x555555f26c40, nosync=0) at ../../io.c:1939
#2 0x000055555589674a in rb_mutex_synchronize (self=140737345534480, func=0x555555700320 <io_binwrite_string>, arg=140737488328896) at ../../thread_sync.c:628
#1 0x0000555555895bf4 in do_mutex_lock (args=0x7fffffff9870, interruptible_p=1) at ../../thread_sync.c:296
#0 rb_raise (exc_class=93824993755556, fmt=0x7fffffff9710 "@\227\377\377\377\177") at ../../error.c:3930
Since rb_signal_exec is executed
while STDOUT is locked by the print statement in the main loop,
a problem occurs when attempting to use print within the trap block
because STDOUT is still locked.
Ideally, this issue could be resolved by ensuring that rb_signal_exec is executed only after the lock is released by the first print statement in the main loop.
But I am concerned that doing so might compromise compatibility.