Released May 11, 2026

CVE-2026-28847

CVSS 8.8 severity high CWE-119CWE-120 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Idan Masas, DARKNAVY (@DarkNavyOrg), Anonymous working with TrendAI Zero Day Initiative, Daniel Rhea

Key insight. The offset vector was sized from a heuristic ((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.

Patch Details

diff --git a/Source/JavaScriptCore/yarr/RegularExpression.cpp b/Source/JavaScriptCore/yarr/RegularExpression.cpp
index d3b46b6629b2..b91ec283b272 100644
--- a/Source/JavaScriptCore/yarr/RegularExpression.cpp
+++ b/Source/JavaScriptCore/yarr/RegularExpression.cpp
@@ -109,7 +109,7 @@ int RegularExpression::match(StringView str, unsigned startFrom, int* matchLengt
     if (str.isNull())
         return -1;
 
-    int offsetVectorSize = (d->m_numSubpatterns + 1) * 2;
+    int offsetVectorSize = d->m_regExpByteCode->m_offsetsSize;
     unsigned* offsetVector;
     Vector<unsigned, 32> nonReturnedOvector;
 
diff --git a/Tools/TestWebKitAPI/CMakeLists.txt b/Tools/TestWebKitAPI/CMakeLists.txt
index 8b3174492bf2..7dc60b7a0ce5 100644
--- a/Tools/TestWebKitAPI/CMakeLists.txt
+++ b/Tools/TestWebKitAPI/CMakeLists.txt
@@ -177,6 +177,7 @@ if (ENABLE_JAVASCRIPTCORE)
         Tests/JavaScriptCore/InspectorConsoleMessage.cpp
         Tests/JavaScriptCore/MarkedVector.cpp
         Tests/JavaScriptCore/PropertySlot.cpp
+        Tests/JavaScriptCore/RegularExpression.cpp
     )
 
     set(TestJavaScriptCore_LIBRARIES
diff --git a/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp b/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
new file mode 100644
index 000000000000..0dc35539cd9d
--- /dev/null
+++ b/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2026 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include <JavaScriptCore/InitializeThreading.h>
+#include <JavaScriptCore/RegularExpression.h>
+#include <JavaScriptCore/YarrFlags.h>
+
+namespace TestWebKitAPI {
+
+using JSC::Yarr::RegularExpression;
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupSimple)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(0, re.match("x"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+
+    EXPECT_EQ(0, re.match("y"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupMultiple)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)|(?<b>x)|(?<b>y)|(?<c>x)|(?<c>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(0, re.match("x"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+
+    EXPECT_EQ(0, re.match("y"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupNoMatch)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(-1, re.match("z"_s, 0, &matchLength));
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupSearchRev)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    EXPECT_EQ(3, re.searchRev("abxyz"_s));
+    EXPECT_EQ(1, re.matchedLength());
+}
+
+} // namespace TestWebKitAPI

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28847
Bugzilla 308707
Component JSC
Bug Class OOB
Severity medium
Commit e5368156542a8414…
Advisory Apple Advisory

Terminology

  • 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/v mode) 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.)

Attack Path

1. 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/v semantics, e.g. (?<a>x)|(?<a>y), or many such groups to enlarge the offsets area.

2. 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.

3. Force offset-vector under-sizing

Because the code computes (d->m_numSubpatterns + 1) * 2 rather than the bytecode’s real m_offsetsSize, the allocated vector is smaller than the number of offset slots the compiled bytecode writes for the duplicate-named layout.

4. 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.

5. 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

The primitive is an out-of-bounds write of Yarr match offsets into an under-allocated unsigned buffer; the number and values written are constrained by the compiled pattern and the matched input, so it is a comparatively weak, structured overflow rather than a freely controlled write. The commit and its Description establish only an unexpected process crash (LogicError, medium severity); no UAF or arbitrary-write primitive toward RCE is demonstrated by the diff. It is confined to the process that runs the JS/Yarr engine, i.e. the sandboxed WebContent process. Realistic escalation to code execution would require a separate, undemonstrated shaping of the adjacent memory and control over the overflowing values.

Audit Directions

Other ovector sizing in the same file/callers

Grep RegularExpression.cpp and Yarr callers for m_numSubpatterns, (... + 1) * 2, and offsetVectorSize to find any remaining site that derives offset-vector size heuristically instead of from m_offsetsSize.

All consumers of Yarr::interpret / bytecode offsets

Search for m_offsetsSize, nonReturnedOvector, and Vector<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 assumptions

In 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.

Changed Functions

Function File Change Note
RegularExpression::match Source/JavaScriptCore/yarr/RegularExpression.cpp modified Offset-vector size now taken from d->m_regExpByteCode->m_offsetsSize instead of the derived (d-&gt;m_numSubpatterns + 1) * 2, fixing an under-sized buffer for patterns (notably duplicate named capture groups) whose real offsets count differs.
DuplicateNamedCaptureGroup* tests Tools/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 Files

  • Source/JavaScriptCore/yarr/RegularExpression.cpp

Test Files

  • Tools/TestWebKitAPI/CMakeLists.txt
  • Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/Tools/TestWebKitAPI/CMakeLists.txt b/Tools/TestWebKitAPI/CMakeLists.txt
index 8b3174492bf2..7dc60b7a0ce5 100644
--- a/Tools/TestWebKitAPI/CMakeLists.txt
+++ b/Tools/TestWebKitAPI/CMakeLists.txt
@@ -177,6 +177,7 @@ if (ENABLE_JAVASCRIPTCORE)
         Tests/JavaScriptCore/InspectorConsoleMessage.cpp
         Tests/JavaScriptCore/MarkedVector.cpp
         Tests/JavaScriptCore/PropertySlot.cpp
+        Tests/JavaScriptCore/RegularExpression.cpp
     )
 
     set(TestJavaScriptCore_LIBRARIES
diff --git a/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp b/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
new file mode 100644
index 000000000000..0dc35539cd9d
--- /dev/null
+++ b/Tools/TestWebKitAPI/Tests/JavaScriptCore/RegularExpression.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2026 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include <JavaScriptCore/InitializeThreading.h>
+#include <JavaScriptCore/RegularExpression.h>
+#include <JavaScriptCore/YarrFlags.h>
+
+namespace TestWebKitAPI {
+
+using JSC::Yarr::RegularExpression;
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupSimple)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(0, re.match("x"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+
+    EXPECT_EQ(0, re.match("y"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupMultiple)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)|(?<b>x)|(?<b>y)|(?<c>x)|(?<c>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(0, re.match("x"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+
+    EXPECT_EQ(0, re.match("y"_s, 0, &matchLength));
+    EXPECT_EQ(1, matchLength);
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupNoMatch)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    int matchLength = 0;
+    EXPECT_EQ(-1, re.match("z"_s, 0, &matchLength));
+}
+
+TEST(JavaScriptCore_RegularExpression, DuplicateNamedCaptureGroupSearchRev)
+{
+    JSC::initialize();
+
+    RegularExpression re("(?<a>x)|(?<a>y)"_s, { JSC::Yarr::Flags::UnicodeSets });
+    EXPECT_TRUE(re.isValid());
+
+    EXPECT_EQ(3, re.searchRev("abxyz"_s));
+    EXPECT_EQ(1, re.matchedLength());
+}
+
+} // namespace TestWebKitAPI