Bug #22133
openRuby's default SIGINT handling ignores `Thread.handle_interrupt` masking.
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:
- A failing regression test for default SIGINT with
Thread.handle_interrupt. - The implementation change routing default SIGINT through the pending interrupt queue.
- Ruby/spec coverage for default SIGINT ->
Interruptand default SIGTERM ->SignalExceptionbeing maskable byThread.handle_interrupt.
Once accepted, I would like to request backports for supported Ruby branches where this default SIGINT behavior is present.
Updated by ioquatix (Samuel Williams) 2 days ago
- Related to Feature #6762: Control interrupt timing added
Updated by kosaki (Motohiro KOSAKI) 2 days ago
I haven't read the code yet, but what you're saying seems reasonable.
Updated by Eregon (Benoit Daloze) about 16 hours ago
Seems like a clear bugfix.
TruffleRuby already behaves like this.