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 |
|---|---|---|
tryCacheGetBySource/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. |
tryCachePutBySource/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(). |
tryCacheInBySource/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
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.