Project

General

Profile

Actions

Bug #22133

open

Ruby's default SIGINT handling ignores `Thread.handle_interrupt` masking.

Bug #22133: Ruby's default SIGINT handling ignores `Thread.handle_interrupt` masking.
1

Added by ioquatix (Samuel Williams) 3 days ago. Updated about 24 hours ago.


Description

Ruby's default SIGINT handling currently bypasses Thread.handle_interrupt masking. When a process receives SIGINT with the default Ruby handler installed, CRuby calls rb_interrupt() directly. That can raise Interrupt immediately inside code wrapped by Thread.handle_interrupt(SignalException => :never), including while blocked in operations like Thread::Queue#pop.

This is inconsistent with other asynchronous exception delivery paths, such as Thread#raise, and with other default signal exceptions such as SIGTERM. Those paths enqueue a pending interrupt on the target thread, allowing Thread.handle_interrupt to defer delivery until the configured point.

Background

This was discovered while working on Async/Async::Container signal handling. Async relies on Thread.handle_interrupt and scheduler-level interruption boundaries to keep signal delivery deterministic. The current default SIGINT path means a Ctrl-C can escape a masked section and interrupt internal synchronization code, which can break graceful shutdown behavior.

A minimal reproduction is:

waiting = Thread::Queue.new
release = Thread::Queue.new
inner = false

Thread.new do
  waiting.pop
  Process.kill(:INT, Process.pid)
  release.push(true)
end

begin
  Thread.handle_interrupt(SignalException => :never) do
    begin
      waiting.push(true)
      release.pop
    rescue Interrupt
      inner = true
      raise
    end
  end
rescue Interrupt
  puts "outer"
end

puts inner

Expected output:

outer
false

Current affected behavior can produce inner == true, meaning the Interrupt was delivered inside the masked region.

Proposed fix

Route default SIGINT through the same pending-interrupt mechanism as other default signal exceptions, while preserving the traditional exception class and no-message Interrupt object. Concretely, the PR replaces the direct rb_interrupt() default SIGINT path with a helper that enqueues an Interrupt on the main thread and wakes that thread.

This makes default SIGINT respect Thread.handle_interrupt(SignalException => :never) in the same way as default SIGTERM/SignalException and Thread#raise.

Pull request

PR: https://github.com/ruby/ruby/pull/17533

The PR includes:

  1. A failing regression test for default SIGINT with Thread.handle_interrupt.
  2. The implementation change routing default SIGINT through the pending interrupt queue.
  3. Ruby/spec coverage for default SIGINT -> Interrupt and default SIGTERM -> SignalException being maskable by Thread.handle_interrupt.

Once accepted, I would like to request backports for supported Ruby branches where this default SIGINT behavior is present.


Related issues 1 (0 open1 closed)

Related to Ruby - Feature #6762: Control interrupt timingClosedko1 (Koichi Sasada)Actions

Updated by ioquatix (Samuel Williams) 2 days ago Actions #1

Updated by kosaki (Motohiro KOSAKI) 2 days ago Actions #2 [ruby-core:125872]

I haven't read the code yet, but what you're saying seems reasonable.

Updated by Eregon (Benoit Daloze) about 24 hours ago Actions #3 [ruby-core:125893]

Seems like a clear bugfix.
TruffleRuby already behaves like this.

Actions

Also available in: PDF Atom