Project

General

Profile

Actions

Bug #22133

closed

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) 22 days ago. Updated 7 days ago.

Status:
Closed
Target version:
-
[ruby-core:125864]

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) 22 days ago Actions #1

Updated by kosaki (Motohiro KOSAKI) 22 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) 21 days ago Actions #3 [ruby-core:125893]

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

Updated by ko1 (Koichi Sasada) 12 days ago Actions #4 [ruby-core:125988]

There are no objection, so let's fix it.

Updated by headius (Charles Nutter) 12 days ago Actions #5 [ruby-core:126006]

JRuby does not by default trap SIGINT to raise Interrupt, since that would interfere with the JVM's own clean shutdown connected to SIGINT by default. Users can opt into that behavior if they want.

If a JRuby user traps SIGINT and chooses to raise Interrupt, they would be subject to Thread.handle_interrupt controls like any other Ruby exception.

Updated by Anonymous 11 days ago Actions #6

  • Status changed from Open to Closed

Applied in changeset git|ef67c377508ea3a07516c1fa413a038b5268fc1c.


Respect handle_interrupt for default SIGINT. (#17533)

[[Bug #22133]]

Updated by k0kubun (Takashi Kokubun) 7 days ago Actions #7 [ruby-core:126080]

  • Backport changed from 3.3: REQUIRED, 3.4: REQUIRED, 4.0: REQUIRED to 3.3: REQUIRED, 3.4: REQUIRED, 4.0: DONE
Actions

Also available in: PDF Atom