# Reproducer: rb_profile_frames() called from a signal handler (as profilers
# like Datadog's do) segfaults if the signal arrives while vm_make_env_each()
# is between escaping a parent frame and updating the child frame's prev EP.
#
# Usage:
#   ruby rb_profile_frames_signal_crash.rb                    # MODE=class (default)
#   MODE=define_method ruby rb_profile_frames_signal_crash.rb
#   MODE=thread ruby rb_profile_frames_signal_crash.rb
#   INTERVAL_US=1000 ruby rb_profile_frames_signal_crash.rb   # 1 signal per ms instead of back-to-back

require 'tmpdir'
require 'rbconfig'

def inline_c_extension(c_code)
  Dir.mktmpdir('inline_c_extension') do |dir|
    File.write("#{dir}/cext.c", c_code)
    File.write("#{dir}/extconf.rb", <<~RUBY)
    require 'mkmf'
    create_makefile('cext')
    RUBY

    out = IO.popen([RbConfig.ruby, 'extconf.rb'], chdir: dir, &:read)
    raise "ruby extconf.rb failed: #{$?.inspect}\n#{out}" unless $?.success?

    out = IO.popen(['make'], chdir: dir, &:read)
    raise "make failed: #{$?.inspect}\n#{out}" unless $?.success?

    require "#{dir}/cext.#{RbConfig::CONFIG['DLEXT']}"
  end
end

inline_c_extension <<~C
#include <ruby.h>
#include <ruby/debug.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>

#define MAX_FRAMES 400

static pthread_t target_thread;
static pthread_t sampler_thread;
static volatile int running = 0;
static useconds_t interval_us;
static volatile unsigned long samples = 0;

static void sigprof_handler(int sig, siginfo_t *info, void *ucontext) {
  int saved_errno = errno;
  VALUE frames[MAX_FRAMES];
  int lines[MAX_FRAMES];
  rb_profile_frames(0, MAX_FRAMES, frames, lines);
  samples++;
  errno = saved_errno;
}

static void *sampler_loop(void *arg) {
  while (running) {
    pthread_kill(target_thread, SIGPROF);
    if (interval_us) usleep(interval_us);
  }
  return NULL;
}

// Start sending SIGPROF to the calling (main) thread every interval_us microseconds
static VALUE pf_start(VALUE self, VALUE interval) {
  struct sigaction sa = {0};
  sa.sa_sigaction = sigprof_handler;
  sa.sa_flags = SA_SIGINFO | SA_RESTART;
  sigemptyset(&sa.sa_mask);
  if (sigaction(SIGPROF, &sa, NULL) != 0) rb_sys_fail("sigaction");

  interval_us = NUM2UINT(interval);
  target_thread = pthread_self();
  running = 1;
  if (pthread_create(&sampler_thread, NULL, sampler_loop, NULL) != 0) rb_sys_fail("pthread_create");
  return Qnil;
}

static VALUE pf_stop(VALUE self) {
  running = 0;
  pthread_join(sampler_thread, NULL);
  return Qnil;
}

static VALUE pf_samples(VALUE self) {
  return ULONG2NUM(samples);
}

void Init_cext(void) {
  VALUE mod = rb_define_module("ProfileFramesSigprof");
  rb_define_module_function(mod, "start", pf_start, 1);
  rb_define_module_function(mod, "stop", pf_stop, 0);
  rb_define_module_function(mod, "samples", pf_samples, 0);
}
C

interval_us = Integer(ENV.fetch('INTERVAL_US', '0'))
iterations = Integer(ENV.fetch('ITERATIONS', '10_000_000'))
mode = ENV.fetch('MODE', 'class')

ProfileFramesSigprof.start(interval_us)

# Each class body execution is a fresh "local" frame whose ME_CREF is a cref
# (not a method entry) and which has no block handler (SPECVAL == 0).
# The block passed to #times is the frame that creates the Proc, whose prev EP is
# the class body EP, so vm_make_env_each() recurses into the class body frame.
case mode
when 'class'
  iterations.times do
    class Foo
      1.times { proc {} }
    end
  end
when 'define_method'
  iterations.times do
    class Foo
      1.times { define_method(:foo) {} }
    end
  end
when 'thread'
  iterations.times do
    class Foo
      1.times { Thread.new {}.join }
    end
  end
else
  abort "Unknown MODE: #{mode}"
end

ProfileFramesSigprof.stop
puts "No crash after #{iterations} iterations, #{ProfileFramesSigprof.samples} samples"
