Bug #18444
openTrapped TSTP causes a locking deadlock in 3.0.3 onward
Description
A curious case:
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.
Files
Updated by jeremyevans0 (Jeremy Evans) over 4 years ago
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?
Updated by xtkoba (Tee KOBAYASHI) about 4 years ago
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.
Tested environments:
$ ruby27 -v
ruby 2.7.6p219 (2022-04-12 revision c9c2245c0a) [x86_64-linux]
$ ruby30 -v
ruby 3.0.4p208 (2022-04-12 revision 3fa771dded) [x86_64-linux]
$ ruby31 -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
$ uname -a
Linux localhost 5.4.188-gentoo #1 SMP Wed Mar 30 09:04:25 JST 2022 x86_64 Intel(R) Core(TM)2 CPU U7500 @ 1.06GHz GenuineIntel GNU/Linux
Updated by wanabe (_ wanabe) 3 days ago
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_execis executed- while STDOUT is locked by the
printstatement in the main loop,
- while STDOUT is locked by the
- a problem occurs when attempting to use
printwithin thetrapblock - 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.
Updated by wanabe (_ wanabe) 1 day ago
- Related to Bug #19557: Deadlock on STDOUT(ERR) lock on signal handler added