From d57664ef89ee09f944a29e1e58029a0f22962c13 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 27 Aug 2026 12:57:22 +1200 Subject: [PATCH 1/4] Add SQE acquisition failure regression test Assisted-By: devx/efad4cc8-cf12-4d6e-8c55-6f2c3a6683d2 --- ext/io/event/selector/uring.c | 38 +++++++++++++++++++++++++++++++++ test/io/event/selector/uring.rb | 33 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/io/event/selector/uring.rb diff --git a/ext/io/event/selector/uring.c b/ext/io/event/selector/uring.c index 9b3b959a..57740c7f 100644 --- a/ext/io/event/selector/uring.c +++ b/ext/io/event/selector/uring.c @@ -59,6 +59,9 @@ struct IO_Event_Selector_URing struct IO_Event_Array completions; struct IO_Event_List free_list; + + // Test hook for exercising SQE acquisition failures deterministically. + int test_sqe_error; }; struct IO_Event_Selector_URing_Completion; @@ -316,6 +319,7 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) { selector->completions.element_initialize = IO_Event_Selector_URing_Completion_initialize; selector->completions.element_free = IO_Event_Selector_URing_Completion_free; IO_Event_Array_initialize(&selector->completions, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_URing_Completion)); + selector->test_sqe_error = 0; return instance; } @@ -546,6 +550,12 @@ void io_uring_submit_pending(struct IO_Event_Selector_URing *selector) { } struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) { + if (selector->test_sqe_error) { + int error = selector->test_sqe_error; + selector->test_sqe_error = 0; + rb_syserr_fail(error, "io_get_sqe:test failure"); + } + struct io_uring_sqe *sqe = io_uring_get_sqe(&selector->ring); while (sqe == NULL) { @@ -1760,6 +1770,31 @@ static int IO_Event_Selector_URing_supported_p(void) { return 1; } +static VALUE IO_Event_Selector_URing_test_fail_next_sqe(VALUE self, VALUE _error) { + struct IO_Event_Selector_URing *selector = NULL; + TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); + + selector->test_sqe_error = NUM2INT(_error); + + return Qnil; +} + +static VALUE IO_Event_Selector_URing_test_pending_completions(VALUE self) { + struct IO_Event_Selector_URing *selector = NULL; + TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); + + size_t pending = 0; + for (size_t index = 0; index < selector->completions.limit; index += 1) { + struct IO_Event_Selector_URing_Completion *completion = selector->completions.base[index]; + + if (completion && (completion->waiting || completion->operation_pending || completion->cancellation_pending)) { + pending += 1; + } + } + + return SIZET2NUM(pending); +} + void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector) { if (!IO_Event_Selector_URing_supported_p()) { return; @@ -1803,4 +1838,7 @@ void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector) { rb_define_method(IO_Event_Selector_URing, "io_close", IO_Event_Selector_URing_io_close, 1); rb_define_method(IO_Event_Selector_URing, "process_wait", IO_Event_Selector_URing_process_wait, 3); + + rb_define_private_method(IO_Event_Selector_URing, "test_fail_next_sqe", IO_Event_Selector_URing_test_fail_next_sqe, 1); + rb_define_private_method(IO_Event_Selector_URing, "test_pending_completions", IO_Event_Selector_URing_test_pending_completions, 0); } diff --git a/test/io/event/selector/uring.rb b/test/io/event/selector/uring.rb new file mode 100644 index 00000000..9cbe2151 --- /dev/null +++ b/test/io/event/selector/uring.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "io/event" +require "io/event/selector" + +return unless defined?(IO::Event::Selector::URing) + +describe IO::Event::Selector::URing do + it "releases completion state when SQE acquisition fails" do + pid = Process.fork do + selector = subject.new(Fiber.current) + input, output = IO.pipe + error = false + + selector.send(:test_fail_next_sqe, Errno::EIO::Errno) + + begin + selector.io_wait(Fiber.current, input, IO::READABLE) + rescue Errno::EIO + error = true + end + + pending = selector.send(:test_pending_completions) + exit!(error && pending == 0 ? 0 : 1) + end + + _, status = Process.wait2(pid) + expect(status).to be(:success?) + end +end From dd9b627ee3ed5586653d5583355b31562dc659e6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 27 Aug 2026 12:58:45 +1200 Subject: [PATCH 2/4] Rollback completion state when SQE setup fails Assisted-By: devx/efad4cc8-cf12-4d6e-8c55-6f2c3a6683d2 --- ext/io/event/selector/uring.c | 59 +++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/ext/io/event/selector/uring.c b/ext/io/event/selector/uring.c index 57740c7f..ca0a7932 100644 --- a/ext/io/event/selector/uring.c +++ b/ext/io/event/selector/uring.c @@ -216,11 +216,18 @@ struct IO_Event_Selector_URing_Completion * IO_Event_Selector_URing_Completion_a waiting->completion = completion; completion->waiting = waiting; - completion->operation_pending = true; + // The operation is not pending until an SQE has been prepared successfully. return completion; } +inline static +void IO_Event_Selector_URing_Completion_submit(struct IO_Event_Selector_URing_Completion *completion) +{ + assert(!completion->operation_pending); + completion->operation_pending = true; +} + inline static void IO_Event_Selector_URing_Completion_cancel(struct IO_Event_Selector_URing_Completion *completion) { @@ -242,6 +249,16 @@ void IO_Event_Selector_URing_Completion_recycle(struct IO_Event_Selector_URing * IO_Event_List_prepend(&selector->free_list, &completion->list); } +inline static +void IO_Event_Selector_URing_Completion_abandon(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Completion *completion) +{ + assert(!completion->operation_pending); + assert(!completion->cancellation_pending); + + IO_Event_Selector_URing_Completion_cancel(completion); + IO_Event_Selector_URing_Completion_recycle(selector, completion); +} + inline static void IO_Event_Selector_URing_Completion_complete(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Completion *completion) { @@ -568,14 +585,32 @@ struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) { return sqe; } +static VALUE IO_Event_Selector_URing_get_sqe_protected(VALUE _selector) { + struct IO_Event_Selector_URing *selector = (struct IO_Event_Selector_URing *)(uintptr_t)_selector; + + return (VALUE)(uintptr_t)io_get_sqe(selector); +} + +static struct io_uring_sqe * IO_Event_Selector_URing_Completion_get_sqe(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Completion *completion) { + int state = 0; + VALUE result = rb_protect(IO_Event_Selector_URing_get_sqe_protected, (VALUE)selector, &state); + + if (state) { + IO_Event_Selector_URing_Completion_abandon(selector, completion); + rb_jump_tag(state); + } + + return (struct io_uring_sqe *)(uintptr_t)result; +} + static void IO_Event_Selector_URing_Completion_cancel_async(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Completion *completion) { if (completion->cancellation_pending) return; - completion->cancellation_pending = true; - struct io_uring_sqe *sqe = io_get_sqe(selector); + completion->cancellation_pending = true; + io_uring_prep_cancel(sqe, completion, 0); io_uring_sqe_set_data64(sqe, IO_Event_Selector_URing_Completion_cancellation_data(completion)); io_uring_submit_pending(selector); @@ -791,7 +826,7 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid, #endif }; - struct io_uring_sqe *sqe = io_get_sqe(selector); + struct io_uring_sqe *sqe = IO_Event_Selector_URing_Completion_get_sqe(selector, completion); #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID id_t id; @@ -809,6 +844,7 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid, io_uring_prep_poll_add(sqe, descriptor, POLLIN|POLLHUP|POLLERR); #endif io_uring_sqe_set_data(sqe, completion); + IO_Event_Selector_URing_Completion_submit(completion); io_uring_submit_pending(selector); return rb_ensure(process_wait_transfer, (VALUE)&process_wait_arguments, process_wait_ensure, (VALUE)&process_wait_arguments); @@ -900,9 +936,10 @@ VALUE IO_Event_Selector_URing_io_wait(VALUE self, VALUE fiber, VALUE io, VALUE e struct IO_Event_Selector_URing_Completion *completion = IO_Event_Selector_URing_Completion_acquire(selector, &waiting); - struct io_uring_sqe *sqe = io_get_sqe(selector); + struct io_uring_sqe *sqe = IO_Event_Selector_URing_Completion_get_sqe(selector, completion); io_uring_prep_poll_add(sqe, descriptor, flags); io_uring_sqe_set_data(sqe, completion); + IO_Event_Selector_URing_Completion_submit(completion); // If we are going to wait, we assume that we are waiting for a while: io_uring_submit_pending(selector); @@ -954,9 +991,11 @@ io_read_submit(VALUE _arguments) if (DEBUG) fprintf(stderr, "io_read_submit:io_uring_prep_read(waiting=%p, completion=%p, descriptor=%d, buffer=%p, length=%ld)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion, arguments->descriptor, arguments->buffer, arguments->length); - struct io_uring_sqe *sqe = io_get_sqe(selector); + struct IO_Event_Selector_URing_Completion *completion = arguments->waiting->completion; + struct io_uring_sqe *sqe = IO_Event_Selector_URing_Completion_get_sqe(selector, completion); io_uring_prep_read(sqe, arguments->descriptor, arguments->buffer, arguments->length, arguments->offset); - io_uring_sqe_set_data(sqe, arguments->waiting->completion); + io_uring_sqe_set_data(sqe, completion); + IO_Event_Selector_URing_Completion_submit(completion); io_uring_submit_pending(selector); IO_Event_Selector_loop_yield(&selector->backend); @@ -1221,9 +1260,11 @@ io_write_submit(VALUE _argument) if (DEBUG) fprintf(stderr, "io_write_submit:io_uring_prep_write(waiting=%p, completion=%p, descriptor=%d, buffer=%p, length=%ld)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion, arguments->descriptor, arguments->buffer, arguments->length); - struct io_uring_sqe *sqe = io_get_sqe(selector); + struct IO_Event_Selector_URing_Completion *completion = arguments->waiting->completion; + struct io_uring_sqe *sqe = IO_Event_Selector_URing_Completion_get_sqe(selector, completion); io_uring_prep_write(sqe, arguments->descriptor, arguments->buffer, arguments->length, arguments->offset); - io_uring_sqe_set_data(sqe, arguments->waiting->completion); + io_uring_sqe_set_data(sqe, completion); + IO_Event_Selector_URing_Completion_submit(completion); io_uring_submit_pending(selector); IO_Event_Selector_loop_yield(&selector->backend); From 34958a5375787c81b0a6cbd2c34d579caebf541a Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 27 Aug 2026 13:01:27 +1200 Subject: [PATCH 3/4] Fix test formatting Assisted-By: devx/efad4cc8-cf12-4d6e-8c55-6f2c3a6683d2 --- test/io/event/selector/uring.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/io/event/selector/uring.rb b/test/io/event/selector/uring.rb index 9cbe2151..f0397669 100644 --- a/test/io/event/selector/uring.rb +++ b/test/io/event/selector/uring.rb @@ -14,19 +14,19 @@ selector = subject.new(Fiber.current) input, output = IO.pipe error = false - + selector.send(:test_fail_next_sqe, Errno::EIO::Errno) - + begin selector.io_wait(Fiber.current, input, IO::READABLE) rescue Errno::EIO error = true end - + pending = selector.send(:test_pending_completions) exit!(error && pending == 0 ? 0 : 1) end - + _, status = Process.wait2(pid) expect(status).to be(:success?) end From d01bba0f7e30ee202b9f28c5fa97c394d5bb2938 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 27 Aug 2026 13:44:55 +1200 Subject: [PATCH 4/4] Remove runtime fault injection hooks Assisted-By: devx/efad4cc8-cf12-4d6e-8c55-6f2c3a6683d2 --- ext/io/event/selector/uring.c | 38 --------------------------------- test/io/event/selector/uring.rb | 33 ---------------------------- 2 files changed, 71 deletions(-) delete mode 100644 test/io/event/selector/uring.rb diff --git a/ext/io/event/selector/uring.c b/ext/io/event/selector/uring.c index ca0a7932..fa69b463 100644 --- a/ext/io/event/selector/uring.c +++ b/ext/io/event/selector/uring.c @@ -59,9 +59,6 @@ struct IO_Event_Selector_URing struct IO_Event_Array completions; struct IO_Event_List free_list; - - // Test hook for exercising SQE acquisition failures deterministically. - int test_sqe_error; }; struct IO_Event_Selector_URing_Completion; @@ -336,7 +333,6 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) { selector->completions.element_initialize = IO_Event_Selector_URing_Completion_initialize; selector->completions.element_free = IO_Event_Selector_URing_Completion_free; IO_Event_Array_initialize(&selector->completions, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_URing_Completion)); - selector->test_sqe_error = 0; return instance; } @@ -567,12 +563,6 @@ void io_uring_submit_pending(struct IO_Event_Selector_URing *selector) { } struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) { - if (selector->test_sqe_error) { - int error = selector->test_sqe_error; - selector->test_sqe_error = 0; - rb_syserr_fail(error, "io_get_sqe:test failure"); - } - struct io_uring_sqe *sqe = io_uring_get_sqe(&selector->ring); while (sqe == NULL) { @@ -1811,31 +1801,6 @@ static int IO_Event_Selector_URing_supported_p(void) { return 1; } -static VALUE IO_Event_Selector_URing_test_fail_next_sqe(VALUE self, VALUE _error) { - struct IO_Event_Selector_URing *selector = NULL; - TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); - - selector->test_sqe_error = NUM2INT(_error); - - return Qnil; -} - -static VALUE IO_Event_Selector_URing_test_pending_completions(VALUE self) { - struct IO_Event_Selector_URing *selector = NULL; - TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); - - size_t pending = 0; - for (size_t index = 0; index < selector->completions.limit; index += 1) { - struct IO_Event_Selector_URing_Completion *completion = selector->completions.base[index]; - - if (completion && (completion->waiting || completion->operation_pending || completion->cancellation_pending)) { - pending += 1; - } - } - - return SIZET2NUM(pending); -} - void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector) { if (!IO_Event_Selector_URing_supported_p()) { return; @@ -1879,7 +1844,4 @@ void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector) { rb_define_method(IO_Event_Selector_URing, "io_close", IO_Event_Selector_URing_io_close, 1); rb_define_method(IO_Event_Selector_URing, "process_wait", IO_Event_Selector_URing_process_wait, 3); - - rb_define_private_method(IO_Event_Selector_URing, "test_fail_next_sqe", IO_Event_Selector_URing_test_fail_next_sqe, 1); - rb_define_private_method(IO_Event_Selector_URing, "test_pending_completions", IO_Event_Selector_URing_test_pending_completions, 0); } diff --git a/test/io/event/selector/uring.rb b/test/io/event/selector/uring.rb deleted file mode 100644 index f0397669..00000000 --- a/test/io/event/selector/uring.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2026, by Samuel Williams. - -require "io/event" -require "io/event/selector" - -return unless defined?(IO::Event::Selector::URing) - -describe IO::Event::Selector::URing do - it "releases completion state when SQE acquisition fails" do - pid = Process.fork do - selector = subject.new(Fiber.current) - input, output = IO.pipe - error = false - - selector.send(:test_fail_next_sqe, Errno::EIO::Errno) - - begin - selector.io_wait(Fiber.current, input, IO::READABLE) - rescue Errno::EIO - error = true - end - - pending = selector.send(:test_pending_completions) - exit!(error && pending == 0 ? 0 : 1) - end - - _, status = Process.wait2(pid) - expect(status).to be(:success?) - end -end