From 2f72b3d142f4b66c3dfba4a7b49aaa101f9780cd Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 6 Sep 2026 08:27:55 +0200 Subject: [PATCH 1/4] struct.c: Strop reserving extra space for ivars [Bug #22292] This was a leftover 6f339aebfe9b233304ea98f53274a48f614e7aea --- struct.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/struct.c b/struct.c index 84a0940ba7ae85..8bc1694700bdc5 100644 --- a/struct.c +++ b/struct.c @@ -831,12 +831,7 @@ struct_alloc(VALUE klass) { long n = num_members(klass); size_t embedded_size = offsetof(struct RStruct, as.ary) + (sizeof(VALUE) * n); - if (RCLASS_MAX_IV_COUNT(klass) > 0) { - embedded_size += sizeof(VALUE); - } - VALUE flags = T_STRUCT; - const long embed_len_max = RSTRUCT_EMBED_LEN_MASK >> RSTRUCT_EMBED_LEN_SHIFT; if (n > 0 && n <= embed_len_max && rb_gc_size_allocatable_p(embedded_size)) { From 13fc4ffd325b2704686e2703b0a7487afd22efe6 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sun, 6 Sep 2026 06:22:48 -0500 Subject: [PATCH 2/4] [DOC] New symbolic links doc- #18619 --- doc/file/symbolic_links.md | 289 +++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 doc/file/symbolic_links.md diff --git a/doc/file/symbolic_links.md b/doc/file/symbolic_links.md new file mode 100644 index 00000000000000..74df11f5d8111b --- /dev/null +++ b/doc/file/symbolic_links.md @@ -0,0 +1,289 @@ +# Symbolic Links + +A [symbolic link][symbolic link] (sometimes called a _symlink_ or _soft link_) +is a filesystem entry that stores a filesystem path. +The stored path refers to a _target_ filesystem entry, which may or may not exist. +Further, the stored path need not even be a valid path. + +## Methods That Follow Symlinks + +Most Ruby methods that deal with filesystem paths "follow" symbolic links; +that is, if the path refers to a symlink, the method does not operate on the entry +at that path, but instead operates on the path stored in the symlink: + +```ruby +File.symlink('README.md', 'foo') +File.read('foo').size # => 3463 # Size of README.md, not foo. +File.delete('foo') +``` + +## Creating Symlinks + +Each of these methods creates a symlink: + +- File::symlink +- FileUtils::ln_s +- Pathname#make_symlink + +If the target path is itself a symlink, that symlink is not followed. + +Examples: + +```ruby +# Symlink for a file. +File.symlink('README.md', 'foo') +# Symlink for another symlink. +File.symlink('foo', 'bar') +File.read('README.md').size # => 3463 +File.read('foo').size # => 3463 +File.read('bar').size # => 3463 +# Symlink for a directory. +File.symlink('doc/', 'baz') +Dir.entries('doc/').size # => 33 +Dir.entries('baz').size # => 33 +File.unlink('foo', 'bar', 'baz') # Clean up. +``` + +## Querying Symlinks + +### `lstat` + +Each of these methods creates a File::Stat object for a filesystem entry: + +- File::lstat +- Pathname#lstat + +The object contains information for the entry at the given path, +even if that entry is a symlink; +i.e., symlinks are not followed. + +By contrast, each of the methods File::stat and Pathname#stat +_do_ follow symlinks. + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', linkpath) +File::stat(linkpath).size # => 3469 # Size of file README.md. +File::lstat(linkpath).size # => 9 # Size of symlink linkpath. +File.unlink(linkpath) # Clean up. +``` + +### `symlink?` + +Each of these methods returns whether the entry at a given path is a symlink: + +- File::symlink? +- File::Stat#symlink? +- Pathname#symlink? + +If the entry is a symlink, it is not followed. + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', 'foo') +File.symlink?(linkpath) # => true +File.symlink?('README.md') # => false +File.symlink?('nosuch') # => false +File.delete(linkpath) # Clean up. +``` + +### `readlink` + +Each of these methods returns the path stored in a symlink: + +- File::readlink +- Pathname#readlink + +Examples: + +```ruby +linkpath = 'foo' +File.symlink('README.md', 'foo') +File.readlink(linkpath) # => "README.md" +File.delete(linkpath) # Clean up. +``` + +## Modifying Symlinks + +### `lchmod` + +Each of these methods changes the mode of the symlink entry: + +- File::lchmod +- Pathname#lchmod + +These methods are not supported on Windows or Linux (raise NotImplementedError). + +### `lchown` + +Each of these methods changes the ownership of a symlink entry: + +- File::lchown +- Pathname#lchown + +Example: + +```ruby +# Super user; all privileges. +Process.uid # => 0 +Process.gid # => 0 +# Create regular file and symlink to it. +filepath = 't.tmp' +linkpath = 'foo' +File.write(filepath, '') +File.symlink(filepath, linkpath) +# Capture original statuses. +fstat0 = File.stat(filepath) +lstat0 = File.lstat(linkpath) +fstat0.uid # => 0 +fstat0.gid # => 0 +lstat0.uid # => 0 +lstat0.gid # => 0 +# Change owner for the symlink. +File.lchown(1000, 1000, linkpath) +# Capture new statuses. +fstat1 = File.stat(filepath) +lstat1 = File.lstat(linkpath) +# User id and group id for file not changed. +fstat1.uid # => 0 +fstat1.gid # => 0 +# User id and group id for link changed. +lstat1.uid # => 1000 +lstat1.gid # => 1000 +# Clean up. +File.delete(filepath, linkpath) +``` + +### `lutime` + +Each of these methods updates timestamps for a symlink entry: + +- File::lutime +- Pathname#lutime + +Example: + +```ruby +filepath = 'README.md' +linkpath = 'foo' +File.symlink(filepath, linkpath) +# Take snapshots of both. +fstat0 = File.stat(filepath) +lstat0 = File.lstat(linkpath) +# Fetch access times and modification times of both. +fstat0.atime # => 2026-09-03 07:46:04.940377552 -0500 +fstat0.mtime # => 2026-09-01 09:09:28.378987388 -0500 +lstat0.atime # => 2026-09-03 09:14:13.753865727 -0500 +lstat0.mtime # => 2026-09-03 09:14:13.753865727 -0500 +# Update access time and modification time of the symlink. +time = Time.now # => 2026-09-03 09:16:42.619702232 -0500 +File.lutime(time, time, linkpath) +# Take fresh snapshots of both. +fstat1 = File.stat(filepath) +lstat1 = File.lstat(linkpath) +# Fetch access time and modification time of file (not changed). +fstat1.atime # => 2026-09-03 07:46:04.940377552 -0500 +fstat1.mtime # => 2026-09-01 09:09:28.378987388 -0500 +# Fetch access time and modification time of link (changed). +lstat1.atime # => 2026-09-03 09:16:52.77029217 -0500 +lstat1.mtime # => 2026-09-03 09:16:42.619702232 -0500 +# Clean up. +File.delete(linkpath) +``` + +### `rename` + +Each of these methods changes the name of an entry (which need not be a symlink): + +- File::rename +- Pathname#rename + +Examples: + +```ruby +File.symlink('README.md', 'foo') +File.rename('foo', 'bar') +File.symlink?('bar') # => true +File.rename('bar', 'baz') +File.symlink?('baz') # => true +File.delete('baz') # Clean up. +``` + +## Removing Symlinks + +### `unlink` + +Each of these methods removes an entry (which need not be a symlink): + +- File::delete +- File::unlink +- Pathname#unlink (aliased as Pathname#delete) + +Example: + +```ruby +linkpath = 'foo' +File.symlink('README.md', linkpath) +File.unlink(linkpath) +``` + +## Methods That Don't Follow Symlinks + +Sometimes it's necessary to query, modify, or delete a symlink; +therefore certain Ruby methods do not follow symlinks, +but instead operate directly on the symlinks. + +### Symlink-Specific Methods + +Each method in the table below has a symlink-specific purpose. + +| Method Name | Effect | +|-----------------------|-----------------------------------------------------| +| File::Stat#symlink? | Returns whether a path is a symlink. | +| File::lchmod | Changes the mode of the symlink. See Note 1. | +| File::lchown | Changes the ownership of the symlink. See Note 2. | +| File::lstat | Creates a File::Stat object for a link. See Note 3. | +| File::lutime | Updates timestamps for the symlink. See Note 4. | +| File::readlink | Returns the path stored in a symlink. | +| File::symlink | Creates a symlink. | +| File::symlink? | Returns whether a path is a symlink. | +| FileUtils::ln_s | Creates a symlink. | +| FileUtils::ln_sf | Creates a symlink. | +| FileUtils::ln_sr | Creates a symlink. | +| Pathname#lchmod | Changes the mode of the symlink. See Note 1. | +| Pathname#lchown | Changes the ownership of the symlink. See Note 2. | +| Pathname#lstat | Creates a File::Stat object for a link. See Note 3. | +| Pathname#lutime | Updates timestamps for the symlink. See Note 4. | +| Pathname#make_symlink | Creates a symlink. | +| Pathname#readlink | Returns the path stored in a symlink. | +| Pathname#symlink? | Returns whether a path is a symlink. | + +Notes: + +1. File::chmod and Pathname#chmod follow symlinks before changing the mode. +1. File::chown and Pathname#chown follow symlinks before changing the ownership. +1. File::stat and Pathname#stat follow symlinks before creating the File::Stat object. +1. File::utime and Pathname#utime follow symlinks before updating timestamps. + +### Other Non-Following Methods + +The methods in the table below do not follow symlinks, +but instead operate directly on the entry at the path +(which may or may not be a symlink). + +| Method | Effect | +|----------------------------------------------|----------------------------------| +| File::delete | Removes the entry. | +| File::link | Creates a hard link. | +| File::rename | Changes the name of the entry. | +| File::unlink | Removes the entry. | +| FileUtils::link_entry | Creates a hard link. | +| FileUtils::ln (aliased as FileUtils.link) | Creates a hard link. | +| Pathname#rename | Changes the name of the entry. | +| Pathname#unlink (aliased as Pathname#delete) | Removes the entry. | + +[symbolic link]: https://en.wikipedia.org/wiki/Symbolic_link From bb1d3236349b2dcf80d82276cd1a60ecaead44d4 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 6 Sep 2026 20:21:53 +0900 Subject: [PATCH 3/4] [DOC] Fix error raised for lchmod When lchmod isn't available, it raises NotImplementedError. --- file.c | 2 +- pathname_builtin.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index cd206a692bb1cf..167cc32933b823 100644 --- a/file.c +++ b/file.c @@ -3119,7 +3119,7 @@ lchmod_internal(const char *path, void *mode) * call-seq: * File.lchmod(mode, *paths) -> paths_count * - * Not supported on some platforms (raises Errno:: ENOTSUP). + * Not supported on some platforms (raises NotImplementedError). * * When supported: like File::chmod, but does not follow symbolic links, * and therefore changes the mode of the entries given by `paths`; diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 79264a18882f06..39ff95fd3b47c0 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1543,7 +1543,7 @@ def chmod(mode) File.chmod(mode, @path) end # call-seq: # lchmod(mode) -> 1 # - # Not supported on some platforms (raises Errno::ENOTSUP). + # Not supported on some platforms (raises NotImplementedError). # # When supported: like Pathname::chmod, but does not follow symbolic links, # and therefore changes the mode of the entry specified by `self`: From e536482403c19e68366c359a593e99e9514758d8 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Sun, 6 Sep 2026 09:15:39 +0000 Subject: [PATCH 4/4] Do not treat a closed fd as ready on the M:N scheduler Before parking a thread on an fd, the M:N scheduler polls it once to see whether it is ready already, and counts any answer poll gives as readiness. poll answers POLLNVAL for a closed fd, so the wait reported the event its caller asked for: IO#wait_writable on a closed fd returned the IO instead of raising Errno::EBADF, where a thread on a dedicated native thread raises it from the POLLNVAL its own poll returns. The probe has read POLLNVAL as readiness since the M:N scheduler was added, but it was harmless while rb_thread_wait_for_single_fd polled regardless and only asked the scheduler whether to retry. Since 41dd15944f a scheduler that reports readiness skips the poll and fills in the requested events, so the POLLNVAL that becomes EBADF below is never seen. Ruby 3.3 raises it; 3.4 and later do not. Report readiness only for an actual event. A closed fd then goes on to the registration, which the backend also refuses for it (EBADF), and from there to the blocking path, whose poll reports it as before. The test covers it from a Ractor, whose threads use the M:N scheduler whatever RUBY_MN_THREADS says. Co-Authored-By: Claude Opus 5 --- test/-ext-/wait/test_wait.rb | 21 +++++++++++++++++++++ thread_sched_mn.c | 25 +++++++++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/test/-ext-/wait/test_wait.rb b/test/-ext-/wait/test_wait.rb index 8e53f067cf37b2..a0bbfcc302c3d6 100644 --- a/test/-ext-/wait/test_wait.rb +++ b/test/-ext-/wait/test_wait.rb @@ -26,6 +26,27 @@ def test_wait_for_invalid_fd RUBY end + def test_wait_for_invalid_fd_in_ractor + # Threads of a Ractor always run on the M:N scheduler, whose readiness + # probe must not report a closed fd as ready. + assert_ractor(<<~'RUBY') + error = Ractor.new do + r, w = IO.pipe + r.close + IO.for_fd(w.fileno).close + + begin + w.wait_writable + nil + rescue SystemCallError => e + e.class + end + end.value + + assert_equal Errno::EBADF, error + RUBY + end + def test_wait_for_closed_pipe IO.pipe do |r,w| w.close diff --git a/thread_sched_mn.c b/thread_sched_mn.c index 038fe0b5fba1df..bac0e9eeaf63c4 100644 --- a/thread_sched_mn.c +++ b/thread_sched_mn.c @@ -1343,23 +1343,16 @@ fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want, bool consumed) } static bool -fd_readable_nonblock(int fd) +fd_ready_nonblock(int fd, short events) { struct pollfd pfd = { .fd = fd, - .events = POLLIN, + .events = events, }; - return poll(&pfd, 1, 0) != 0; -} -static bool -fd_writable_nonblock(int fd) -{ - struct pollfd pfd = { - .fd = fd, - .events = POLLOUT, - }; - return poll(&pfd, 1, 0) != 0; + // A "ready" answer makes the caller report the requested event, so a + // closed fd (POLLNVAL) must not count: it owes the caller EBADF. + return poll(&pfd, 1, 0) > 0 && !(pfd.revents & POLLNVAL); } static void @@ -1479,16 +1472,16 @@ timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting } if (flags & thread_sched_waiting_io_read) { - if (!(flags & thread_sched_waiting_io_force) && fd_readable_nonblock(fd)) { - RUBY_DEBUG_LOG("fd_readable_nonblock"); + if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLIN)) { + RUBY_DEBUG_LOG("fd readable"); return timer_thread_already_ready; } VM_ASSERT(fd >= 0); } if (flags & thread_sched_waiting_io_write) { - if (!(flags & thread_sched_waiting_io_force) && fd_writable_nonblock(fd)) { - RUBY_DEBUG_LOG("fd_writable_nonblock"); + if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLOUT)) { + RUBY_DEBUG_LOG("fd writable"); return timer_thread_already_ready; } VM_ASSERT(fd >= 0);