April 12, 2022·4 min read·engineeringrubysecurityregexmemory

Ruby fixes a double free while compiling regular expressions

CVE-2022-28738 affects construction of regex objects from crafted pattern strings. Ruby 3.0.4 and 3.1.2 repair the memory handling, independently of the risks of expensive pattern matching.

Constructing a regular expression is already a parsing and compilation operation. It happens before the resulting object tries to match application data. Ruby's new advisory identifies a memory error in that construction path, where a crafted pattern string can cause the same memory to be freed twice. A service that accepts patterns therefore needs to distinguish the safety of compiling one from the cost of executing it.

The April 12 advisory for CVE-2022-28738 recommends Ruby 3.0.4 or 3.1.2. The affected versions are the 3.0 series through 3.0.3 and the 3.1 series through 3.1.1. Ruby 2.6 and 2.7 are explicitly unaffected. Those branch boundaries matter, since an older version number alone does not describe exposure to this particular defect.

I find Ruby's readable text-processing facilities useful, but a short call can enter a substantial native implementation. The security boundary is the operation the input controls. Turning a supplied string into a pattern gives that string a different role from ordinary text being searched.

The advisory identifies construction as the trigger

The documented condition is creating a Regexp object from a crafted source string. A successful match is not part of that condition, and the advisory does not describe a pattern that merely consumes excessive processor time. Its named failure is a double free in the compilation process.

That is a memory-management error below the Ruby object abstraction. The same allocation should not be released twice. A Ruby caller ordinarily sees an object constructor, but the native implementation has to maintain ownership of the compiled pattern's storage while processing that constructor's input.

The advisory also says that generating and using regex objects from untrusted input is generally unsafe. It nevertheless treats this defect as a vulnerability after assessment. That wording establishes both the update requirement and a limit on what the update promises. Fixing this memory error does not make arbitrary external patterns a universally safe input format.

The distinction is useful for reading application code. A search operation with a developer-defined pattern lets external text supply the subject. A search operation that compiles an external pattern lets the input define part of the computation. Both may involve strings, but they give the sender different control.

The release diff makes the storage correction visible

The Ruby 3.1.1 to 3.1.2 diff changes onig_reg_resize in regcomp.c. The updated function handles the case where a compiled regex buffer has no used contents. It frees the buffer, sets its allocation count to zero and clears the pointer.

For a nonempty buffer with excess allocation, the resizing path remains. The changed structure therefore distinguishes an empty allocation from a nonempty buffer that can be reduced to its used length. That is more explicit than sending both cases through the same resize operation.

Clearing the pointer records that the empty buffer no longer owns live storage. The count and pointer then agree about the allocation's state. That is the concrete invariant visible in the diff, without needing to publish a crafted triggering pattern or speculate about exploit outcomes the advisory does not establish.

The function also continues across chained regex structures, with the revised loop expressing that traversal. The relevant security correction is the explicit empty-buffer handling. A small source change can repair a consequential lifetime error when the previous operation left storage ownership represented incorrectly.

Literal text needs different treatment from pattern syntax

The Regexp documentation in the Ruby 3.1.2 source documents Regexp.escape as escaping characters that have special regex meaning. It also documents construction with Regexp.new. Those operations can express an intentional distinction when an application needs to search for literal supplied text.

term = "a.b"
pattern = Regexp.new(Regexp.escape(term))
p pattern.match?("a.b")
# true
p pattern.match?("axb")
# false

Here the period is text to find. Escaping prevents it from becoming the regex operator that could match a different character. This example illustrates an input interpretation choice, not a substitute for installing the fixed interpreter. The advisory's remediation remains the patched Ruby versions.

Escaping is also inappropriate when the feature deliberately accepts a regex language. In that case it would change the feature into literal search. The application has to decide which input language it actually offers, and its implementation should reflect that decision explicitly.

I prefer that distinction to describing every string field as a search pattern. A literal search box and a programmable pattern field serve different requirements. Giving ordinary text pattern semantics unintentionally increases the implementation behaviour that the sender can influence.

The patch release has more than one security fix

The Ruby 3.1.2 release announcement links both this advisory and a separate buffer overrun in string-to-float conversion. Those are distinct defects, despite shipping in the same maintenance release. The regex advisory's affected branches and trigger should not be inferred from the other issue's description.

For CVE-2022-28738, the published evidence establishes a crafted pattern reaching a double free during construction, the affected version ranges and the fixed maintenance releases. The source diff explains the empty-buffer ownership correction. None of that establishes that Ruby 2.6 or 2.7 needs this particular fix, since the advisory explicitly excludes them.

The resulting practical boundary is specific. A fixed interpreter repairs the documented compilation defect. The application still decides whether external input is literal text or regex syntax, and that choice determines whether it should escape special characters before constructing the object. Pattern execution costs remain a separate property of the computation requested.

The advisory credits the researcher piao and records its original publication time as noon UTC on April 12. That contemporaneous notice is the basis for the version guidance here. The public explanation does not provide an application-specific exposure assessment or assert that every program using a regex constructor processes attacker-controlled input. Reaching the constructor with a crafted source string is the condition an application analysis has to establish.