From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-09-21T16:46:32+00:00 Subject: [ruby-core:126824] [Ruby Bug#22339] Ruby::Box: crash / hang at VM shutdown with RUBY_FREE_AT_EXIT=1 Issue #22339 has been reported by Eregon (Benoit Daloze). ---------------------------------------- Bug #22339: Ruby::Box: crash / hang at VM shutdown with RUBY_FREE_AT_EXIT=1 https://bugs.ruby-lang.org/issues/22339 * Author: Eregon (Benoit Daloze) * Status: Open * ruby -v: ruby 4.1.0dev (2026-09-21T14:42:24Z master 41e04ff507) +PRISM [arm64-darwin25] * Backport: 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED ---------------------------------------- ## Summary With `RUBY_BOX=1` and `RUBY_FREE_AT_EXIT=1`, Ruby crashes or hangs while freeing boxes at VM shutdown. No user box needs to be created: the built-in boxes are enough. `box_entry_free()` walks `box->classext_cow_classes` at shutdown, but the objects in that table have already been freed by the shutdown sweep, so `free_classext_for_box()` sees a type that is no longer `T_CLASS`/`T_MODULE`/`T_ICLASS` and calls `rb_bug()`. Under ASAN the same teardown is caught one step earlier, as an iclass classext freed twice. ## Reproduction ```console $ RUBY_BOX=1 RUBY_FREE_AT_EXIT=1 ruby -e '' ``` Either environment variable alone is fine; only the combination fails. ## Observed behaviour `ruby 4.1.0dev (2026-09-21T14:42:24Z master 41e04ff507) +PRISM [arm64-darwin25]` hangs, spinning at 99% CPU. `sample(1)` of the main thread: ``` rb_ec_cleanup rb_objspace_call_finalizer rb_gc_obj_free box_entry_free (box.c, st_foreach over classext_cow_classes) st_general_foreach free_classext_for_box rb_bug ("Invalid type of object in classext_cow_classes: %s") rb_bug_without_die_internal rb_source_location_cstr _sigtramp <- SIGSEGV while printing the bug report sigsegv rb_bug_for_fatal_signal rb_source_location_cstr rb_iseq_path <- loops here forever ``` `ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]` (docker `ruby:4.0.7-slim-trixie`) dies with SIGSEGV (exit 139, core dumped) instead of hanging. The offending branch is in `free_classext_for_box()`: ```c if (RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)) { ... } else if (RB_TYPE_P(obj, T_ICLASS)) { ... } else { rb_bug("Invalid type of object in classext_cow_classes: %s", rb_type_str(BUILTIN_TYPE(obj))); } ``` ## ASAN evidence A Linux ASAN build of master reports a use-after-free in the same shutdown teardown, where the same `id_table` is freed twice through the identical call path: ``` ERROR: AddressSanitizer: heap-use-after-free, READ of size 8 #0 rb_id_table_free id_table.c:112:16 #1 rb_iclass_classext_free class.c:128:9 #2 rb_class_classext_foreach class.c:460:5 #3 rb_gc_obj_free gc.c:1402:9 #4 rb_gc_impl_shutdown_call_finalizer gc/default/default.c:3087:25 #5 rb_objspace_call_finalizer gc.c:1739:5 #6 rb_ec_finalize eval.c:177:5 #7 rb_ec_cleanup eval.c:268:5 freed by thread T0 here: #0 free #1 rb_gc_impl_free gc/default/default.c:8285:9 #2 rb_iclass_classext_free class.c:128:9 #3 rb_class_classext_foreach class.c:460:5 #4 rb_gc_obj_free gc.c:1402:9 #5 rb_gc_impl_shutdown_call_finalizer gc/default/default.c:3087:25 #6 rb_objspace_call_finalizer gc.c:1739:5 ``` That build additionally had a user box created before exit; the `rb_bug` reproduction above does not need one. ## Affected versions Same one-liner on each build: | build | result | | --- | --- | | master `41e04ff507`, arm64-darwin25 | hang (99% CPU) | | 4.0.7, 4.0.6, 4.0.3 (ruby-build), arm64-darwin25 | hang (99% CPU) | | 4.0.7 linux (`ruby:4.0.7-slim-trixie`, `ruby:4.0.7-slim-bookworm`) | SIGSEGV | | 4.0.6 linux (`ruby:4.0.6-slim-trixie`) | SIGSEGV | | 4.0.5, 4.0.3, 4.0.2, 4.0.1 linux (`ruby:4.0.x-slim`, `-slim-trixie`) | exits 0 | The base image makes no difference (bookworm and trixie behave the same), so this is the Ruby build rather than the distribution. Since the underlying defect is a use-after-free, the rows that exit 0 are presumably latent rather than unaffected: whether the freed object still reads back as a class depends on allocator reuse. ## Possibly a separate issue Once `rb_bug()` is reached at this point in shutdown, the bug reporter itself crashes in `rb_source_location_cstr()`, and the fatal signal handler re-enters the same function, so the process never terminates. A `rb_bug()` this late should still be able to abort. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/