Released March 31, 2025

CVE-2025-30427

CVSS 4.3 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

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

Researchers: rheza (@ginggilBesel)

Key insight. Private-name field defines must not generate prototype property-miss conditions; the spurious conditions referenced structures that GC could free, leaving the cached case dangling.

Patch Details

diff --git a/JSTests/stress/static-private-fields-dont-need-property-conditions.js b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
new file mode 100644
index 000000000000..37ccd62442c2
--- /dev/null
+++ b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
@@ -0,0 +1,28 @@
+function f0() {
+}
+const v3 = Object.getOwnPropertyNames(this);
+this / Object;
+const v7 = f0();
+try {
+    Object(f0, true);
+} catch(e11) {
+}
+for (const v12 of v3) {
+    const v14 = this[v12];
+    for (let i16 = 0;
+        (() => {
+            class C21 extends Object {
+                static #f = v14;
+                6 = Object;
+            }
+            new C21();
+            new C21();
+            new C21();
+            Object.__proto__ = v3;
+            new Float64Array(2887);
+            return i16 < 32;
+        })();
+        i16++) {
+    }
+    Object[v12] = this;
+}
\ No newline at end of file
diff --git a/Source/JavaScriptCore/bytecode/Repatch.cpp b/Source/JavaScriptCore/bytecode/Repatch.cpp
index 075b4de86dd3..0a69aac032c0 100644
--- a/Source/JavaScriptCore/bytecode/Repatch.cpp
+++ b/Source/JavaScriptCore/bytecode/Repatch.cpp
@@ -497,6 +497,9 @@ static InlineCacheAction tryCacheGetBy(JSGlobalObject* globalObject, CodeBlock*
                 newCase = AccessCase::create(vm, codeBlock, AccessCase::IndexedProxyObjectLoad, nullptr);
                 break;
             }
+            case GetByKind::PrivateName:
+            case GetByKind::PrivateNameById:
+                RELEASE_ASSERT_NOT_REACHED();
             default:
                 break;
             }
