diff --git a/ext/io/event/selector/epoll.c b/ext/io/event/selector/epoll.c index 5a8b1187..0905ebaf 100644 --- a/ext/io/event/selector/epoll.c +++ b/ext/io/event/selector/epoll.c @@ -847,6 +847,7 @@ struct select_arguments { int count; int result; + int error; struct epoll_event events[EPOLL_MAX_EVENTS]; struct timespec * timeout; @@ -868,9 +869,9 @@ static int make_timeout_ms(struct timespec * timeout) { } static -int enosys_error(int result) { +int enosys_error(int result, int error) { if (result == -1) { - return errno == ENOSYS; + return error == ENOSYS; } return 0; @@ -882,12 +883,13 @@ void * select_internal(void *_arguments) { #if defined(HAVE_EPOLL_PWAIT2) arguments->result = epoll_pwait2(arguments->selector->descriptor, arguments->events, arguments->count, arguments->timeout, NULL); + arguments->error = errno; // Comment out the above line and enable the below lines to test ENOSYS code path. // arguments->result = -1; - // errno = ENOSYS; + // arguments->error = ENOSYS; - if (!enosys_error(arguments->result)) { + if (!enosys_error(arguments->result, arguments->error)) { return NULL; } else { @@ -896,6 +898,7 @@ void * select_internal(void *_arguments) { #endif arguments->result = epoll_wait(arguments->selector->descriptor, arguments->events, arguments->count, make_timeout_ms(arguments->timeout)); + arguments->error = errno; return NULL; } @@ -903,12 +906,12 @@ void * select_internal(void *_arguments) { static int select_internal_without_gvl(struct select_arguments *arguments) { arguments->result = -1; + arguments->error = EINTR; IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0); if (arguments->result == -1) { - // If Ruby skips the native callback, the result sentinel can remain `-1`; `errno` may be `0` or `EINTR` depending on the Ruby implementation. Both cases mean the blocking wait did not produce any events. - if (errno != EINTR && errno != 0) { - rb_sys_fail("select_internal_without_gvl:epoll_wait"); + if (arguments->error != EINTR) { + rb_syserr_fail(arguments->error, "select_internal_without_gvl:epoll_wait"); } else { return 0; } @@ -922,8 +925,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) { select_internal((void *)arguments); if (arguments->result == -1) { - if (errno != EINTR) { - rb_sys_fail("select_internal_with_gvl:epoll_wait"); + if (arguments->error != EINTR) { + rb_syserr_fail(arguments->error, "select_internal_with_gvl:epoll_wait"); } else { return 0; } diff --git a/ext/io/event/selector/kqueue.c b/ext/io/event/selector/kqueue.c index 0c614c49..b0c7cb84 100644 --- a/ext/io/event/selector/kqueue.c +++ b/ext/io/event/selector/kqueue.c @@ -851,6 +851,7 @@ struct select_arguments { int count; int result; + int error; struct kevent events[KQUEUE_MAX_EVENTS]; struct timespec storage; @@ -864,6 +865,7 @@ void * select_internal(void *_arguments) { struct select_arguments * arguments = (struct select_arguments *)_arguments; arguments->result = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout); + arguments->error = errno; return NULL; } @@ -871,12 +873,12 @@ void * select_internal(void *_arguments) { static int select_internal_without_gvl(struct select_arguments *arguments) { arguments->result = -1; + arguments->error = EINTR; IO_Event_Selector_blocking_operation(&arguments->selector->backend, select_internal, (void *)arguments, RUBY_UBF_IO, 0); if (arguments->result == -1) { - // If Ruby skips the native callback, the result sentinel can remain `-1`; `errno` may be `0` or `EINTR` depending on the Ruby implementation. Both cases mean the blocking wait did not produce any events. - if (errno != EINTR && errno != 0) { - rb_sys_fail("select_internal_without_gvl:kevent"); + if (arguments->error != EINTR) { + rb_syserr_fail(arguments->error, "select_internal_without_gvl:kevent"); } else { return 0; } @@ -890,8 +892,8 @@ int select_internal_with_gvl(struct select_arguments *arguments) { select_internal((void *)arguments); if (arguments->result == -1) { - if (errno != EINTR) { - rb_sys_fail("select_internal_with_gvl:kevent"); + if (arguments->error != EINTR) { + rb_syserr_fail(arguments->error, "select_internal_with_gvl:kevent"); } else { return 0; } diff --git a/releases.md b/releases.md index 7ccc63d1..b83cc5b8 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Capture `errno` immediately after `epoll_wait` / `kevent`, preventing stale or subsequently clobbered values from raising a spurious `Errno::*` when a native selector wait is interrupted or skipped. + ## v1.19.3 - Prevent `URing`, `EPoll`, and `KQueue` from entering a native wait when a signal exception becomes pending during the GVL transition. On Ruby versions without `RB_NOGVL_PENDING_INTR_FAIL`, the fallback now refreshes pending-interrupt state immediately before releasing the GVL while preserving the caller's exception-delivery timing. diff --git a/test/io/event/selector/interruptable.rb b/test/io/event/selector/interruptable.rb index ec1a353a..449a13a7 100644 --- a/test/io/event/selector/interruptable.rb +++ b/test/io/event/selector/interruptable.rb @@ -8,7 +8,7 @@ require "socket" Interruptable = Sus::Shared("interruptable") do - it "does not block with pending interrupts" do + it "ignores stale errno with pending interrupts" do selector = subject.new(Fiber.current) woken = false @@ -23,7 +23,16 @@ begin begin Thread.handle_interrupt(::SignalException => :never) do + # Keep an interrupt pending so Ruby may skip the native selector callback. Thread.current.raise(::Interrupt) + + # Leave an unrelated ENOENT in thread-local errno. A skipped callback must + # not mistake this stale value for an error from epoll_wait/kevent. + begin + File.stat(__FILE__ + ".missing") + rescue Errno::ENOENT + end + result = selector.select(nil) end rescue ::Interrupt