April 23, 2024·4 min read·engineeringrubyregexpsecuritymemory-safety

Ruby repairs a regex stack lifetime error that can disclose memory

CVE-2024-27282 permits heap disclosure through attacker-supplied regex input. The correction records a stack index before an operation that may move the stack storage.

A regular-expression engine can lose track of a capture even when its bookkeeping uses stack indices. Computing an index from a pointer after the stack has grown can already be too late. Ruby's correction for CVE-2024-27282 changes the order of two operations so the pointer-to-index conversion happens before a push can invalidate the original relationship.

The April 23 advisory reports that attacker-supplied data passed to the Ruby regex compiler can permit extraction of heap data relative to the start of the text. The disclosed contents can include pointers and sensitive strings. Corrected releases are Ruby 3.0.7, 3.1.5, 3.2.4 and 3.3.1.

I came to Ruby through Perl, so expressive regular-expression support is a familiar part of the appeal. The concise call site gives little indication of the native execution machinery underneath it. This defect concerns the lifetime of that machinery's internal storage, with consequences beyond whether a pattern returns the expected match.

The correction moves index calculation before a push

The upstream correction is titled as a use-after-free fix for Regexp. It changes OP_MEMORY_END_PUSH_REC handling in match_at. The operation obtains a pointer to a capture's start entry and then needs to record that entry's stack index.

Before the correction, the code pushed a memory-end entry first. It calculated the start entry's index afterward. The corrected order records the index immediately after obtaining the pointer, before the push.

STACK_GET_MEM_START(mem, stkp);
mem_start_stk[mem] = GET_STACK_INDEX(stkp);
STACK_PUSH_MEM_END(mem, s);

These lines are an excerpt of the corrected engine operation, with an explanatory comment omitted. The order establishes the index while stkp still belongs to the current stack storage. The subsequent push can then use index-based bookkeeping without requiring that saved pointer to remain valid across growth.

A small source diff does not imply an unimportant runtime condition. The changed ordering sits inside repeated matcher execution. Its correctness depends on what the called macros may do to storage, which is not apparent from the three lines alone.

A stack push can change the base address

The Ruby 3.3.1 implementation of the matcher defines GET_STACK_INDEX as subtraction of the stack base from an entry pointer. That calculation assumes both identify the same current allocation. It is meaningful before any operation that can relocate the stack.

STACK_PUSH_MEM_END first invokes STACK_ENSURE(1). If the available capacity is insufficient, that macro calls stack_double with references to the base, end and current stack pointers. The growth operation can therefore change the storage to which the active stack pointers refer.

Saving an old entry pointer across that operation is unsafe. Even if the logical entry remains present after copying or reallocating the stack, its previous address does not necessarily identify the entry in the new allocation. Subtracting the new base from the old pointer cannot recover a reliable index.

That is the mechanism visible in the correction and surrounding implementation. Moving the calculation earlier preserves the logical location while the original address relationship still holds. The fix does not add a broad application-level restriction on which strings may be searched.

The macro implementation also shows why preserving the index is useful. It stores capture start and end information in the new stack entry, then updates the capture's current end position. The bookkeeping describes logical matcher state across operations that can change physical storage. An index can survive that change because it names an offset, provided the program computes it against the allocation that actually contains the entry.

This is a distinction between keeping the logical contents and keeping an address valid. Growing a data structure may preserve every existing entry while relocating them all. Code holding a pointer into its old storage must complete the required calculation before growth, or obtain a new pointer afterward. This correction uses the first approach at the exact operation where the saved capture pointer was still available.

Memory disclosure differs from expensive matching

Regular-expression security discussions often concern patterns that consume excessive CPU time. This advisory describes a different failure class. The reported effect is reading heap data outside the intended text, including information unrelated to the application's intended match.

The distinction changes which evidence is relevant. A time limit on matching concerns execution duration. It does not repair an internal pointer whose allocation relationship has already become invalid. The relevant correction here is in the engine's memory bookkeeping.

The notice conditions the issue on attacker-supplied input reaching the regex compiler. It does not claim that every ordinary search using a fixed application pattern discloses memory. It also does not provide a universal remote-code-execution consequence. The reported confidentiality impact should be stated on its own terms.

Pointers and sensitive strings are specifically named because heap contents can carry both implementation information and application data. The public description does not identify which secrets a particular process would expose, or a guaranteed amount of memory returned per attempt. Those depend on conditions not established by the advisory.

The fix belongs in each interpreter line

The affected-version list names Ruby 3.0.6 and earlier in that series, 3.1.4 and earlier, 3.2.3 and earlier, plus 3.3.0. The advisory recommends 3.3.1 or later, with the listed patched alternatives for applications retaining an older interpreter line.

This differs from a vulnerability isolated in an independently updated gem. The correction is in the regex engine distributed with Ruby. Updating a framework dependency alone would not replace the interpreter's regexec.c implementation.

The range also explains why an application already on Ruby 3.3.0 still needs the patch. A recent feature release is not evidence that every native-memory defect has been eliminated. The relevant distinction for this issue is whether the runtime includes the corrected operation order.

I remain cautious about choosing Ruby automatically for new systems, but existing programs need an accurate maintenance path. This advisory supplies one for each affected series. The substantive correction is specific, recording the capture's stack position before stack growth can move its storage, so later matching uses a position derived from a valid pointer relationship.