January 15, 2025·4 min read·engineeringrubygarbage-collectionmaintenanceregex

Ruby 3.3.7 repairs method hashes across compaction

Ruby 3.3.7 fixes a Method hash invariant and a regex timeout cleanup failure. Both show how runtime maintenance preserves the behavior expected by ordinary Ruby code.

A method object used as a hash key must not acquire a different hash value merely because the garbage collector moved internal storage. Ruby 3.3.7 includes a fix for exactly that problem. It is a small maintenance release whose changes reach below the object model into the runtime machinery that makes the model dependable.

The January 15 announcement describes a routine update with minor bug fixes. Its release notes include the method-hash correction alongside regex cleanup, IO argument handling, and other runtime repairs. These are fixes on the 3.3 branch, not a new collection of language syntax.

I came to Ruby from Perl and still find its object model more consistent for larger object-oriented programs. That preference depends on the runtime preserving apparently simple invariants. A method object's behavior should not expose an unrelated storage relocation to the code using it as a key.

Compaction must preserve observable identity

Peter Zhu's bug report #20868 demonstrates a method hash changing across compaction. The report explains why that is incorrect. A method must retain a constant hash value for reliable use as a hash-table key. Moving internal objects is not a semantic change to the method. Its hash must describe the same callable value before and after the collector finishes.

The reproducer defines a method and obtains its hash through a separate helper. That separation matters because an instruction sequence referenced on the C stack can be pinned during compaction. A pinned object does not move, so a simpler example might fail to exercise the bug.

This illustrates the report's shape without depending on a particular numeric hash value:

def example
end

def example_hash
  method(:example).hash
end

before = example_hash
GC.verify_compaction_references(expand_heap: true, toward: :empty)
after = example_hash
puts before == after

The verification call deliberately exercises compaction behavior. It is a diagnostic reproducer, not application initialization code. The corrected behavior preserves equality of the two hash results. Their actual integer value is irrelevant to the invariant under examination.

The distinction between logical identity and storage location is fundamental here. Runtime internals may relocate an object while Ruby code continues to refer to the same value. Any observable operation that accidentally depends on the old location can make that implementation detail visible. The release notes identify this particular case through Method#hash.

The failure is also more subtle than a crash. The program can continue executing while a hash-key assumption becomes invalid. Maintenance that preserves these invariants matters to code that uses reflection and callable objects, even when its source contains no explicit memory management.

Regex timeout cleanup had two owners

The release also includes bug #20886, reported by John Hawthorn. A regex timeout could reach cleanup paths that freed the same allocation twice. The report identifies stk_alloc, also referenced through msa->stack_p, as the storage involved.

One release happened at the end of match_at. Another happened in the parent caller's FREE_MATCH_ARG cleanup. Both paths treated themselves as responsible for releasing the same storage. That ownership error turned an exceptional regex path into a process-level failure.

The report includes a reproducer using a regex with an explicit timeout and a repeated input. It notes that reproduction depends on the timeout and input size and is not completely deterministic. This is useful context when interpreting a failed attempt to reproduce the crash. A single successful run does not contradict the reported cleanup problem.

The issue arose after earlier work on regex memory leaks. Hawthorn's backport discussion says the corresponding leak changes had not reached the 3.2 branch, so that branch did not appear to need this particular correction. A bug's presence follows the relevant implementation changes, not simply whether a runtime has the same high-level regex API.

A maintenance branch still changes implementation behavior

Both fixes preserve contracts that an application already expects. The method hash remains stable across a collector operation. Regex cleanup releases its storage once when matching exits through a timeout. Neither requires the application to adopt a new language idiom to benefit.

These cases also explain why a patch release can matter to apparently ordinary Ruby. An object-oriented interface does not remove native memory management inside the interpreter. Its users depend on that implementation to preserve object behavior and handle exceptional control flow correctly.

I remain selective about choosing Ruby for new systems, despite appreciating its readable blocks and collection methods. For an existing Ruby application, runtime maintenance has a different purpose from language selection. This release improves the implementation underneath code that is already committed to the 3.3 series.

The release notes mark their issue list as automatically generated and potentially incomplete. They provide a full comparison with the previous tag for the broader changeset. The method-hash and regex issues supply concrete reasons for the update, while the announcement itself keeps the release's scope at routine maintenance.