[2] YARR RegularExpression heap overflow on duplicate named captures
Three NetworkProcess IPCs were missing capability gates — and the allowlist they mutate is process-global across every sibling renderer.
Rated High because the diff shows the caller's offset-vector size is recomputed from a stale (numSubpatterns + 1) * 2 formula while the interpreter writes the larger m_offsetsSize slot count, producing a deterministic OOB unsigned write of attacker-influenced indices on any UnicodeSets pattern with duplicate named captures; the four committed tests confirm the OOB is reachable through the public RegularExpression API.
RegularExpression's offsets vector allocation size is incorrect: the formula was updated when named captures were added, but RegularExpression's computation was not updated correctly. This patch fixes it.
Source/JavaScriptCore/yarr/RegularExpression.cpp
Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
Patch Details
RegularExpression::match previously derived offsetVectorSize locally as (d->m_numSubpatterns + 1) * 2 and grew nonReturnedOvector to that count before calling interpret(). The patch replaces that formula with the bytecode's authoritative d->m_regExpByteCode->m_offsetsSize. Four new TestWebKitAPI tests (DuplicateNamedCaptureGroupSimple, Multiple, NoMatch, SearchRev) construct a RegularExpression with patterns like (?<a>x)|(?<a>y) under Flags::UnicodeSets and exercise the previously-corrupting match/searchRev paths.
Caller-side allocation size formula left out of sync with a callee-side contract after the feature extension that added duplicate named capture groups.
Background
YARR is JavaScriptCore's regex engine. RegularExpression (in yarr/RegularExpression.cpp) is a thin C++ wrapper used by non-JS WebKit code that needs regex matching — text search, find-in-page, content-extension matching, and similar internal needs — distinct from the JavaScript RegExp object's own match path. It compiles a pattern to a BytecodePattern and runs interpret() against it.
The offsets vector is an array of unsigned indices used by the YARR interpreter to record where each capture group (and the whole match) starts and ends in the input string. Classically its size is (numSubpatterns + 1) * 2 — two indices, start and end, per subpattern, plus one for the overall match.
ES2024 introduced duplicate named capture groups: a pattern like (?<a>x)|(?<a>y) is allowed under the v flag (Unicode sets, surfaced in WebKit as Flags::UnicodeSets), letting the same group name appear in different alternation branches. To support this, the YARR bytecode compiler stores the authoritative offsets-vector size in BytecodePattern::m_offsetsSize.
Vector<unsigned, 32> is a WTF dynamic array with 32 elements of inline storage — small offset vectors live inside the Vector struct itself, larger ones spill to the heap.
Analysis
The bug is a caller/callee size-contract disagreement. The YARR interpreter writes to the offsets array using the bytecode's expected layout — every slot up to m_offsetsSize — but RegularExpression::match only allocated the smaller (m_numSubpatterns + 1) * 2 slots. When the bytecode compiler started reserving additional slots so each duplicate same-named capture group could be tracked independently, m_offsetsSize became the source of truth and the local formula in the caller became stale. The mismatch produces a heap (or inline-buffer stack) out-of-bounds write of unsigned values whenever the bytecode steps reach the extra duplicate-group slots.
The trigger is essentially the committed PoC: build RegularExpression("(?<a>x)|(?<a>y)", { UnicodeSets }) and call match on any input. The values written are the unsigned offsets the interpreter tracks for each duplicate-name slot — string indices into the input and offsetNoMatch sentinels. If the under-sized vector lives in inline storage, the write would corrupt stack-adjacent memory of the match() frame; if it spills to the heap, the write would corrupt adjacent heap chunks. With multiple alternation branches and large input strings the attacker could partly control the overwritten values via matched indices, giving a controlled write of a small set of integer values into adjacent memory. Write size is bounded by m_offsetsSize - (m_numSubpatterns + 1) * 2, which scales with the number of duplicate named groups in the pattern.
Reachability from untrusted web content depends on the specific WebKit caller of RegularExpression, which the diff does not pin down. The affected process is whichever runs YARR's RegularExpression helper with an attacker-influenced pattern — most commonly WebContent. This vulnerability weakens memory safety inside that process: the invariant that the offsets buffer passed to interpret() is sized to the bytecode's m_offsetsSize was violated whenever UnicodeSets patterns used duplicate named capture groups, and an attacker who can supply such a pattern could corrupt adjacent stack or heap memory.
This is a classic "two formulas for the same size, only one was updated" bug. When ES2024 duplicate-named-capture support landed, m_offsetsSize was added as the source of truth — but a sibling caller kept the legacy (numSubpatterns + 1) * 2 formula. Anywhere YARR exposes a "how big should my offsets buffer be?" contract to external callers, the callers must consult m_offsetsSize rather than recompute.
Audit directions
- Caller-side allocation formulas that duplicate a callee-side size contract drift out of sync when the callee's data model is extended. Audit every callsite of YARR's
interpret()and every place that allocates an offsets vector for YARR, and verify each one readsBytecodePattern::m_offsetsSizerather than recomputing fromnumSubpatterns. GrepSource/JavaScriptCore/yarr/fornumSubpatternsarithmetic used as a buffer size and confirm each instance is duplicate-named-capture-safe. - Features added behind a flag (here
UnicodeSets/v) that change data-layout requirements while leaving legacy non-vcode paths formally untouched. This hides size-contract drift behind a feature gate that few callers test. Audit YARR helpers (RegularExpression, content extensions' regex compiler, find-in-page regex glue) by constructing patterns withUnicodeSetsplus the features it unlocks (duplicate named groups, set notation, string-properties) and checking that buffer sizes still satisfy the bytecode's expectations. Vector<T, N>with inline storage of size N can mask OOB writes when the over-write stays within inline capacity but still corrupts trailing fields of theVectoror stack-adjacent memory. Review WTFVector<unsigned, 32>(and similar inline-capacity vectors) in YARR and JSC for cases wheregrow(n)is called with an externally-derivednand subsequent writes are performed by a callee that uses a different size source. Start withSource/JavaScriptCore/yarr/andSource/JavaScriptCore/runtime/RegExp*.cpp.- Parallel size derivations after a spec/feature extension. Variant-hunt this specific bug by grepping for
(m_numSubpatterns + 1) * 2,(numSubpatterns + 1) * 2, and equivalent expressions across JSC and WebCore — every match is a candidate for the same drift.