CVE-2026-28847
Overview
Background
- Offset vector (ovector)
- The array Yarr fills with pairs of start/end string offsets, one pair for the whole match and one per capturing subpattern, that the caller reads back after a match.
- m_numSubpatterns
- The count of capturing subpatterns recorded for a compiled regular expression, historically used to size the offset vector as (count+1)*2.
- m_offsetsSize
- The authoritative number of offset slots the compiled Yarr bytecode expects, recorded on the bytecode object at compile time.
- Duplicate named capture groups
- An ECMAScript feature (with UnicodeSets/
vmode) allowing the same group name to appear in mutually exclusive alternatives, which can change the offsets layout the compiler emits. - UnicodeSets flag
- The Yarr
Flags::UnicodeSets(v) mode enabling set-notation and, relevant here, duplicate named capture group semantics.
Root Cause Analysis
RegularExpression::match in Source/JavaScriptCore/yarr/RegularExpression.cpp allocates an offset vector to receive the start/end offsets of every capturing subpattern produced by a Yarr match. Before the patch it sized that vector as (d->m_numSubpatterns + 1) * 2, i.e. it assumed the number of offset slots the compiled bytecode writes is derived purely from the count of subpatterns plus one for the whole match, times two (start and end per group).
The patch replaces this with d->m_regExpByteCode->m_offsetsSize, the authoritative offsets count the bytecode compiler itself recorded. This means the old computation could under-size the offset vector relative to what the Yarr interpreter actually writes.
The added tests all use duplicate named capture groups under UnicodeSets/v flag (e.g. (?<a>x)|(?<a>y)), which strongly indicates the mismatch arises with duplicate named capture groups: when the same name is reused across alternatives, the compiler produces an offsets layout whose size is not simply (m_numSubpatterns + 1) * 2. The violated invariant is that the buffer handed to the Yarr interpreter must be at least as large as the offsets area the compiled bytecode assumes; when it is not, the interpreter writes match offsets past the end of nonReturnedOvector (the 32-element inline Vector<unsigned, 32>) or the returned vector, corrupting adjacent memory or reading/writing out of bounds.
The fix restores the invariant by sizing the offset vector from the single source of truth, m_offsetsSize, so the allocation always matches what the bytecode was compiled to use. (Inference: the exact divergence between (m_numSubpatterns + 1) * 2 and m_offsetsSize for duplicate named groups lives in the Yarr compiler, which is not part of this diff; the patch itself only shows the sizing correction in the consumer.)
(subpatterns+1)*2) that no longer matches how the Yarr compiler lays out offsets for duplicate named capture groups; the fix makes the allocation defer to the bytecode’s own recorded m_offsetsSize, the only value guaranteed to match the writer.Attack Path
- Craft a duplicate-named-group regex
From attacker-controlled web content (JavaScript), construct a regular expression that reuses the same capture-group name across alternatives under the UnicodeSets/
vsemantics, e.g.(?<a>x)|(?<a>y), or many such groups to enlarge the offsets area. - Trigger the C++ RegularExpression path Cause WebKit to route the pattern through JSC::Yarr::RegularExpression::match (the WTF/WebCore-facing wrapper used internally), reaching the offset-vector sizing at line 112.
- Force offset-vector under-sizing
Because the code computes
(d->m_numSubpatterns + 1) * 2rather than the bytecode’s realm_offsetsSize, the allocated vector is smaller than the number of offset slots the compiled bytecode writes for the duplicate-named layout. - Drive a match that populates all offsets Supply an input string that matches, so the Yarr interpreter writes the full set of offset entries, spilling past the end of the under-sized inline/heap offset vector.
- Observe memory corruption / crash
The out-of-bounds write of match offsets corrupts adjacent stack/heap memory (the inline
Vector<unsigned, 32>nonReturnedOvector or its heap backing), producing an unexpected WebContent process crash; escalation beyond a crash is not demonstrated by the commit.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
RegularExpression::matchSource/JavaScriptCore/yarr/RegularExpression.cpp |
modified | Offset-vector size now taken from d->m_regExpByteCode->m_offsetsSize instead of the derived `(d->m_numSubpatterns + 1) * 2`, fixing an under-sized buffer for patterns (notably duplicate named capture groups) whose real offsets count differs. |
DuplicateNamedCaptureGroup* testsTools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp |
added | New API tests validating that duplicate named capture groups under UnicodeSets compile, match, fail-to-match, and reverse-search correctly with the corrected offset sizing. |
Files Changed
Source/JavaScriptCore/yarr/RegularExpression.cppTools/TestWebKitAPI/CMakeLists.txtTools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
Audit Directions
- Other ovector sizing in the same file/callersGrep RegularExpression.cpp and Yarr callers for
m_numSubpatterns,(... + 1) * 2, andoffsetVectorSizeto find any remaining site that derives offset-vector size heuristically instead of fromm_offsetsSize. - All consumers of Yarr::interpret / bytecode offsetsSearch for
m_offsetsSize,nonReturnedOvector, andVector<unsigned, 32>-style inline ovectors across Source/JavaScriptCore/yarr and runtime RegExp code to confirm every match entrypoint sizes buffers to the bytecode’s expectation. - Duplicate-named-group layout assumptionsIn the Yarr compiler (YarrPattern/YarrInterpreter/YarrJIT), audit how duplicate named capture groups affect subpattern-to-offset mapping; grep for
hasNamedCaptures,duplicateNamed,m_numSubpatterns, and offset-slot allocation to ensure count-derived sizing is never reused elsewhere.