From abd3ec57e9a0b2ef2767504c302766b9a75786e4 Mon Sep 17 00:00:00 2001 From: David CARLIER Date: Sat, 12 Sep 2026 08:41:48 +0100 Subject: [PATCH 1/8] Use `PROT_MAX` when available to prevent page promotion. (#2946) --- cont.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cont.c b/cont.c index 42cebf539c93ad..00ecf23357dee4 100644 --- a/cont.c +++ b/cont.c @@ -299,8 +299,15 @@ static ID fiber_initialize_keywords[3] = {0}; */ #if defined(MAP_STACK) && !defined(__FreeBSD__) && !defined(__FreeBSD_kernel__) #define FIBER_STACK_FLAGS (MAP_PRIVATE | MAP_ANON | MAP_STACK) +#define FIBER_PROT_FLAGS (PROT_READ | PROT_WRITE) #else #define FIBER_STACK_FLAGS (MAP_PRIVATE | MAP_ANON) +#ifdef PROT_MAX +#define FIBER_BASE_PROT_FLAGS PROT_READ | PROT_WRITE +#define FIBER_PROT_FLAGS (FIBER_BASE_PROT_FLAGS | PROT_MAX(FIBER_BASE_PROT_FLAGS)) +#else +#define FIBER_PROT_FLAGS (PROT_READ | PROT_WRITE) +#endif #endif #define ERRNOMSG strerror(errno) @@ -487,7 +494,7 @@ fiber_pool_allocate_memory(size_t * count, size_t stride) #else errno = 0; size_t mmap_size = (*count)*stride; - void * base = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, FIBER_STACK_FLAGS, -1, 0); + void * base = mmap(NULL, mmap_size, FIBER_PROT_FLAGS, FIBER_STACK_FLAGS, -1, 0); if (base == MAP_FAILED) { // If the allocation fails, count = count / 2, and try again. From ae5a02b056ebb3040daf47d04e0ba3cafa5d2748 Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Sat, 12 Sep 2026 16:48:37 +0900 Subject: [PATCH 2/8] Free temporary `IO::Buffer` objects when I/O hooks raise. (#18666) --- scheduler.c | 85 ++++++++++++++++++++---- test/fiber/scheduler.rb | 25 +++++++ test/fiber/test_scheduler.rb | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 13 deletions(-) diff --git a/scheduler.c b/scheduler.c index 4cf7aa6620c241..d315b275fbad10 100644 --- a/scheduler.c +++ b/scheduler.c @@ -936,16 +936,45 @@ rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buf return rb_thread_io_blocking_operation(io, fiber_scheduler_io_pwrite, (VALUE)&arguments); } +struct fiber_scheduler_io_memory_arguments { + VALUE scheduler; + VALUE io; + VALUE buffer; + rb_off_t from; + size_t offset; + size_t length; +}; + +static VALUE +fiber_scheduler_io_read_memory(VALUE _arguments) +{ + struct fiber_scheduler_io_memory_arguments *arguments = (void *)_arguments; + + return rb_fiber_scheduler_io_read(arguments->scheduler, arguments->io, arguments->buffer, arguments->offset, arguments->length); +} + VALUE rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t size) { VALUE buffer = rb_io_buffer_new_locked(base, size, 0); - VALUE result = rb_fiber_scheduler_io_read(scheduler, io, buffer, 0, size); + struct fiber_scheduler_io_memory_arguments arguments = { + .scheduler = scheduler, + .io = io, + .buffer = buffer, + .offset = 0, + .length = size, + }; - rb_io_buffer_free_locked(buffer); + return rb_ensure(fiber_scheduler_io_read_memory, (VALUE)&arguments, rb_io_buffer_free_locked, buffer); +} - return result; +static VALUE +fiber_scheduler_io_write_memory(VALUE _arguments) +{ + struct fiber_scheduler_io_memory_arguments *arguments = (void *)_arguments; + + return rb_fiber_scheduler_io_write(arguments->scheduler, arguments->io, arguments->buffer, arguments->offset, arguments->length); } VALUE @@ -953,11 +982,23 @@ rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, { VALUE buffer = rb_io_buffer_new_locked((void*)base, size, RB_IO_BUFFER_READONLY); - VALUE result = rb_fiber_scheduler_io_write(scheduler, io, buffer, 0, size); + struct fiber_scheduler_io_memory_arguments arguments = { + .scheduler = scheduler, + .io = io, + .buffer = buffer, + .offset = 0, + .length = size, + }; + + return rb_ensure(fiber_scheduler_io_write_memory, (VALUE)&arguments, rb_io_buffer_free_locked, buffer); +} - rb_io_buffer_free_locked(buffer); +static VALUE +fiber_scheduler_io_pread_memory(VALUE _arguments) +{ + struct fiber_scheduler_io_memory_arguments *arguments = (void *)_arguments; - return result; + return rb_fiber_scheduler_io_pread(arguments->scheduler, arguments->io, arguments->from, arguments->buffer, arguments->offset, arguments->length); } VALUE @@ -965,11 +1006,24 @@ rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, voi { VALUE buffer = rb_io_buffer_new_locked(base, size, 0); - VALUE result = rb_fiber_scheduler_io_pread(scheduler, io, from, buffer, 0, size); + struct fiber_scheduler_io_memory_arguments arguments = { + .scheduler = scheduler, + .io = io, + .buffer = buffer, + .from = from, + .offset = 0, + .length = size, + }; + + return rb_ensure(fiber_scheduler_io_pread_memory, (VALUE)&arguments, rb_io_buffer_free_locked, buffer); +} - rb_io_buffer_free_locked(buffer); +static VALUE +fiber_scheduler_io_pwrite_memory(VALUE _arguments) +{ + struct fiber_scheduler_io_memory_arguments *arguments = (void *)_arguments; - return result; + return rb_fiber_scheduler_io_pwrite(arguments->scheduler, arguments->io, arguments->from, arguments->buffer, arguments->offset, arguments->length); } VALUE @@ -977,11 +1031,16 @@ rb_fiber_scheduler_io_pwrite_memory(VALUE scheduler, VALUE io, rb_off_t from, co { VALUE buffer = rb_io_buffer_new_locked((void*)base, size, RB_IO_BUFFER_READONLY); - VALUE result = rb_fiber_scheduler_io_pwrite(scheduler, io, from, buffer, 0, size); - - rb_io_buffer_free_locked(buffer); + struct fiber_scheduler_io_memory_arguments arguments = { + .scheduler = scheduler, + .io = io, + .buffer = buffer, + .from = from, + .offset = 0, + .length = size, + }; - return result; + return rb_ensure(fiber_scheduler_io_pwrite_memory, (VALUE)&arguments, rb_io_buffer_free_locked, buffer); } /* diff --git a/test/fiber/scheduler.rb b/test/fiber/scheduler.rb index 7158eadec74a33..af66ba106002e1 100644 --- a/test/fiber/scheduler.rb +++ b/test/fiber/scheduler.rb @@ -427,6 +427,31 @@ def io_write(io, buffer, offset, length) end end +class FailingIOScheduler < Scheduler + # Expose the last temporary IO::Buffer to test escaping scenarios. + attr_reader :buffer + + def io_read(io, buffer, offset, length) + @buffer = buffer + raise "scheduler read error" + end + + def io_write(io, buffer, offset, length) + @buffer = buffer + raise "scheduler write error" + end + + def io_pread(io, buffer, from, offset, length) + @buffer = buffer + raise "scheduler pread error" + end + + def io_pwrite(io, buffer, from, offset, length) + @buffer = buffer + raise "scheduler pwrite error" + end +end + # This scheduler has a broken implementation of `unblock`` in the sense that it # raises an exception. This is used to test the behavior of the scheduler when # unblock raises an exception. diff --git a/test/fiber/test_scheduler.rb b/test/fiber/test_scheduler.rb index aa65c5ce1a903d..32255731a48b11 100644 --- a/test/fiber/test_scheduler.rb +++ b/test/fiber/test_scheduler.rb @@ -397,4 +397,129 @@ def test_io_write_flush_error thread.kill rescue nil FileUtils.rm_f(path) end + + def test_io_read_exception_frees_temporary_buffer + r, w = IO.pipe + scheduler = FailingIOScheduler.new + error = nil + + thread = Thread.new do + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + begin + r.read(1) + rescue RuntimeError => exception + error = exception + end + end + + Fiber.set_scheduler(nil) + end + + thread.join + + assert_kind_of RuntimeError, error + assert_equal "scheduler read error", error.message + assert_predicate scheduler.buffer, :null? + assert_not_predicate scheduler.buffer, :locked? + ensure + thread&.kill + r&.close + w&.close + end + + def test_io_write_exception_frees_temporary_buffer + r, w = IO.pipe + w.sync = true + scheduler = FailingIOScheduler.new + error = nil + + thread = Thread.new do + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + begin + w.write("Hello World") + rescue RuntimeError => exception + error = exception + end + end + + Fiber.set_scheduler(nil) + end + + thread.join + + assert_kind_of RuntimeError, error + assert_equal "scheduler write error", error.message + assert_predicate scheduler.buffer, :null? + assert_not_predicate scheduler.buffer, :locked? + ensure + thread&.kill + r&.close + w&.close + end + + def test_io_pread_exception_frees_temporary_buffer + r, w = IO.pipe + scheduler = FailingIOScheduler.new + error = nil + + thread = Thread.new do + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + begin + r.pread(1, 0) + rescue RuntimeError => exception + error = exception + end + end + + Fiber.set_scheduler(nil) + end + + thread.join + + assert_kind_of RuntimeError, error + assert_equal "scheduler pread error", error.message + assert_predicate scheduler.buffer, :null? + assert_not_predicate scheduler.buffer, :locked? + ensure + thread&.kill + r&.close + w&.close + end + + def test_io_pwrite_exception_frees_temporary_buffer + r, w = IO.pipe + scheduler = FailingIOScheduler.new + error = nil + + thread = Thread.new do + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + begin + w.pwrite("Hello World", 0) + rescue RuntimeError => exception + error = exception + end + end + + Fiber.set_scheduler(nil) + end + + thread.join + + assert_kind_of RuntimeError, error + assert_equal "scheduler pwrite error", error.message + assert_predicate scheduler.buffer, :null? + assert_not_predicate scheduler.buffer, :locked? + ensure + thread&.kill + r&.close + w&.close + end end From dac86d0d354e33a8ea90376a7ef30e74872d7b46 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Sat, 12 Sep 2026 17:11:50 +0900 Subject: [PATCH 3/8] Remove the dependency from internal/cont.h to prism. (#13984) --- cont.c | 2 ++ eval.c | 2 +- internal/cont.h | 4 ---- internal/jit.h | 16 ++++++++++++++++ iseq.h | 1 - jit.c | 1 + ruby.c | 2 +- thread.c | 1 + vm.c | 1 + yjit.c | 2 +- yjit/src/cruby_bindings.inc.rs | 8 ++++---- zjit.c | 1 + zjit/src/cruby_bindings.inc.rs | 8 ++++---- 13 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 internal/jit.h diff --git a/cont.c b/cont.c index 00ecf23357dee4..b999db478ff4e1 100644 --- a/cont.c +++ b/cont.c @@ -21,6 +21,7 @@ #include "eval_intern.h" #include "internal.h" #include "internal/cont.h" +#include "internal/jit.h" #include "internal/thread.h" #include "internal/error.h" #include "internal/eval.h" @@ -29,6 +30,7 @@ #include "internal/sanitizers.h" #include "internal/vm_map.h" #include "internal/warnings.h" +#include "iseq.h" #include "ruby/fiber/scheduler.h" #include "yjit.h" #include "vm_core.h" diff --git a/eval.c b/eval.c index 15a8cad2f4e193..17a3f2743ff336 100644 --- a/eval.c +++ b/eval.c @@ -20,7 +20,7 @@ #include "eval_intern.h" #include "internal.h" #include "internal/class.h" -#include "internal/cont.h" +#include "internal/jit.h" #include "internal/error.h" #include "internal/eval.h" #include "internal/gc.h" diff --git a/internal/cont.h b/internal/cont.h index 6540dd272d4bc6..f9f200e420379b 100644 --- a/internal/cont.h +++ b/internal/cont.h @@ -9,7 +9,6 @@ * @brief Internal header for Fiber. */ #include "ruby/ruby.h" /* for VALUE */ -#include "iseq.h" struct rb_thread_struct; /* in vm_core.h */ struct rb_fiber_struct; /* in cont.c */ @@ -18,9 +17,6 @@ struct rb_execution_context_struct; /* in vm_core.c */ /* cont.c */ void rb_fiber_reset_root_local_storage(struct rb_thread_struct *); void ruby_register_rollback_func_for_ensure(VALUE (*ensure_func)(VALUE), VALUE (*rollback_func)(VALUE)); -void rb_jit_cont_init(void); -void rb_jit_cont_each_iseq(rb_iseq_callback callback, void *data); -void rb_jit_cont_finish(void); /* vm.c */ void rb_free_shared_fiber_pool(void); diff --git a/internal/jit.h b/internal/jit.h new file mode 100644 index 00000000000000..5e2f3db9f4cf2d --- /dev/null +++ b/internal/jit.h @@ -0,0 +1,16 @@ +#ifndef INTERNAL_JIT_H /*-*-C-*-vi:se ft=c:*/ +#define INTERNAL_JIT_H + +#include "iseq.h" + +typedef void (*rb_iseq_callback)(const rb_iseq_t *, void *); + +/* cont.c */ +void rb_jit_cont_init(void); +void rb_jit_cont_each_iseq(rb_iseq_callback callback, void *data); +void rb_jit_cont_finish(void); + +/* jit.c */ +void rb_jit_for_each_iseq(rb_iseq_callback callback, void *data); + +#endif /* INTERNAL_JIT_H */ diff --git a/iseq.h b/iseq.h index 7c68038c15c1f7..7569ae4dfd13d5 100644 --- a/iseq.h +++ b/iseq.h @@ -55,7 +55,6 @@ iseq_lvar_state_set(uint8_t *buf, unsigned int i, enum lvar_state state) typedef struct rb_iseq_struct rb_iseq_t; #define rb_iseq_t rb_iseq_t #endif -typedef void (*rb_iseq_callback)(const rb_iseq_t *, void *); extern const ID rb_iseq_shared_exc_local_tbl[]; diff --git a/jit.c b/jit.c index 24b7e1956a4171..df9accf87f0802 100644 --- a/jit.c +++ b/jit.c @@ -14,6 +14,7 @@ #include "iseq.h" #include "internal/compile.h" #include "internal/gc.h" +#include "internal/jit.h" #include "vm_sync.h" #include "internal/fixnum.h" #include "internal/hash.h" diff --git a/ruby.c b/ruby.c index 3d7ef4ff993968..0f8f1e3db90563 100644 --- a/ruby.c +++ b/ruby.c @@ -44,7 +44,6 @@ #include "eval_intern.h" #include "internal.h" #include "internal/cmdlineopt.h" -#include "internal/cont.h" #include "internal/coverage.h" #include "internal/error.h" #include "internal/file.h" @@ -57,6 +56,7 @@ #include "internal/thread.h" #include "internal/ruby_parser.h" #include "internal/variable.h" +#include "prism_compile.h" #include "ruby/encoding.h" #include "ruby/thread.h" #include "ruby/util.h" diff --git a/thread.c b/thread.c index 56e7c2b5c15fd3..5de075cf73ad9a 100644 --- a/thread.c +++ b/thread.c @@ -77,6 +77,7 @@ #include "internal.h" #include "internal/class.h" #include "internal/cont.h" +#include "internal/jit.h" #include "internal/coverage.h" #include "internal/error.h" #include "internal/eval.h" diff --git a/vm.c b/vm.c index 0efad702883cec..f9d9d8ef0272c8 100644 --- a/vm.c +++ b/vm.c @@ -21,6 +21,7 @@ #include "internal/eval.h" #include "internal/gc.h" #include "internal/inits.h" +#include "internal/jit.h" #include "internal/missing.h" #include "internal/object.h" #include "internal/proc.h" diff --git a/yjit.c b/yjit.c index 5e08b31afc880b..cf0ff15ebf006e 100644 --- a/yjit.c +++ b/yjit.c @@ -29,6 +29,7 @@ #include "iseq.h" #include "ruby/debug.h" #include "internal/cont.h" +#include "internal/jit.h" // For mmapp(), sysconf() #ifndef _WIN32 @@ -523,4 +524,3 @@ static VALUE yjit_c_builtin_p(rb_execution_context_t *ec, VALUE self) { return Q // Preprocessed yjit.rb generated during build #include "yjit.rbinc" - diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 88d9c043364f65..c4f7d85ed283e3 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1039,9 +1039,6 @@ pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 257; pub const YARVINSN_zjit_opt_regexpmatch2: ruby_vminsn_type = 258; pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 259; pub type ruby_vminsn_type = u32; -pub type rb_iseq_callback = ::std::option::Option< - unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), ->; pub const DEFINED_NOT_DEFINED: defined_type = 0; pub const DEFINED_NIL: defined_type = 1; pub const DEFINED_IVAR: defined_type = 2; @@ -1061,6 +1058,9 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; +pub type rb_iseq_callback = ::std::option::Option< + unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), +>; pub const YJIT_ISEQ_TRANSLATED: yjit_bindgen_constants = 1048576; pub type yjit_bindgen_constants = u32; pub type rb_seq_param_keyword_struct = @@ -1253,6 +1253,7 @@ extern "C" { lines: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; pub fn rb_jit_cont_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); + pub fn rb_jit_for_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); pub fn rb_yjit_exit_locations_dict( yjit_raw_samples: *mut VALUE, yjit_line_samples: *mut ::std::os::raw::c_int, @@ -1412,7 +1413,6 @@ extern "C" { pub fn rb_iseq_reset_jit_func(iseq: *const rb_iseq_t); pub fn rb_jit_get_page_size() -> u32; pub fn rb_jit_reserve_addr_space(mem_size: u32) -> *mut u8; - pub fn rb_jit_for_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); pub fn rb_jit_mark_writable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; pub fn rb_jit_mark_executable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32); pub fn rb_jit_mark_unused(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; diff --git a/zjit.c b/zjit.c index f22c9299c21a29..9ae0ea6283a71b 100644 --- a/zjit.c +++ b/zjit.c @@ -23,6 +23,7 @@ #include "iseq.h" #include "ruby/debug.h" #include "internal/cont.h" +#include "internal/jit.h" #include "ractor_core.h" #include "shape.h" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index d235b7071e490e..023353ba18695a 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2014,9 +2014,6 @@ pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 257; pub const YARVINSN_zjit_opt_regexpmatch2: ruby_vminsn_type = 258; pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 259; pub type ruby_vminsn_type = u32; -pub type rb_iseq_callback = ::std::option::Option< - unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), ->; #[repr(C)] #[repr(align(8))] #[derive(Debug, Copy, Clone)] @@ -2105,6 +2102,9 @@ pub struct zjit_jit_frame { pub stack_size: u32, pub stack: __IncompleteArrayField, } +pub type rb_iseq_callback = ::std::option::Option< + unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), +>; pub const ISEQ_BODY_OFFSET_PARAM: zjit_struct_offsets = 16; pub const ISEQ_BODY_OFFSET_OUTER_VARIABLES: zjit_struct_offsets = 240; pub const RUBY_OFFSET_THREAD_RACTOR: zjit_struct_offsets = 24; @@ -2450,6 +2450,7 @@ unsafe extern "C" { pub fn rb_profile_frame_absolute_path(frame: VALUE) -> VALUE; pub fn rb_profile_frame_full_label(frame: VALUE) -> VALUE; pub fn rb_jit_cont_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); + pub fn rb_jit_for_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); pub static rb_zjit_runtime_offsets: rb_zjit_runtime_offsets; pub fn rb_zjit_reserve_low_addr_space(size: usize) -> *mut ::std::os::raw::c_void; pub fn rb_zjit_profile_disable(iseq: *const rb_iseq_t); @@ -2599,7 +2600,6 @@ unsafe extern "C" { pub fn rb_iseq_reset_jit_func(iseq: *const rb_iseq_t); pub fn rb_jit_get_page_size() -> u32; pub fn rb_jit_reserve_addr_space(mem_size: u32) -> *mut u8; - pub fn rb_jit_for_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); pub fn rb_jit_mark_writable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; pub fn rb_jit_mark_executable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32); pub fn rb_jit_mark_unused(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; From 43405ac6f2fc8b59087e39230a0c1fbbfa927aca Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 4 Sep 2026 07:59:23 +0900 Subject: [PATCH 4/8] Translate SO_ERROR into an errno on Windows Winsock leaves a WSA error code in SO_ERROR, while the socket library reads it as an errno everywhere. wait_connectable() compares it with ECONNREFUSED and friends, and Socket.tcp hands it to SystemCallError, so a connection refused on Windows surfaced as a bare SystemCallError rather than Errno::ECONNREFUSED. Co-Authored-By: Claude Opus 5 --- NEWS.md | 10 ++++++++++ test/socket/test_socket.rb | 3 +-- win32/win32.c | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 02db0c2c33f3a0..815368b54080eb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -305,6 +305,15 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det process starts, from the `USER` or `USERNAME` environment variable or `GetUserName()`. It used to follow later changes to `ENV['USER']`. +* Socket + + * On Windows, `BasicSocket#getsockopt(:SOCKET, :ERROR)` now reports an + errno as it does on the other platforms, instead of the raw WinSock + error code. Code comparing it with a `WSAE*` value has to compare it + with the matching `Errno::*::Errno` instead. + + [[Bug #18661]] + ## C API updates ### Embedded TypedData @@ -421,6 +430,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT +[Bug #18661]: https://bugs.ruby-lang.org/issues/18661 [Bug #18947]: https://bugs.ruby-lang.org/issues/18947 [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #9779]: https://bugs.ruby-lang.org/issues/9779 diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index b840e92c686850..49a23b330a6b9f 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -1029,8 +1029,7 @@ def test_tcp_socket_hostname_resolution_failed_after_connection_failure server.close - # SystemCallError is a workaround for Windows environment - assert_raise(Errno::ECONNREFUSED, SystemCallError) do + assert_raise(Errno::ECONNREFUSED) do Socket.tcp("localhost", port) end RUBY diff --git a/win32/win32.c b/win32/win32.c index 59e875ac903747..e4c27472a805d4 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -3488,6 +3488,15 @@ rb_w32_getsockopt(int s, int level, int optname, char *optval, int *optlen) if (r == SOCKET_ERROR) errno = map_errno(WSAGetLastError()); } + /* Winsock leaves a WSA error code in SO_ERROR, but the callers expect + * an errno as on the other platforms. [Bug #18661] */ + if (r == 0 && level == SOL_SOCKET && optname == SO_ERROR && + *optlen == (int)sizeof(int)) { + int sockerr; + memcpy(&sockerr, optval, sizeof(sockerr)); + sockerr = map_errno(sockerr); + memcpy(optval, &sockerr, sizeof(sockerr)); + } return r; } From f1af5e5d902d167c97c580a139fb4105c57016b1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 4 Sep 2026 07:59:34 +0900 Subject: [PATCH 5/8] Wait for the exceptfds of a connecting socket on Windows Winsock reports a failed non-blocking connect(2) only through the exceptfds of select(2), which IO#wait_writable never asks for. So Addrinfo#connect_internal kept waiting on a socket whose connection had already been refused, and burned the whole connect_timeout before raising Errno::ETIMEDOUT instead of the real error. Ask for the priority event on Windows alone. The M:N thread scheduler refuses any wait carrying another event, so requesting it where it cannot fire would take the wait off that scheduler for nothing. Co-Authored-By: Claude Opus 5 --- NEWS.md | 5 +++++ ext/socket/lib/socket.rb | 12 +++++++++++- test/socket/test_socket.rb | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 815368b54080eb..8f9dd8900d6ef1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -307,6 +307,11 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det * Socket + * On Windows, a connection that is refused or unreachable now raises the + matching `Errno` class as soon as Winsock reports it, instead of + `Errno::ETIMEDOUT` once the whole `connect_timeout` has passed. Code + rescuing `Errno::ETIMEDOUT` there has to rescue the real error instead. + * On Windows, `BasicSocket#getsockopt(:SOCKET, :ERROR)` now reports an errno as it does on the other platforms, instead of the raw WinSock error code. Code comparing it with a `WSAE*` value has to compare it diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb index 0ade75c2fc028f..d2a601e5d7eea5 100644 --- a/ext/socket/lib/socket.rb +++ b/ext/socket/lib/socket.rb @@ -3,6 +3,16 @@ require 'socket.so' class Addrinfo + # :stopdoc: + # Windows signals a failed connect(2) through the exceptfds of select(2) + # only. Elsewhere the priority event would just take the wait off the M:N + # thread scheduler, which refuses any event but readable and writable. + # [Bug #18661] + CONNECT_EVENTS = IO::WRITABLE | + (/mswin|mingw|cygwin/.match?(RUBY_PLATFORM) ? IO::PRIORITY : 0) + private_constant :CONNECT_EVENTS + # :startdoc: + # creates an Addrinfo object from the arguments. # # The arguments are interpreted as similar to self. @@ -56,7 +66,7 @@ def connect_internal(local_addrinfo, timeout=nil) # :yields: socket when 0 # success or EISCONN, other errors raise break when :wait_writable - sock.wait_writable(timeout) or + sock.wait(CONNECT_EVENTS, timeout) or raise Errno::ETIMEDOUT, "user specified timeout for #{self.ip_address}:#{self.ip_port}" # Check SO_ERROR instead of relying on the connect_nonblock retry; # some kernels (e.g. Darwin 27) answer the retry connect(2) with diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index 49a23b330a6b9f..59bcc918476a49 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -612,7 +612,7 @@ def test_connect_timeout_connection_refused assert_raise(Errno::ECONNREFUSED) do Socket.tcp("127.0.0.1", port, connect_timeout: 5) end - end unless /mswin|mingw/ =~ RUBY_PLATFORM + end def test_getifaddrs begin From 35f46c051f9bccc03ea1fc8a27ee0243ec7a4fce Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 12 Sep 2026 09:31:29 +0900 Subject: [PATCH 6/8] Create internal/set.h for set.c function definitions --- array.c | 3 +-- internal/set.h | 16 ++++++++++++++++ set.c | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 internal/set.h diff --git a/array.c b/array.c index f0d50502506d96..abaf5184e92cad 100644 --- a/array.c +++ b/array.c @@ -23,6 +23,7 @@ #include "internal/object.h" #include "internal/proc.h" #include "internal/rational.h" +#include "internal/set.h" #include "internal/string.h" #include "internal/vm.h" #include "probes.h" @@ -6751,8 +6752,6 @@ rb_ary_count(int argc, VALUE *argv, VALUE ary) return LONG2NUM(n); } -VALUE rb_ident_set_new(void); - static VALUE flatten(VALUE ary, int level) { diff --git a/internal/set.h b/internal/set.h new file mode 100644 index 00000000000000..48c282c3d4e23d --- /dev/null +++ b/internal/set.h @@ -0,0 +1,16 @@ +#ifndef INTERNAL_SET_H /*-*-C-*-vi:se ft=c:*/ +#define INTERNAL_SET_H +/** + * @author Ruby developers + * @copyright This file is a part of the programming language Ruby. + * Permission is hereby granted, to either redistribute and/or + * modify this file, provided that the conditions mentioned in the + * file COPYING are met. Consult the file for details. + * @brief Internal header for Set. + */ +#include "ruby/internal/config.h" +#include "ruby/ruby.h" + +VALUE rb_ident_set_new(void); + +#endif /* INTERNAL_SET_H */ diff --git a/set.c b/set.c index 6954342ad8af6f..1c19c2ca81f057 100644 --- a/set.c +++ b/set.c @@ -12,6 +12,7 @@ #include "internal/object.h" #include "internal/proc.h" #include "internal/sanitizers.h" +#include "internal/set.h" #include "internal/set_table.h" #include "internal/symbol.h" #include "internal/variable.h" From 578c46252412f23ec4a009f90f4dead5552e8858 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 12 Sep 2026 13:25:37 +0200 Subject: [PATCH 7/8] Update to ruby/spec@aae6309 --- spec/ruby/core/method/syntax_tree_spec.rb | 3 ++- spec/ruby/core/proc/syntax_tree_spec.rb | 3 ++- spec/ruby/core/string/tr_spec.rb | 13 +++++++++++-- .../thread/backtrace/location/source_range_spec.rb | 7 +++---- .../thread/backtrace/location/syntax_tree_spec.rb | 3 ++- spec/ruby/core/unboundmethod/syntax_tree_spec.rb | 3 ++- spec/ruby/fixtures/source_range_helpers.rb | 14 +++++++++++++- 7 files changed, 35 insertions(+), 11 deletions(-) diff --git a/spec/ruby/core/method/syntax_tree_spec.rb b/spec/ruby/core/method/syntax_tree_spec.rb index 04dfd0f30b88db..9d243bde493d9c 100644 --- a/spec/ruby/core/method/syntax_tree_spec.rb +++ b/spec/ruby/core/method/syntax_tree_spec.rb @@ -1,4 +1,5 @@ require_relative '../../spec_helper' +require_relative '../../fixtures/source_range_helpers' require_relative 'shared/syntax_tree' ruby_version_is "4.1" do @@ -6,7 +7,7 @@ before :each do @object = -> method { method } - skip "parse.y" if method(:it).syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + skip "parse.y" unless syntax_tree_returns_prism_node end it_behaves_like :method_syntax_tree, :syntax_tree diff --git a/spec/ruby/core/proc/syntax_tree_spec.rb b/spec/ruby/core/proc/syntax_tree_spec.rb index 9ad89ee44bad5b..aed9aea778498c 100644 --- a/spec/ruby/core/proc/syntax_tree_spec.rb +++ b/spec/ruby/core/proc/syntax_tree_spec.rb @@ -1,9 +1,10 @@ require_relative '../../spec_helper' +require_relative '../../fixtures/source_range_helpers' ruby_version_is "4.1" do describe "Proc#syntax_tree" do before :each do - skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + skip "parse.y" unless syntax_tree_returns_prism_node end def return_block(&b) diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb index d806898b50f701..0b868caf7ce636 100644 --- a/spec/ruby/core/string/tr_spec.rb +++ b/spec/ruby/core/string/tr_spec.rb @@ -39,10 +39,19 @@ it "raises an ArgumentError when given wrong number of arguments" do -> { "hello".tr }.should.raise(ArgumentError) - ruby_version_is ""..."4.1" do + -> { "hello".tr("a", "b", "c") }.should.raise(ArgumentError) + end + + ruby_version_is ""..."4.1" do + it "raises an ArgumentError when given 1 argument" do -> { "hello".tr("a") }.should.raise(ArgumentError) end - -> { "hello".tr("a", "b", "c") }.should.raise(ArgumentError) + end + + ruby_version_is "4.1" do + it "raises an ArgumentError when given 1 argument" do + -> { "hello".tr("a") }.should.raise(TypeError) + end end it "raises an ArgumentError when the replacement contains a descending range" do diff --git a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb index 530e12480b766a..a5a3980c6a307a 100644 --- a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb @@ -3,10 +3,6 @@ ruby_version_is "4.1" do describe "Thread::Backtrace::Location#source_range" do - before do - skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) - end - it "returns a Ruby::SourceRange with the location paths" do location, range, path, absolute_path = capture_backtrace_location_source_range(<<-RUBY, :CallNode) $nil.foo$ @@ -380,6 +376,9 @@ def value.to_s RUBY }.each_pair do |description, (source, prism_class, frame)| it "returns the precise range for #{description}" do + # Currently fails with parse.y, needs to be fixed + skip "parse.y" if description == "top-level constant operator assignments" && !syntax_tree_returns_prism_node + capture_backtrace_location_source_range(source, prism_class, frame: frame || 0) end end diff --git a/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb b/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb index 0c7580edaec976..3b651a795f2a40 100644 --- a/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/syntax_tree_spec.rb @@ -1,11 +1,12 @@ require_relative '../../../../spec_helper' +require_relative '../../../../fixtures/source_range_helpers' ruby_version_is "4.1" do describe "Thread::Backtrace::Location#syntax_tree" do # This is tested more extensively in core/thread/backtrace/location/source_range_spec.rb before do - skip "parse.y" if proc {}.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + skip "parse.y" unless syntax_tree_returns_prism_node end it "returns a CallNode for the first location from caller_locations" do diff --git a/spec/ruby/core/unboundmethod/syntax_tree_spec.rb b/spec/ruby/core/unboundmethod/syntax_tree_spec.rb index f1239effd880e6..da1f273086e339 100644 --- a/spec/ruby/core/unboundmethod/syntax_tree_spec.rb +++ b/spec/ruby/core/unboundmethod/syntax_tree_spec.rb @@ -1,4 +1,5 @@ require_relative '../../spec_helper' +require_relative '../../fixtures/source_range_helpers' require_relative '../method/shared/syntax_tree' ruby_version_is "4.1" do @@ -6,7 +7,7 @@ before :each do @object = -> method { method.unbind } - skip "parse.y" if method(:it).unbind.syntax_tree.is_a?(RubyVM::AbstractSyntaxTree::Node) + skip "parse.y" unless syntax_tree_returns_prism_node end it_behaves_like :method_syntax_tree, :syntax_tree diff --git a/spec/ruby/fixtures/source_range_helpers.rb b/spec/ruby/fixtures/source_range_helpers.rb index 5e73a391f8f983..ba8cfc0fadab01 100644 --- a/spec/ruby/fixtures/source_range_helpers.rb +++ b/spec/ruby/fixtures/source_range_helpers.rb @@ -23,6 +23,18 @@ def keep_source(value = true) end end +# #syntax_tree on CRuby currently returns RubyVM::AbstractSyntaxTree::Node with --parser=parse.y. +# We must not refer to RubyVM in ruby/spec as it only exists on CRuby. +def syntax_tree_returns_prism_node + receiver = -> {} + if receiver.respond_to?(:syntax_tree) + node = receiver.syntax_tree + defined?(Prism) && node.is_a?(Prism::Node) + else + true + end +end + def source_range_source(source) raise "Expected 2 '$' to mark start and end of source_range" unless source.count('$') == 2 from = source.byteindex('$') @@ -89,7 +101,7 @@ def capture_backtrace_location_source_range(marked_source, prism_class, frame: 0 expected_class_name = "Prism::#{prism_class or raise "prism_class must be passed"}" # Also check #syntax_tree is consistent - if location.respond_to?(:syntax_tree) + if location.respond_to?(:syntax_tree) && syntax_tree_returns_prism_node node = location.syntax_tree node.class.name.should == expected_class_name source_range_values(node).should == expected From b2ec82d4ea84f5526b3759923159a039bc5c1d3b Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 12 Sep 2026 13:39:50 +0200 Subject: [PATCH 8/8] Cover the whole `Const op= value` expression in the constant read node in parse.y For `Const += value`, parse.y builds a NODE_CDECL whose value is an OPCALL on a NODE_CONST read node, and that read node only spanned the constant name. The NameError for an undefined constant is raised by that read, so Thread::Backtrace::Location#source_range reported only the constant name. Prism has no separate read node and reports the whole ConstantOperatorWriteNode, and the same holds for ConstantAndWriteNode. Give the NODE_CONST read node the location of the whole operator assignment (`+=`, `&&=` and `||=`) so both parsers report the same range. Other variable reads keep their own location as error_highlight locates the operator from the end of the receiver node. See [Bug #22235] for more details. Co-Authored-By: Claude Fable 5.1 --- parse.y | 12 +++++++++--- .../thread/backtrace/location/source_range_spec.rb | 3 --- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/parse.y b/parse.y index 416a1bcc917e33..545cf17b18e1f2 100644 --- a/parse.y +++ b/parse.y @@ -14826,19 +14826,25 @@ new_op_assign(struct parser_params *p, NODE *lhs, ID op, NODE *rhs, struct lex_c if (lhs) { ID vid = get_nd_vid(p, lhs); YYLTYPE lhs_loc = lhs->nd_loc; + /* A constant read can raise NameError. Prism has no separate read node + * for `Const op= value` and reports the whole expression, so give the + * NODE_CONST the location of the whole operator assignment as well, + * for consistent Thread::Backtrace::Location#source_range between both + * parsers. */ + const YYLTYPE *read_loc = nd_type_p(lhs, NODE_CDECL) ? loc : &lhs_loc; if (op == tOROP) { set_nd_value(p, lhs, rhs); nd_set_loc(lhs, loc); - asgn = NEW_OP_ASGN_OR(gettable(p, vid, &lhs_loc), lhs, loc); + asgn = NEW_OP_ASGN_OR(gettable(p, vid, read_loc), lhs, loc); } else if (op == tANDOP) { set_nd_value(p, lhs, rhs); nd_set_loc(lhs, loc); - asgn = NEW_OP_ASGN_AND(gettable(p, vid, &lhs_loc), lhs, loc); + asgn = NEW_OP_ASGN_AND(gettable(p, vid, read_loc), lhs, loc); } else { asgn = lhs; - rhs = NEW_CALL(gettable(p, vid, &lhs_loc), op, NEW_LIST(rhs, &rhs->nd_loc), loc); + rhs = NEW_CALL(gettable(p, vid, read_loc), op, NEW_LIST(rhs, &rhs->nd_loc), loc); set_nd_value(p, asgn, rhs); nd_set_loc(asgn, loc); } diff --git a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb index a5a3980c6a307a..24683602406551 100644 --- a/spec/ruby/core/thread/backtrace/location/source_range_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/source_range_spec.rb @@ -376,9 +376,6 @@ def value.to_s RUBY }.each_pair do |description, (source, prism_class, frame)| it "returns the precise range for #{description}" do - # Currently fails with parse.y, needs to be fixed - skip "parse.y" if description == "top-level constant operator assignments" && !syntax_tree_returns_prism_node - capture_backtrace_location_source_range(source, prism_class, frame: frame || 0) end end