From e74aa875461e1981ce40fba55a4819c29c998f0a Mon Sep 17 00:00:00 2001 From: cfis Date: Fri, 13 Mar 2026 18:27:01 -0700 Subject: [PATCH 1/6] rb_gc_register_address() can trigger GC, so Anchor must register its VALUE slot before storing a heap object in it. Use RB_GC_GUARD(value) to keep the VALUE alive through the end of the method. --- rice/detail/Anchor.ipp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rice/detail/Anchor.ipp b/rice/detail/Anchor.ipp index 05543136..ddeee59b 100644 --- a/rice/detail/Anchor.ipp +++ b/rice/detail/Anchor.ipp @@ -2,14 +2,20 @@ namespace Rice { namespace detail { - inline Anchor::Anchor(VALUE value) : value_(value) + inline Anchor::Anchor(VALUE value) { + // rb_gc_register_address() can trigger GC, so we must register the + // empty this->value_ slot before storing a heap VALUE in it. + // RB_GC_GUARD(value) keeps the ctor argument alive through the end of + // this method until the registered slot has been updated. if (!RB_SPECIAL_CONST_P(value)) { Anchor::registerExitHandler(); detail::protect(rb_gc_register_address, &this->value_); this->registered_ = true; } + this->value_ = value; + RB_GC_GUARD(value); } inline Anchor::~Anchor() From 85f6fbb36899218afa67254d6f96c71cf793ce51 Mon Sep 17 00:00:00 2001 From: cfis Date: Fri, 13 Mar 2026 21:32:37 -0700 Subject: [PATCH 2/6] Update includes. --- include/rice/rice.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/rice/rice.hpp b/include/rice/rice.hpp index 39a2f031..97bb6fc7 100644 --- a/include/rice/rice.hpp +++ b/include/rice/rice.hpp @@ -1667,14 +1667,20 @@ namespace Rice { namespace detail { - inline Anchor::Anchor(VALUE value) : value_(value) + inline Anchor::Anchor(VALUE value) { + // rb_gc_register_address() can trigger GC, so we must register the + // empty this->value_ slot before storing a heap VALUE in it. + // RB_GC_GUARD(value) keeps the ctor argument alive through the end of + // this method until the registered slot has been updated. if (!RB_SPECIAL_CONST_P(value)) { Anchor::registerExitHandler(); detail::protect(rb_gc_register_address, &this->value_); this->registered_ = true; } + this->value_ = value; + RB_GC_GUARD(value); } inline Anchor::~Anchor() From f14f02d36bc673db0f87c8000c5cfc321494594e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 22 Mar 2026 20:45:27 -0700 Subject: [PATCH 3/6] Fix Valgrind invalid reads caused by stale GC root addresses. Fixes https://github.com/ruby-rice/rice/issues/399 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Anchor class uses rb_gc_register_address/rb_gc_unregister_address to protect Ruby VALUEs held by Rice::Object (via Pin) from garbage collection. The destructor skipped rb_gc_unregister_address when Anchor::enabled_ was false, a flag set by a callback registered with rb_set_end_proc during the first Anchor construction. The problem is that rb_set_end_proc callbacks and Ruby at_exit blocks both live in the same end_procs list and execute in LIFO (last-registered-first) order. If a test framework like minitest registers its at_exit block BEFORE the Rice extension loads (a common pattern: require "minitest/autorun" before require "my_extension"), the LIFO order becomes: 1. Anchor::disable runs first → enabled_ = false 2. minitest at_exit runs second → test suite executes During step 2, every temporary Rice::Object creates an Anchor that calls rb_gc_register_address. When the temporary is destroyed, the Anchor destructor sees enabled_ == false and skips rb_gc_unregister_address. The address remains in Ruby's global_object_list but the Anchor memory is freed. When GC runs (especially under GC.stress), rb_vm_mark at vm.c:3008 walks global_object_list and dereferences the freed pointer: rb_gc_mark_maybe(*list->varptr); // invalid read of size 8 The fix replaces rb_set_end_proc with ruby_vm_at_exit (declared in ruby/vm.h). The key difference in timing: - rb_set_end_proc fires DURING rb_ec_exec_end_proc, interleaved with at_exit blocks in unpredictable LIFO order (depends on require order). - ruby_vm_at_exit fires DURING ruby_vm_destruct, which runs AFTER all end_procs/at_exit blocks have completed and AFTER rb_ec_finalize. This is the correct boundary: all Ruby code has finished, but C++ static destruction (where global Rice::Object instances are destroyed) has not yet begun. Additionally, rb_gc_unregister_address is now called directly instead of through detail::protect (rb_protect wrapper). This is safe because rb_gc_unregister_address is a simple linked-list removal that never raises Ruby exceptions and never triggers GC (xfree does not trigger GC stress). Avoiding rb_protect in the destructor also eliminates any risk of longjmp during stack unwinding. --- rice/detail/Anchor.hpp | 2 +- rice/detail/Anchor.ipp | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rice/detail/Anchor.hpp b/rice/detail/Anchor.hpp index 1b1a3de9..23333caa 100644 --- a/rice/detail/Anchor.hpp +++ b/rice/detail/Anchor.hpp @@ -2,6 +2,7 @@ #define Rice__detail__Anchor__hpp_ #include +#include namespace Rice { @@ -36,7 +37,6 @@ namespace Rice VALUE get() const; private: - static void disable(VALUE); static void registerExitHandler(); inline static bool enabled_ = true; diff --git a/rice/detail/Anchor.ipp b/rice/detail/Anchor.ipp index ddeee59b..7fe1dc31 100644 --- a/rice/detail/Anchor.ipp +++ b/rice/detail/Anchor.ipp @@ -22,9 +22,8 @@ namespace Rice { if (Anchor::enabled_ && this->registered_) { - detail::protect(rb_gc_unregister_address, &this->value_); + rb_gc_unregister_address(&this->value_); } - // Ruby auto detects VALUEs in the stack, so make sure up in case this object is on the stack this->registered_ = false; this->value_ = Qnil; } @@ -34,17 +33,18 @@ namespace Rice return this->value_; } - // This will be called by ruby at exit - we want to disable further unregistering - inline void Anchor::disable(VALUE) - { - Anchor::enabled_ = false; - } - inline void Anchor::registerExitHandler() { if (!Anchor::exitHandlerRegistered_) { - detail::protect(rb_set_end_proc, &Anchor::disable, Qnil); + // Use ruby_vm_at_exit which fires AFTER the VM is destroyed, + // not rb_set_end_proc which fires BEFORE. rb_set_end_proc + // runs as an end_proc in LIFO order alongside at_exit blocks, + // so its timing depends on require order — if the extension + // loads after minitest/autorun, the disable callback runs + // before tests execute, causing Anchor destructors to skip + // rb_gc_unregister_address and leave dangling root pointers. + ruby_vm_at_exit([](ruby_vm_t*) { Anchor::enabled_ = false; }); Anchor::exitHandlerRegistered_ = true; } } From ca6eeeeee6085bd137acd6187ed819ea3136b2ad Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 22 Mar 2026 20:47:46 -0700 Subject: [PATCH 4/6] Update headers --- include/rice/rice.hpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/include/rice/rice.hpp b/include/rice/rice.hpp index 97bb6fc7..1729f632 100644 --- a/include/rice/rice.hpp +++ b/include/rice/rice.hpp @@ -859,6 +859,7 @@ namespace Rice::detail // ========= Anchor.hpp ========= #include +#include namespace Rice { @@ -893,7 +894,6 @@ namespace Rice VALUE get() const; private: - static void disable(VALUE); static void registerExitHandler(); inline static bool enabled_ = true; @@ -1687,9 +1687,8 @@ namespace Rice { if (Anchor::enabled_ && this->registered_) { - detail::protect(rb_gc_unregister_address, &this->value_); + rb_gc_unregister_address(&this->value_); } - // Ruby auto detects VALUEs in the stack, so make sure up in case this object is on the stack this->registered_ = false; this->value_ = Qnil; } @@ -1699,17 +1698,18 @@ namespace Rice return this->value_; } - // This will be called by ruby at exit - we want to disable further unregistering - inline void Anchor::disable(VALUE) - { - Anchor::enabled_ = false; - } - inline void Anchor::registerExitHandler() { if (!Anchor::exitHandlerRegistered_) { - detail::protect(rb_set_end_proc, &Anchor::disable, Qnil); + // Use ruby_vm_at_exit which fires AFTER the VM is destroyed, + // not rb_set_end_proc which fires BEFORE. rb_set_end_proc + // runs as an end_proc in LIFO order alongside at_exit blocks, + // so its timing depends on require order — if the extension + // loads after minitest/autorun, the disable callback runs + // before tests execute, causing Anchor destructors to skip + // rb_gc_unregister_address and leave dangling root pointers. + ruby_vm_at_exit([](ruby_vm_t*) { Anchor::enabled_ = false; }); Anchor::exitHandlerRegistered_ = true; } } From a49c32921229ffd44d7c984a39a81051c1016e3d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 22 Mar 2026 21:03:23 -0700 Subject: [PATCH 5/6] Line endings --- bin/rice-doc.rb | 0 bin/rice-rbs.rb | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/rice-doc.rb mode change 100644 => 100755 bin/rice-rbs.rb diff --git a/bin/rice-doc.rb b/bin/rice-doc.rb old mode 100644 new mode 100755 diff --git a/bin/rice-rbs.rb b/bin/rice-rbs.rb old mode 100644 new mode 100755 From f351c315a8a8ccf4e06a9c32c853e2c6837942a0 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 22 Mar 2026 21:11:18 -0700 Subject: [PATCH 6/6] Add -Wno-array-bounds for g++ 15 false positive in Ruby's RSTRING macro g++ 15 produces a false positive -Warray-bounds warning when inlining through Ruby's RSTRING macro (ruby/internal/core/rstring.h). This is harmless but breaks builds that use -Werror. Add -Wno-array-bounds to mkmf-rice.rb and CMakePresets.json for all GCC targets. Also document a separate g++ 15.2.1 / binutils 2.45.1 issue where LTO triggers an internal assembler segfault. This is not something Rice can fix, but the workaround (-fno-lto) is documented in build_settings.md. Both issues were discovered while investigating https://github.com/ruby-rice/rice/issues/399 --- CMakePresets.json | 6 +++--- docs/packaging/build_settings.md | 22 ++++++++++++++++++++-- lib/mkmf-rice.rb | 4 ++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index e43ae4d4..6a7e03ba 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,7 @@ "installDir": "${sourceDir}/install/${presetName}", "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden" + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden" } }, { @@ -81,7 +81,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_COMPILER": "g++.exe", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", "CMAKE_CXX_FLAGS_DEBUG": "-g3 -Og -fno-omit-frame-pointer -fno-inline -gsplit-dwarf" }, "condition": { @@ -97,7 +97,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_COMPILER": "g++.exe", - "CMAKE_CXX_FLAGS": "-Wall -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wno-array-bounds -ftemplate-backtrace-limit=0 -Wa,-mbig-obj -fvisibility=hidden -fvisibility-inlines-hidden", "CMAKE_CXX_FLAGS_RELEASE": "-O3 -DNDEBUG", "CMAKE_SHARED_LINKER_FLAGS_RELEASE": "-Wl,--exclude-all-symbols" }, diff --git a/docs/packaging/build_settings.md b/docs/packaging/build_settings.md index b101068e..baa3e4f8 100644 --- a/docs/packaging/build_settings.md +++ b/docs/packaging/build_settings.md @@ -7,13 +7,13 @@ Rice extensions requires several compiler settings to be set. These are captured For Clang and GCC: ```bash --std=c++17 -Wa,-mbig-obj -ftemplate-backtrace-limit=0 +-std=c++17 -Wno-array-bounds -Wa,-mbig-obj -ftemplate-backtrace-limit=0 ``` For MINGW: ```bash --std=c++17, -Wa,-mbig-obj +-std=c++17 -Wno-array-bounds -Wa,-mbig-obj ``` For Microsoft Visual C++ and Windows Clang: @@ -44,6 +44,14 @@ By default, MSVC does not update the `__cplusplus` preprocessor macro to reflect For Visual C++, the default exception [model](https://learn.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model?view=msvc-170) setting of `/EHsc` crashes Ruby when calling longjmp with optimizations enabled (/O2). Therefore you must `/EHs` instead. +### Array Bounds Warning + +g++ 15 produces false positive `-Warray-bounds` warnings when inlining through Ruby's `RSTRING` macro (in `ruby/internal/core/rstring.h`). This is not a bug in Rice or Ruby. The warning is harmless but becomes a build failure if `-Werror` is enabled. Rice suppresses it with: + +```bash +-Wno-array-bounds +``` + ### Template Backtrace For g++, you must set `-ftemplate-backtrace-limit=0` to avoid compilation errors. @@ -125,6 +133,16 @@ $CXXFLAGS << " /GL" $LDFLAGS << " /LTCG" ``` +### GCC 15 LTO Assembler Bug + +g++ 15.2.1 with binutils 2.45.1 (shipped in Fedora 43) can trigger an internal assembler segfault when LTO is enabled. If you hit this, disable LTO as a workaround: + +```ruby +$CXXFLAGS += " -fno-lto" +``` + +Or with CMake, set `CMAKE_INTERPROCEDURAL_OPTIMIZATION` to `OFF`. This only affects the specific GCC/binutils version combination and should be resolved in a future binutils release. + ### Debug Symbol Splitting (GCC/Clang) For debug builds with GCC or Clang, consider using `-gsplit-dwarf` to separate debug information into `.dwo` files. This keeps the main binary smaller while preserving full debug capability: diff --git a/lib/mkmf-rice.rb b/lib/mkmf-rice.rb index da4b7fcc..4b74cf82 100644 --- a/lib/mkmf-rice.rb +++ b/lib/mkmf-rice.rb @@ -31,9 +31,9 @@ def cpp_command(outfile, opt="") $CXXFLAGS += " /std:c++#{std} /EHs /permissive- /bigobj /utf-8 /Zc:__cplusplus" $CPPFLAGS += " -D_ALLOW_KEYWORD_MACROS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE" elsif IS_MINGW - $CXXFLAGS += " -std=c++#{std} -Wa,-mbig-obj" + $CXXFLAGS += " -std=c++#{std} -Wa,-mbig-obj -Wno-array-bounds" else - $CXXFLAGS += " -std=c++#{std}" + $CXXFLAGS += " -std=c++#{std} -Wno-array-bounds" end # Rice needs to include its header. Let's setup the include path