Bug #22339
closedRuby::Box: crash / hang at VM shutdown with RUBY_FREE_AT_EXIT=1
Description
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¶
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():
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.
Updated by Eregon (Benoit Daloze) 4 days ago
- Tags set to box
Updated by Eregon (Benoit Daloze) 4 days ago
- Assignee set to tagomoris (Satoshi Tagomori)
Updated by hsbt (Hiroshi SHIBATA) 3 days ago
I was already aware of this, and https://github.com/ruby/ruby/pull/18710 fixes it by skipping classes that were already freed when a box is freed.
The ASAN double free in the description comes from ruby_4_0 and has a separate cause. It happens when String gets a prepended module in a box, and master already fixed it with https://github.com/ruby/ruby/pull/18535. The 4.0 backport needs both PRs.
Updated by hsbt (Hiroshi SHIBATA) 3 days ago
- Status changed from Open to Closed
Applied in changeset git|4617d60fe4b5c18e7350d37b2f2c4508c62df708.
[Bug #22339] Skip classes already freed while freeing a box
RUBY_FREE_AT_EXIT=1 frees every object in an arbitrary order, so a
class a box refers to can go before the box itself, and freeing a class
already frees its classext for that box. The box then reached rb_bug
with a T_NONE object, and the report crashed and looped in the signal
handler instead of aborting, so the process spun at exit.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com