Project

General

Profile

Actions

Bug #22339

open

Ruby::Box: crash / hang at VM shutdown with RUBY_FREE_AT_EXIT=1

Bug #22339: Ruby::Box: crash / hang at VM shutdown with RUBY_FREE_AT_EXIT=1

Added by Eregon (Benoit Daloze) 17 minutes ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.1.0dev (2026-09-21T14:42:24Z master 41e04ff507) +PRISM [arm64-darwin25]
[ruby-core:126824]

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

$ 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():

    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.

No data to display

Actions

Also available in: PDF Atom