From c4e06b4e30d0dbdd1a9cd76f5d97fe1220b3a23c Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Fri, 11 Sep 2026 10:50:17 -0400 Subject: [PATCH] Differentiate crash from other process sending signal Recently we were debugging a Ruby crash by looking at a core dump and realized that there was no crash, it was another process sending a SIGABRT signal to the process. There was no differentiation in the crash report for this case. Now, when another process sends a SIGABRT, SIGSEGV, etc to a process it shows the PID of the sender. This way we can know right away that it was not a real crash. The crash message now looks like this: ``` [BUG] Aborted at 0x00000001831c150c (sent by pid 53859) ``` If "(sent by pid XXX)" or "(sent by another process)" is included in the crash message, the signal was sent by another process. --- signal.c | 36 ++++++++++++++++++++++++++++++++++- test/ruby/test_rubyoptions.rb | 29 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/signal.c b/signal.c index 77778dc9531081..c645c8036beb58 100644 --- a/signal.c +++ b/signal.c @@ -896,7 +896,41 @@ check_stack_overflow(int sig, const void *addr) # else # define CHECK_STACK_OVERFLOW_() check_stack_overflow(sig, FAULT_ADDRESS) # endif -# define MESSAGE_FAULT_ADDRESS " at %p", FAULT_ADDRESS + +/* Only the si_code values whose siginfo_t carries a sender pid; SI_TIMER and + * SI_SIGIO overlay si_tid/si_band at si_pid's offset. */ +static bool +is_user_generated(const siginfo_t *info) +{ + int code = info->si_code; + if (code == SI_USER) return true; +# ifdef SI_QUEUE + if (code == SI_QUEUE) return true; +# endif +# ifdef SI_TKILL + if (code == SI_TKILL) return true; +# endif + return code == 0; +} + +static const char * +signal_sender_message(const siginfo_t *info) +{ + /* check_reserved_signal_ lets only the first fatal signal get this far. */ + static char buf[64]; + + if (info && is_user_generated(info)) { + rb_pid_t pid = info->si_pid; + if (pid == getpid()) return ""; + /* si_pid is 0 when the sender is not visible in our PID namespace. */ + if (pid == 0) return " (sent by another process)"; + snprintf(buf, sizeof(buf), " (sent by pid %"PRI_PIDT_PREFIX"d)", pid); + return buf; + } + return ""; +} + +# define MESSAGE_FAULT_ADDRESS " at %p%s", FAULT_ADDRESS, signal_sender_message(info) # define SIGNAL_FROM_USER_P() ((info)->si_code == SI_USER) # define CHECK_STACK_OVERFLOW() (SIGNAL_FROM_USER_P() ? (void)0 : CHECK_STACK_OVERFLOW_()) # endif diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index 71ccf0ed90f159..b00d852823e631 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -973,6 +973,35 @@ def test_crash_report_pipe_script end end + # The sender pid is only reported for a signal another process sent us. + def abrt_crash_report(code) + Dir.mktmpdir("ruby_crash_report") do |dir| + IO.popen([{"RUBY_CRASH_REPORT" => "abrt.log", "RUBY_ON_BUG" => nil}, + EnvUtil.rubybin, "--disable-gems", "-e", "STDOUT.sync = true; puts; #{code}"], + chdir: dir, err: File::NULL, rlimit_core: 0) do |child| + child.gets + yield child if block_given? + end + break File.read(File.join(dir, "abrt.log")) + end + end + + def test_crash_report_sender_pid + omit "needs siginfo" unless (macos? || linux?) + + report = abrt_crash_report("sleep") {|child| Process.kill(:ABRT, child.pid)} + assert_include(report, "[BUG] Aborted") + assert_include(report, "(sent by pid #{Process.pid})") + end + + def test_crash_report_no_sender_pid_when_self_inflicted + omit "needs siginfo" unless (macos? || linux?) + + report = abrt_crash_report("Process.kill(:ABRT, $$)") + assert_include(report, "[BUG] Aborted") + assert_not_include(report, "sent by pid") + end + def test_DATA Tempfile.create(["test_ruby_test_rubyoption", ".rb"]) {|t| t.puts "puts DATA.read.inspect"