Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions ext/io/event/selector/epoll.c
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ struct select_arguments {

int count;
int result;
int error;
struct epoll_event events[EPOLL_MAX_EVENTS];

struct timespec * timeout;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -896,19 +898,20 @@ 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;
}

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;
}
Expand All @@ -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;
}
Expand Down
12 changes: 7 additions & 5 deletions ext/io/event/selector/kqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,7 @@ struct select_arguments {

int count;
int result;
int error;
struct kevent events[KQUEUE_MAX_EVENTS];

struct timespec storage;
Expand All @@ -864,19 +865,20 @@ 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;
}

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;
}
Expand All @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
11 changes: 10 additions & 1 deletion test/io/event/selector/interruptable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
Loading