From 5cf85ca32b697d43c5b808a905667ad4c0a4ba18 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 10 Jul 2026 12:47:22 -0500 Subject: [PATCH 01/15] [DOC] Doc for Pathname#rmdir --- pathname_builtin.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 17c924cf8e41db..43f7494707f625 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2296,7 +2296,23 @@ def each_entry(&block) # :yield: pathname # Argument `permissions` is ignored on Windows. def mkdir(...) Dir.mkdir(@path, ...) end - # See Dir.rmdir. Remove the referenced directory. + # :markup: markdown + # + # call-seq: + # rmdir -> 0 + # + # Deletes the directory at the path in +self+; returns `0`: + # + # ```ruby + # pn = Pathname('doc/foo') + # pn.mkdir + # pn.rmdir + # ``` + # + # Raises an exception if the directory is not empty, + # or if the path does not point to a directory. + # + # Use method #rmtree to delete the entire filetree at the path. def rmdir() Dir.rmdir(@path) end # :markup: markdown From 65ee33b895aaab720ce371cba6e990b645417f45 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 10 Jul 2026 16:20:48 -0500 Subject: [PATCH 02/15] [DOC] Doc for Pathname#size --- pathname_builtin.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 43f7494707f625..3777f031df4d32 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2135,7 +2135,24 @@ def setuid?() FileTest.setuid?(@path) end # See FileTest.setgid?. def setgid?() FileTest.setgid?(@path) end - # See FileTest.size. + # :markup: markdown + # + # call-seq: + # size -> integer + # + # Returns the size of the entry at the path in `self`: + # + # ```ruby + # Pathname('README.md').size # => 3469 + # Pathname('doc').size # => 4096 + # pn = Pathname('doc/t.tmp') + # pn.write('') + # pn.size # => 0 + # pd.delete # Clean up. + # ``` + # + # Raises an exception if the entry does not exist. + # def size() FileTest.size(@path) end # See FileTest.size?. From 94a555151784d43bf80068b09b8c0edaf95407e4 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 10 Jul 2026 16:10:39 -0500 Subject: [PATCH 03/15] [DOC] Doc for Pathname#size? --- pathname_builtin.rb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 3777f031df4d32..f404402beb93f8 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2155,7 +2155,29 @@ def setgid?() FileTest.setgid?(@path) end # def size() FileTest.size(@path) end - # See FileTest.size?. + # :markup: markdown + # + # call-seq: + # size? -> integer or nil + # + # If the file or directory entry at the path in `self` exists, + # returns its size if non-zero, or `nil` if zero: + # + # ```ruby + # pn = Pathname('doc/t.tmp') + # pn.write('foo') + # pn.size? # => 3 + # pn.write('') + # pn.size? # => nil + # ``` + # + # Returns `nil` if the entry does not exist: + # + # ```ruby + # pn.delete + # pn.size? # => nil + # ``` + # def size?() FileTest.size?(@path) end # See FileTest.sticky?. def sticky?() FileTest.sticky?(@path) end From 0e3866868d8cd82c57b03586ce0283df74a79b65 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 10 Jul 2026 15:35:25 -0500 Subject: [PATCH 04/15] [DOC] Doc for Pathname#setgid? --- pathname_builtin.rb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index f404402beb93f8..f9b2b47ff3ff74 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2132,7 +2132,28 @@ def readable_real?() FileTest.readable_real?(@path) end # See FileTest.setuid?. def setuid?() FileTest.setuid?(@path) end - # See FileTest.setgid?. + # :markup: markdown + # + # call-seq: + # setgid? -> true or false + # + # Returns whether the [setgid bit](https://en.wikipedia.org/wiki/Setuid) is set + # in the permissions for the entry at the path in `self`: + # + # ```ruby + # # Create a file and get its permissions and setgid? setting. + # pn = Pathname('doc/t.tmp') + # pn.write('foo') + # mode = pn.stat.mode.to_s(8) # => "100664" + # pn.setgid? # => false + # # Set the bit. + # pn.chmod(0o2644) + # mode = pn.stat.mode.to_s(8) # => "102644" + # pn.setgid? # => true + # pn.delete # Clean up. + # ``` + # + # On Windows, the bit is never set; the method always returns `false`. def setgid?() FileTest.setgid?(@path) end # :markup: markdown From ca20425fd5ae626c779b5786e729a09c437934ea Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 10 Jul 2026 19:10:19 +0000 Subject: [PATCH 05/15] Avoid a stale TLS execution context in fiber_switch after NT migration `fiber_current()` reads `GET_EC()`, which the compiler may inline as a thread-pointer-relative TLS access whose base is cached in a callee-saved register. `coroutine_transfer` preserves callee-saved registers across the stack switch, so when a fiber's coroutine resumes on a different native thread (M:N scheduler migration), the cached thread pointer is stale and `fiber_current()` returns another thread's execution context. `fiber_switch()` calls `fiber_current()` right after the transfer. With `-DRUBY_DEBUG=1` this trips `VM_ASSERT(ec == rb_current_ec_noinline())` in `rb_current_execution_context`; in release builds it silently uses the wrong (or NULL) EC. Only reproduces with >= 2 Ractors (a single Ractor does not migrate the coroutine). Read the current EC through `rb_current_ec_noinline()` in `fiber_current()`, which forces a fresh thread-pointer load. fiber_current() is only used by the (cold) fiber machinery, so the non-inlined read costs nothing on hot paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- bootstraptest/test_fiber.rb | 19 +++++++++++++++++++ cont.c | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bootstraptest/test_fiber.rb b/bootstraptest/test_fiber.rb index ae809a59366779..d71fb5b9f0ed3d 100644 --- a/bootstraptest/test_fiber.rb +++ b/bootstraptest/test_fiber.rb @@ -42,3 +42,22 @@ assert_normal_exit %q{ Thread.new { Fiber.current.kill }.join } + +# fiber_current() must not read a stale (cached) TLS execution context after a +# fiber's coroutine migrates between native threads under the M:N scheduler. +# Needs >= 2 Ractors so coroutines actually migrate; sleep drives the migration. +assert_equal 'ok', %q{ + Warning[:experimental] = false + 2.times.map do + Ractor.new do + 200.times do + f = Fiber.new { Fiber.yield; 1 } + f.resume + sleep(0.0003) # block -> M:N native-thread migration + f.resume rescue nil + end + :ok + end + end.each(&:value) + :ok +} diff --git a/cont.c b/cont.c index 6bb61e5ee84be2..77ed342e2438f9 100644 --- a/cont.c +++ b/cont.c @@ -2187,7 +2187,9 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking) static inline rb_fiber_t* fiber_current(void) { - rb_execution_context_t *ec = GET_EC(); + /* Called right after a coroutine transfer: an inlined GET_EC() may read a + * TLS pointer cached before the NT migration, so force a fresh load. */ + rb_execution_context_t *ec = rb_current_ec_noinline(); return ec->fiber_ptr; } From f3e4fc4fee347310ca64d5f6462655b6266c654e Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Fri, 10 Jul 2026 14:44:55 -0500 Subject: [PATCH 06/15] [DOC] Doc for Pathname#root? --- pathname.c | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/pathname.c b/pathname.c index b12f64037392da..13ff83ff764d7e 100644 --- a/pathname.c +++ b/pathname.c @@ -133,11 +133,45 @@ same_paths(VALUE self, VALUE a, VALUE b) } /* - * Predicate method for root directories. Returns +true+ if the - * pathname consists of consecutive slashes. + * :markup: markdown + * + * call-seq: + * root? -> true or false + * + * Returns whether the path in `self` points to a root directory. + * + * On a non-Windows system, a root directory path is one whose name begins + * with one or more slash characters (`'/'): + * + * ```ruby + * Pathname('/').root? # => true + * Pathname('////').root? # => true + * Pathname('/usr').root? # => false + * Pathname('foo').root? # => false + * ``` + * + * Does not resolve dot directories: + * + * ```ruby + * Pathname('/usr/.').root? # => false + * Pathname('/usr/..').root? # => false + * ``` + * + * On a Windows system, a root directory path is one whose name begins as above, + * or with a device letter followed by a colon character (`':'`) + * and one or more slash characters (`'/'): + * + * ```ruby + * Pathname('/').root? # => true + * Pathname('////').root? # => true + * Pathname('C:/').root? # => true + * Pathname('C:////').root? # => true + * Pathname('c:/').root? # => true + * Pathname('H:/').root? # => true + * Pathname('C:/m').root? # => false + * Pathname('C:').root? # => false + * ``` * - * It doesn't access the filesystem. So it may return +false+ for some - * pathnames which points to roots such as /usr/... */ static VALUE path_root_p(VALUE self) From 3fa1c870894353ab172be9d58213181923fe7cb3 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 10 Jul 2026 19:16:31 +0000 Subject: [PATCH 07/15] Do not count a not-yet-running thread as a Ractor barrier waiter `rb_ractor_sched_barrier_join` unconditionally did `barrier_waiting_cnt++`. A thread can reach it (via `vm_lock_enter`) while its Ractor's `sched.running` is set but the thread is not yet in the VM running set (`sched.is_running == false`) -- e.g. resuming from a blocking IO wait. Counting such a thread pushes `barrier_waiting_cnt` past `running_cnt - 1` and trips `VM_ASSERT(running_cnt - 1 >= barrier_waiting_cnt)` in `ractor_sched_barrier_completed_p`; in release builds the unsigned `(running_cnt - barrier_waiting_cnt) == 1` completion test can underflow. Increment `barrier_waiting_cnt` only when `cr->threads.sched.is_running` (O(1), toggled together with running_threads add/del). A not-running thread is parked at the barrier regardless, so it is safe to wait without counting. NOTE (open): this guards the count at the join site. A stricter fix would prevent a not-running thread from reaching barrier_join at all; that needs care so it still waits for the barrier rather than proceeding during compaction. Left for review. Reproduction is currently coupled with a separate pre-existing compaction use-after-poison on a blocked-IO thread's STR_TMPLOCK buffer (only the IO-read blocking path triggers this barrier window), so a standalone passing test awaits that fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- thread_pthread.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 1dc68318495f20..e0190093287976 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1664,10 +1664,16 @@ rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr) ractor_sched_lock(vm, cr); { // running_cnt - vm->ractor.sched.barrier_waiting_cnt++; - RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_waiting_cnt, barrier_serial); - - ractor_sched_barrier_join_signal_locked(vm); + /* A not-yet-running thread (blocking/terminating, joined only to + * sync) must not be counted, or barrier_waiting_cnt > running_cnt-1. */ + if (cr->threads.sched.is_running) { + vm->ractor.sched.barrier_waiting_cnt++; + RUBY_DEBUG_LOG("waiting_cnt:%u serial:%u", vm->ractor.sched.barrier_waiting_cnt, barrier_serial); + ractor_sched_barrier_join_signal_locked(vm); + } + else { + RUBY_DEBUG_LOG("join without counting (not running) serial:%u", barrier_serial); + } ractor_sched_barrier_join_wait_locked(vm, cr->threads.sched.running); } ractor_sched_unlock(vm, cr); From 98097c53f98b8ccf1183f2ca92b2652fe9bb2843 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:11:35 +0900 Subject: [PATCH 08/15] Add `binary-package` target on mswin Stage `install-nodoc` under DESTDIR, bundle the vcpkg runtime DLLs, the VC runtime and the license terms of everything redistributed, then archive the tree into a relocatable zip. Groundwork for official Windows binary packages. Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 75 ++++++++++++++++++++++++++++++++++++++++++ win32/Makefile.sub | 14 ++++++++ 2 files changed, 89 insertions(+) create mode 100644 tool/binary-package.rb diff --git a/tool/binary-package.rb b/tool/binary-package.rb new file mode 100644 index 00000000000000..8948a831c6b0fe --- /dev/null +++ b/tool/binary-package.rb @@ -0,0 +1,75 @@ +#!/usr/bin/ruby +# Assemble a relocatable binary package from a staged installation on +# Windows (mswin). Invoked by the `binary-package` target in +# win32/Makefile.sub; not intended to be run manually. + +require 'optparse' +require 'fileutils' + +stage = name = arch = srcdir = vcpkgdir = output = nil +opt = OptionParser.new +opt.on('--stage=DIR') {|v| stage = v} +opt.on('--name=NAME') {|v| name = v} +opt.on('--arch=ARCH') {|v| arch = v} +opt.on('--srcdir=DIR') {|v| srcdir = v} +opt.on('--vcpkg-dir=DIR') {|v| vcpkgdir = v} +opt.on('--output=FILE') {|v| output = v} +opt.parse!(ARGV) +abort(opt.to_s) unless stage and name and arch and srcdir and vcpkgdir and output + +stage = File.expand_path(stage) +output = File.expand_path(output) + +# Find the installed prefix inside the stage. DESTDIR staging +# reproduces the prefix below the stage directory (with the drive +# letter stripped), so descend while there is a single directory +# until bin/ appears. +root = stage +until File.directory?(File.join(root, "bin")) + entries = Dir.children(root) + unless entries.size == 1 and File.directory?(dir = File.join(root, entries[0])) + abort "#{$0}: could not locate installed prefix under #{stage}" + end + root = dir +end +abort "#{$0}: prefix must not be the stage root" if root == stage + +bindir = File.join(root, "bin") + +# Bundle the vcpkg runtime DLLs next to ruby.exe, where they are +# found first by the DLL search order. +dlls = Dir.glob(File.join(vcpkgdir, "bin", "*.dll")) +dlls.reject! {|f| File.basename(f, ".dll") == "readline"} +abort "#{$0}: no DLLs in #{vcpkgdir}/bin; run `nmake install-vcpkg' first" if dlls.empty? +dlls.each {|f| FileUtils.cp(f, bindir)} + +# Bundle the VC runtime (app-local deployment). UCRT ships with the +# OS since Windows 10, but vcruntime140*.dll does not. +vcruntime = [] +if redist = ENV["VCToolsRedistDir"] + cpu = arch == "i386" ? "x86" : arch + # backslashes would be glob escapes + pattern = File.join(redist.tr("\\", "/"), cpu, "Microsoft.VC*.CRT", "vcruntime140*.dll") + vcruntime = Dir.glob(pattern) + vcruntime.each {|f| FileUtils.cp(f, bindir)} +end +warn "#{$0}: vcruntime140.dll not bundled; run under vcvars to set VCToolsRedistDir" if vcruntime.empty? + +# Collect license terms of everything the package redistributes. +licdir = File.join(root, "LICENSES") +FileUtils.mkdir_p(File.join(licdir, "ruby")) +%w[COPYING COPYING.ja BSDL LEGAL].each do |f| + src = File.join(srcdir, f) + FileUtils.cp(src, File.join(licdir, "ruby", f)) if File.exist?(src) +end +Dir.glob(File.join(vcpkgdir, "share", "*", "copyright")) do |f| + FileUtils.cp(f, File.join(licdir, "#{File.basename(File.dirname(f))}.txt")) +end + +# Rename the prefix directory to the package name and archive it with +# bsdtar, which ships with Windows 10 and later. +pkgdir = File.join(stage, name) +File.rename(root, pkgdir) unless root == pkgdir +FileUtils.rm_f(output) +system("tar", "-a", "-c", "-f", output, "-C", stage, name, exception: true) +puts "packaged #{output}" diff --git a/win32/Makefile.sub b/win32/Makefile.sub index cd0ba933dafc3b..da8df258987dcb 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -614,6 +614,20 @@ prepare-vcpkg:: if not %%~nI == readline mklink %%~nxI %%I \ ) +!ifndef VCPKG_INSTALLED +VCPKG_INSTALLED = $(srcdir)/vcpkg_installed/$(VCPKG_DEFAULT_TRIPLET) +!endif +BINARY_PACKAGE_NAME = $(RUBY_BASE_NAME)-$(RUBY_PROGRAM_VERSION)-$(arch) +BINARY_PACKAGE_STAGE = binary-package-stage + +binary-package: + @if exist $(BINARY_PACKAGE_STAGE)\. rd /s /q $(BINARY_PACKAGE_STAGE) + @$(MAKE) DESTDIR="$(MAKEDIR)\$(BINARY_PACKAGE_STAGE)" install-nodoc + $(RUNRUBY) "$(tooldir)/binary-package.rb" --stage=$(BINARY_PACKAGE_STAGE) \ + --name=$(BINARY_PACKAGE_NAME) --arch=$(ARCH) --srcdir="$(srcdir)" \ + --vcpkg-dir="$(VCPKG_INSTALLED)" \ + --output=$(BINARY_PACKAGE_NAME).zip + .PHONY: reconfig reconfig $(MKFILES): $(srcdir)/common.mk $(srcdir)/version.h \ $(hdrdir)/ruby/version.h $(ABI_VERSION_HDR) \ From fec7b8b6e7667d02ecdc04781c4ba284a476cb7e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 4 Jul 2026 09:20:49 +0900 Subject: [PATCH 09/15] Scrub build-machine paths from packaged rbconfig mkmf applies $configure_args to every extension build, so a leftover --with-opt-dir pointing into the build machine's vcpkg tree breaks or taints gem compilation on the destination machine. Strip arguments containing absolute paths from CONFIG["configure_args"] when assembling a binary package. Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tool/binary-package.rb b/tool/binary-package.rb index 8948a831c6b0fe..98d467407458a8 100644 --- a/tool/binary-package.rb +++ b/tool/binary-package.rb @@ -66,6 +66,22 @@ FileUtils.cp(f, File.join(licdir, "#{File.basename(File.dirname(f))}.txt")) end +# Scrub build-machine specific paths from the recorded configure +# arguments. mkmf applies $configure_args (notably --with-opt-dir) to +# every extension build, so a leftover absolute path breaks or taints +# gem compilation on the destination machine. +rbconfigs = Dir.glob(File.join(root, "lib/ruby/*/*/rbconfig.rb")) +abort "#{$0}: rbconfig.rb not found under #{root}" if rbconfigs.empty? +rbconfigs.each do |file| + src = File.binread(file) + src.sub!(/^(\s*CONFIG\["configure_args"\]\s*=\s*")(.*)(")/) do + pre, args, post = $1, $2, $3 + kept = args.scan(/\\".*?\\"|\S+/).reject {|t| t.match?(%r{[A-Za-z]:[/\\]})} + pre + kept.join(" ") + post + end or abort "#{$0}: configure_args not found in #{file}" + File.binwrite(file, src) +end + # Rename the prefix directory to the package name and archive it with # bsdtar, which ships with Windows 10 and later. pkgdir = File.join(stage, name) From b79f10aa8387c523eb840305ce813e9986fb244e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 10 Jul 2026 14:00:17 +0900 Subject: [PATCH 10/15] Do not bundle the VC runtime in binary-package App-local copies of vcruntime140*.dll are never serviced by Windows Update. Assume the VC++ Redistributable is installed instead, as suggested by naruse. https://bugs.ruby-lang.org/issues/22180#change-117963 Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 20 ++++++-------------- win32/Makefile.sub | 2 +- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tool/binary-package.rb b/tool/binary-package.rb index 98d467407458a8..195b6f1eaf8a74 100644 --- a/tool/binary-package.rb +++ b/tool/binary-package.rb @@ -6,16 +6,15 @@ require 'optparse' require 'fileutils' -stage = name = arch = srcdir = vcpkgdir = output = nil +stage = name = srcdir = vcpkgdir = output = nil opt = OptionParser.new opt.on('--stage=DIR') {|v| stage = v} opt.on('--name=NAME') {|v| name = v} -opt.on('--arch=ARCH') {|v| arch = v} opt.on('--srcdir=DIR') {|v| srcdir = v} opt.on('--vcpkg-dir=DIR') {|v| vcpkgdir = v} opt.on('--output=FILE') {|v| output = v} opt.parse!(ARGV) -abort(opt.to_s) unless stage and name and arch and srcdir and vcpkgdir and output +abort(opt.to_s) unless stage and name and srcdir and vcpkgdir and output stage = File.expand_path(stage) output = File.expand_path(output) @@ -43,17 +42,10 @@ abort "#{$0}: no DLLs in #{vcpkgdir}/bin; run `nmake install-vcpkg' first" if dlls.empty? dlls.each {|f| FileUtils.cp(f, bindir)} -# Bundle the VC runtime (app-local deployment). UCRT ships with the -# OS since Windows 10, but vcruntime140*.dll does not. -vcruntime = [] -if redist = ENV["VCToolsRedistDir"] - cpu = arch == "i386" ? "x86" : arch - # backslashes would be glob escapes - pattern = File.join(redist.tr("\\", "/"), cpu, "Microsoft.VC*.CRT", "vcruntime140*.dll") - vcruntime = Dir.glob(pattern) - vcruntime.each {|f| FileUtils.cp(f, bindir)} -end -warn "#{$0}: vcruntime140.dll not bundled; run under vcvars to set VCToolsRedistDir" if vcruntime.empty? +# The VC runtime (vcruntime140*.dll) is deliberately not bundled. +# App-local copies are never serviced by Windows Update; the package +# assumes the VC++ Redistributable is installed. +# https://bugs.ruby-lang.org/issues/22180 # Collect license terms of everything the package redistributes. licdir = File.join(root, "LICENSES") diff --git a/win32/Makefile.sub b/win32/Makefile.sub index da8df258987dcb..619faf6663cae4 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -624,7 +624,7 @@ binary-package: @if exist $(BINARY_PACKAGE_STAGE)\. rd /s /q $(BINARY_PACKAGE_STAGE) @$(MAKE) DESTDIR="$(MAKEDIR)\$(BINARY_PACKAGE_STAGE)" install-nodoc $(RUNRUBY) "$(tooldir)/binary-package.rb" --stage=$(BINARY_PACKAGE_STAGE) \ - --name=$(BINARY_PACKAGE_NAME) --arch=$(ARCH) --srcdir="$(srcdir)" \ + --name=$(BINARY_PACKAGE_NAME) --srcdir="$(srcdir)" \ --vcpkg-dir="$(VCPKG_INSTALLED)" \ --output=$(BINARY_PACKAGE_NAME).zip From 7af7815b2f508fe891250ca5955f13080f6698e2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 10 Jul 2026 14:12:36 +0900 Subject: [PATCH 11/15] Simplify configure_args scrubbing in binary-package Apply the review suggestion from nobu on GH-17662. Co-authored-by: Nobuyoshi Nakada Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tool/binary-package.rb b/tool/binary-package.rb index 195b6f1eaf8a74..89fdda0edff0a6 100644 --- a/tool/binary-package.rb +++ b/tool/binary-package.rb @@ -66,10 +66,8 @@ abort "#{$0}: rbconfig.rb not found under #{root}" if rbconfigs.empty? rbconfigs.each do |file| src = File.binread(file) - src.sub!(/^(\s*CONFIG\["configure_args"\]\s*=\s*")(.*)(")/) do - pre, args, post = $1, $2, $3 - kept = args.scan(/\\".*?\\"|\S+/).reject {|t| t.match?(%r{[A-Za-z]:[/\\]})} - pre + kept.join(" ") + post + src.sub!(/^\s*CONFIG\["configure_args"\]\s*=\s*"\K.*(?=")/) do |args| + args.scan(/\\".*?\\"|\S+/).grep_v(%r{\b[A-Za-z]:[/\\]}).join(" ") end or abort "#{$0}: configure_args not found in #{file}" File.binwrite(file, src) end From c0ee7c636480678328e5cc2b523d3573732e6466 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 10 Jul 2026 14:13:04 +0900 Subject: [PATCH 12/15] Prefer the inbox tar.exe when archiving a binary package A GNU tar earlier in PATH cannot write zip with -a, which made the archiving step environment dependent. Pointed out by Copilot on GH-17662. Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tool/binary-package.rb b/tool/binary-package.rb index 89fdda0edff0a6..4fcd7cf2cb325a 100644 --- a/tool/binary-package.rb +++ b/tool/binary-package.rb @@ -73,9 +73,12 @@ end # Rename the prefix directory to the package name and archive it with -# bsdtar, which ships with Windows 10 and later. +# bsdtar, which ships with Windows 10 and later. Prefer the inbox +# tar.exe; a GNU tar earlier in PATH cannot write zip with -a. pkgdir = File.join(stage, name) File.rename(root, pkgdir) unless root == pkgdir FileUtils.rm_f(output) -system("tar", "-a", "-c", "-f", output, "-C", stage, name, exception: true) +tar = File.join(ENV["SystemRoot"] || "C:/Windows", "System32", "tar.exe") +tar = "tar" unless File.exist?(tar) +system(tar, "-a", "-c", "-f", output, "-C", stage, name, exception: true) puts "packaged #{output}" From 908150f7e2c0db483a122684fe8ebed317a7080f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 11 Jul 2026 09:19:46 +0900 Subject: [PATCH 13/15] Harden path handling and license collection in binary-package Normalize backslashes in the directory arguments before they reach Dir.glob so an overridden vcpkg path fails clearly instead of through a misleading missing-DLL abort, and abort when no vcpkg copyright files are collected so the package never ships a bundled DLL without its license terms. Co-Authored-By: Claude Fable 5 --- tool/binary-package.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tool/binary-package.rb b/tool/binary-package.rb index 4fcd7cf2cb325a..a86d12b71b1805 100644 --- a/tool/binary-package.rb +++ b/tool/binary-package.rb @@ -18,6 +18,9 @@ stage = File.expand_path(stage) output = File.expand_path(output) +# Normalize so backslashes are not treated as glob escapes below. +srcdir = srcdir.tr("\\", "/") +vcpkgdir = vcpkgdir.tr("\\", "/") # Find the installed prefix inside the stage. DESTDIR staging # reproduces the prefix below the stage directory (with the drive @@ -54,7 +57,9 @@ src = File.join(srcdir, f) FileUtils.cp(src, File.join(licdir, "ruby", f)) if File.exist?(src) end -Dir.glob(File.join(vcpkgdir, "share", "*", "copyright")) do |f| +copyrights = Dir.glob(File.join(vcpkgdir, "share", "*", "copyright")) +abort "#{$0}: no copyright files in #{vcpkgdir}/share" if copyrights.empty? +copyrights.each do |f| FileUtils.cp(f, File.join(licdir, "#{File.basename(File.dirname(f))}.txt")) end From f132ecd1d9c9ff21f60737b23e28175946070f90 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 11 Jul 2026 12:07:28 +0900 Subject: [PATCH 14/15] Verify binary-package on Windows CI Build the package on the check jobs and smoke-test the produced zip: extract it, require openssl/fiddle/psych/zlib with PATH restricted to System32, and assert that no VC runtime is bundled and no build-machine path remains in configure_args. Co-Authored-By: Claude Fable 5 --- .github/workflows/windows.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 9a9c2be282a000..193c4a8eba9c30 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -196,6 +196,29 @@ jobs: RUBY_TESTOPTS: -j${{ env.TEST_JOBS || 4 }} timeout-minutes: 70 + - run: nmake binary-package + if: ${{ matrix.test_task == 'check' }} + timeout-minutes: 10 + + - name: Verify binary package + shell: pwsh + if: ${{ matrix.test_task == 'check' }} + run: | + $zip = Get-Item ruby-*-mswin*.zip + $dest = Join-Path $env:RUNNER_TEMP "binpkg" + if (Test-Path $dest) { Remove-Item -Recurse -Force $dest } + New-Item -ItemType Directory $dest | Out-Null + & "$env:SystemRoot\System32\tar.exe" -x -f $zip.FullName -C $dest + if ((Get-ChildItem $dest).Count -ne 1) { throw "zip must contain a single root directory" } + $root = (Get-ChildItem $dest)[0].FullName + if (!(Test-Path "$root\bin\ruby.exe")) { throw "bin\ruby.exe not found" } + if (Get-ChildItem "$root\bin" -Filter "vcruntime140*.dll") { throw "the VC runtime must not be bundled" } + if (!(Get-ChildItem "$root\LICENSES" -ErrorAction SilentlyContinue)) { throw "LICENSES missing" } + $env:PATH = "$env:SystemRoot\System32" + & "$root\bin\ruby.exe" -v -ropenssl -rfiddle -rpsych -rzlib -e 'abort "configure_args has an absolute path: #{RbConfig::CONFIG[%q(configure_args)]}" if RbConfig::CONFIG[%q(configure_args)] =~ /\b[A-Za-z]:[\/\\]/; puts %q(binary package OK)' + if ($LASTEXITCODE -ne 0) { throw "smoke test failed" } + timeout-minutes: 5 + - uses: ./.github/actions/slack with: label: Windows ${{ matrix.os }} / ${{ matrix.test_task || 'check' }} From 916d8c105079aeae933c50b1641a2fc926059322 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 10 Jul 2026 20:22:19 +0900 Subject: [PATCH 15/15] Prevent overflow of length bits for embedded structs Embedded structs only use 7 bits for the length, so it will overflow if the GC can allocate large slots. We need to add the cap to struct_alloc. --- struct.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/struct.c b/struct.c index 72a5fa7e3d22e9..606208e87268e5 100644 --- a/struct.c +++ b/struct.c @@ -829,7 +829,9 @@ struct_alloc(VALUE klass) VALUE flags = T_STRUCT; - if (n > 0 && rb_gc_size_allocatable_p(embedded_size)) { + 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)) { flags |= n << RSTRUCT_EMBED_LEN_SHIFT; if (RCLASS_MAX_IV_COUNT(klass) == 0) { // We set the flag before calling `NEWOBJ_OF` in case a NEWOBJ tracepoint does