From 6d37e78406b6e002151cef3665665e810bb31529 Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Fri, 19 Dec 2025 17:38:51 +0900 Subject: [PATCH 1/3] Fix TypeError when processing a detached watcher This patch will fix a race condition where `Coolio_Loop_process_event` attempts to process an event for a watcher that has already been detached. When detach is called (e.g., from another thread or a preceding callback in the same loop cycle), the watcher's loop reference is cleared to nil. If an event for this watcher was already pending in the libev queue, the subsequent processing step causes an TypeError: ``` TypeError: wrong argument type nil (expected Coolio::Loop) ``` The patch will add a check to ensure the watcher is still enabled before accessing the loop data. If the watcher is detached (enabled == 0), the pending event is ignored. --- ext/cool.io/loop.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/cool.io/loop.c b/ext/cool.io/loop.c index 7bd1bbc..9e2e4fd 100644 --- a/ext/cool.io/loop.c +++ b/ext/cool.io/loop.c @@ -113,6 +113,12 @@ void Coolio_Loop_process_event(VALUE watcher, int revents) /* The Global VM lock isn't held right now, but hopefully * we can still do this safely */ watcher_data = Coolio_Watcher_ptr(watcher); + + if (watcher_data->enabled == 0) { + /* Ignore event because watcher was already detached. */ + return; + } + loop_data = Coolio_Loop_ptr(watcher_data->loop); /* Well, what better place to explain how this all works than From c12435761f9e32ce5d9d00062dba73f9a8f488c3 Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Sun, 1 Feb 2026 14:58:21 +0900 Subject: [PATCH 2/3] Add test --- spec/detach_race_condition_spec.rb | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 spec/detach_race_condition_spec.rb diff --git a/spec/detach_race_condition_spec.rb b/spec/detach_race_condition_spec.rb new file mode 100644 index 0000000..b22970c --- /dev/null +++ b/spec/detach_race_condition_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Cool.io::Loop do + class Victim < Cool.io::IOWatcher + def initialize(io) + super + @io = io + end + + def on_readable + begin + @io.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + end + end + end + + # https://github.com/socketry/cool.io/issues/87 + it "does not raise TypeError when a watcher is detached while an event is pending" do + loop = Cool.io::Loop.default + + iterations = 200 + + expect { + iterations.times do + r_victim, w_victim = IO.pipe + victim_watcher = Victim.new(r_victim) + victim_watcher.attach(loop) + + t1 = Thread.new do + sleep 0.01 + w_victim.write("dummy\n") + end + + t2 = Thread.new do + sleep 0.01 + victim_watcher.detach + end + + loop.run_once + + t1.join + t2.join + + r_victim.close + w_victim.close + end + }.not_to raise_error + end +end From 290370bd77fbde7340bec95a02df38ef01d3c007 Mon Sep 17 00:00:00 2001 From: Shizuo Fujita Date: Mon, 9 Feb 2026 11:21:14 +0900 Subject: [PATCH 3/3] Fix SEGV on macOS when detaching an already detached watcher --- ext/cool.io/watcher.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/cool.io/watcher.h b/ext/cool.io/watcher.h index 225e44c..b4892f2 100644 --- a/ext/cool.io/watcher.h +++ b/ext/cool.io/watcher.h @@ -33,6 +33,10 @@ if(watcher_data->loop == Qnil) \ rb_raise(rb_eRuntimeError, "not attached to a loop"); \ \ + if (watcher_data->enabled == 0) { \ + /* Ignore because watcher was already detached. */ \ + return Qnil; \ + } \ loop_data = Coolio_Loop_ptr(watcher_data->loop); \ \ ev_##watcher_type##_stop(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \