September 23, 2026·4 min read·engineeringrubyruntimememory-safety

Ruby 3.4.11 makes mutation during unpack fail safely

Ruby 3.4.11 includes a correction for String#unpack when its block mutates the source string. The failure illustrates the lifetime assumptions hidden beneath a convenient iterator-style API.

String#unpack can yield decoded values into a Ruby block. If that block clears the source string, the native decoder can lose the storage it was still reading. Ruby 3.4.11, released on September 23, 2026, includes a correction for that failure. Its maintenance work preserves ordinary language behavior for applications staying on the 3.4 series.

The release announcement calls this a scheduled bugfix update and points to the detailed release record. Among the listed corrections is a use-after-free in string unpacking with a block that mutates its receiver. That provides a concrete subject for understanding the patch beyond the general observation that an older release line still receives maintenance.

I find Ruby's blocks and collection methods elegant because they express iteration without much punctuation. My view is less enthusiastic when choosing Ruby automatically for new systems, but the expressiveness itself remains useful. This fix concerns a responsibility that such APIs put on the runtime. Calling user code partway through an operation means the operation's earlier assumptions may no longer hold when control returns.

The block can invalidate the decoder's input

Bug #22315 gives a small reproducer. It creates a long string and calls unpack with a block that clears that same string. AddressSanitizer reports a read from freed heap storage. The stack trace connects the freeing operation to String#clear executed through the user block and the later read to native integer unpacking.

The issue's explanation says the corrected operation raises RuntimeError with a message indicating that the string changed. The alternative behavior is therefore an explicit failure. It does not attempt to continue decoding through storage that the block has invalidated. That is a useful contract for an operation whose input mutates underneath it.

This is reentrancy on one thread, without requiring a race between independent threads. The native method yields into Ruby, and Ruby code changes an object that the suspended method still needs. Sequential execution does not keep the original buffer alive if the callback is permitted to discard it. The lifetime boundary occurs at the call into user code.

The diagnostic report also notes that increasing the source string's size can reproduce a crash without AddressSanitizer. Sanitizer output explains the invalid read, while an ordinary crash demonstrates that the behavior can affect a normal process. The issue does not need a concurrent workload to establish the defect.

Keeping an object and keeping its buffer are different

The receiver can remain a valid Ruby string object after clear. Its logical contents and underlying storage have changed. A native method holding an earlier pointer into that storage therefore has a stronger dependency than merely keeping the receiver object reachable. It needs the relevant buffer to remain valid for the duration of its reads.

That distinction explains why the callback is consequential. Before yielding, the decoder can obtain a pointer and length that accurately describe the receiver. After the block returns, those observations may be stale. An implementation must reestablish the required invariant or reject the changed input. The reported fix chooses rejection when the source changes.

The block API still serves a useful purpose for ordinary decoding. The problem is the specific interaction between iteration and mutation of its source. A blanket claim that blocks are unsafe would miss the mechanism. A normal callback that leaves the input intact does not create the invalidation shown in the report.

The maintenance release also lists related native-memory corrections involving array operations and encoding. Those entries have separate triggers and should not all be attributed to this unpacking defect. The release record is a collection of fixes across the runtime, not evidence that one shared cause explains every memory error it includes.

Maintenance keeps the interpreter contract usable

Other Ruby 3.4.11 entries concern observable behavior outside string decoding. The release record lists a correction to Enumerator::Lazy#to_enum accepting method names as strings and a fix for coverage collection being overwritten by Coverage.line_stub. It also includes Windows and macOS platform corrections. These changes matter to applications whose runtime choice remains Ruby 3.4.

A patch upgrade within that series has a different scope from adopting the Ruby 4.0 feature set. The release announcement supplies no instruction to enable a new JIT or redesign an application around a new concurrency API. Its value lies in the corrected behavior and the maintained implementation of existing facilities.

My interest in this patch is that its central example is easy to explain from the language level. A block clears the very string being decoded, and the runtime must stop before using invalid storage. The corrected exception makes that unsupported mutation visible to the caller. For applications on Ruby 3.4, the maintenance release carries that safer failure alongside the other documented runtime corrections.