@@ -635,6 +638,7 @@ static InlineCacheAction tryCacheGetBy(JSGlobalObject* globalObject, CodeBlock*
             else {
                 if (isPrivate) {
                     RELEASE_ASSERT(!slot.isUnset());
+                    RELEASE_ASSERT(conditionSet.isEmpty());
                     constexpr bool isGlobalProxy = false;
                     if (!slot.isCacheable())
                         return GiveUpOnCache;
@@ -1113,13 +1117,9 @@ static InlineCacheAction tryCachePutBy(JSGlobalObject* globalObject, CodeBlock*
                     break;
                 }
                 case PutByKind::DefinePrivateNameById:
-                case PutByKind::DefinePrivateNameByVal: {
+                case PutByKind::DefinePrivateNameByVal:
                     ASSERT(ident.isPrivateName());
-                    conditionSet = generateConditionsForPropertyMiss(vm, codeBlock, globalObject, newStructure, ident.impl());
-                    if (!conditionSet.isValid())
-                        return GiveUpOnCache;
                     break;
-                }
                 case PutByKind::ByIdDirectStrict:
                 case PutByKind::ByIdDirectSloppy:
                 case PutByKind::ByValDirectStrict:
@@ -1213,12 +1213,13 @@ static InlineCacheAction tryCachePutBy(JSGlobalObject* globalObject, CodeBlock*
             }
             case PutByKind::DefinePrivateNameById:
             case PutByKind::DefinePrivateNameByVal:
+            case PutByKind::SetPrivateNameById:
+            case PutByKind::SetPrivateNameByVal:
+                RELEASE_ASSERT_NOT_REACHED();
             case PutByKind::ByIdDirectStrict:
             case PutByKind::ByIdDirectSloppy:
             case PutByKind::ByValDirectStrict:
             case PutByKind::ByValDirectSloppy:
-            case PutByKind::SetPrivateNameById:
-            case PutByKind::SetPrivateNameByVal:
                 return GiveUpOnCache;
             }
         }
@@ -1566,7 +1567,7 @@ static InlineCacheAction tryCacheInBy(
                 break;
             }
             default:
-                break;
+                RELEASE_ASSERT_NOT_REACHED();
             }
         } else if (wasFound) {
             if (!structure->propertyAccessesAreCacheable())

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-30427
Bugzilla 285643
Component JSC
Bug Class UAF
Severity medium
Commit d7bd7d8f7cdf153d…
Advisory Apple Advisory

Terminology

  • Inline cache (IC) / Repatch — JSC caches property accesses; Repatch.cpp builds/repatches the cached cases.
  • Property-miss condition set — Watchpoints/conditions over prototype-chain structures that a cached access assumes remain valid.
  • Private name field — A class #private field; a direct own-property define that does NOT need prototype-chain conditions.

Root Cause Analysis

The bug is in JavaScriptCore’s inline-cache generation for property access in Repatch.cpp, on the private-name path. In tryCachePutBy, the DefinePrivateNameById/DefinePrivateNameByVal cases called generateConditionsForPropertyMiss() to build a property-condition set for the define, and gave up if it was invalid. That is wrong: defining a static/instance private field is a direct own-property operation with a known structure transition and does not depend on prototype-chain ‘property miss’ conditions. Generating such a condition set attaches watchpoint/condition objects that reference other structures; those references can become stale as structures are reclaimed, so a later use or repatch of the cached case can dereference freed condition/structure objects — a use-after-free, matching the ‘unexpected Safari crash’ impact. The fix removes the generateConditionsForPropertyMiss call from the DefinePrivateName cases (private fields need no property conditions), and hardens the surrounding state machine: on the private get path it adds RELEASE_ASSERT(conditionSet.isEmpty()) to enforce that private gets carry no conditions; PrivateName/PrivateNameById get kinds and SetPrivateNameById/SetPrivateNameByVal put kinds are moved to RELEASE_ASSERT_NOT_REACHED() so the caching machinery cannot silently follow a path that would build conditions for them; and tryCacheInBy’s default case becomes RELEASE_ASSERT_NOT_REACHED() instead of a silent break. The restored invariant: private-name field accesses are cached without prototype-chain property conditions, so no stale condition objects can be created or later dereferenced. The regression test defines a class with a static #private field and repeatedly instantiates it while mutating proto and allocating Float64Arrays to force structure churn and GC, exercising the previously stale conditions. INFERENCE: the exact freed object and repatch site that dereferences it are in the IC repatch/GC machinery not fully shown here; the commit establishes that the erroneous condition set was created and is now removed.

Attack Path

1. Define a class with static private fields

The page runs JavaScript declaring a class with a static #private field (as in the regression test), whose define goes through JSC’s private-name PutBy inline cache.

2. Force repeated caching with structure transitions

The script instantiates the class many times and mutates object shapes (e.g. Object.proto reassignment) so the DefinePrivateName inline cache builds property-miss condition sets.

3. Apply GC pressure

Large allocations (new Float64Array(…)) trigger garbage collection that can reclaim structures referenced by the spurious condition objects.

4. Re-run the cached access

Subsequent execution repatches/checks the cached private-name case, dereferencing a condition or structure that GC has freed.

5. Trigger the use-after-free

The dangling dereference corrupts or reads freed memory, producing the unexpected Safari crash (and, with heap grooming, a potentially exploitable primitive — standard escalation, not shown by the patch).

Impact Assessment

A use-after-free of condition/structure objects reachable from crafted JS with structure churn and GC; medium as a crash but a UAF that can be shaped into a type-confusion primitive.

Audit Directions

Condition sets on non-prototype accesses

Grep tryCachePutBy/tryCacheGetBy for generateConditionsForProperty* on private-name/own-property kinds that shouldn’t carry conditions.

Unreachable IC kinds

Ensure private-name kinds hit RELEASE_ASSERT_NOT_REACHED rather than silently building caches.

Changed Functions

Function File Change Note
tryCacheGetBy Source/JavaScriptCore/bytecode/Repatch.cpp modified Adds RELEASE_ASSERT_NOT_REACHED() for GetByKind::PrivateName/PrivateNameById and RELEASE_ASSERT(conditionSet.isEmpty()) on the isPrivate get path to enforce that private gets carry no conditions.
tryCachePutBy Source/JavaScriptCore/bytecode/Repatch.cpp modified Removes the erroneous generateConditionsForPropertyMiss() call for DefinePrivateNameById/ByVal (private fields need no property conditions) and moves SetPrivateNameById/ByVal to RELEASE_ASSERT_NOT_REACHED().
tryCacheInBy Source/JavaScriptCore/bytecode/Repatch.cpp modified Default case changed from a silent break to RELEASE_ASSERT_NOT_REACHED(), so unexpected kinds fail loudly instead of caching incorrectly.

Files Changed

Source Files

  • Source/JavaScriptCore/bytecode/Repatch.cpp

Test Files

  • JSTests/stress/static-private-fields-dont-need-property-conditions.js

Regression Test

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

diff --git a/JSTests/stress/static-private-fields-dont-need-property-conditions.js b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
new file mode 100644
index 000000000000..37ccd62442c2
--- /dev/null
+++ b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
@@ -0,0 +1,28 @@
+function f0() {
+}
+const v3 = Object.getOwnPropertyNames(this);
+this / Object;
+const v7 = f0();
+try {
+    Object(f0, true);
+} catch(e11) {
+}
+for (const v12 of v3) {
+    const v14 = this[v12];
+    for (let i16 = 0;
+        (() => {
+            class C21 extends Object {
+                static #f = v14;
+                6 = Object;
+            }
+            new C21();
+            new C21();
+            new C21();
+            Object.__proto__ = v3;
+            new Float64Array(2887);
+            return i16 < 32;
+        })();
+        i16++) {
+    }
+    Object[v12] = this;
+}
\ No newline at end of file