Medium CVSS 4.3 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC Bytecode
Bug ClassUAF
Tracker285643
Fix commitd7bd7d8f7cdf (WebKit/WebKit) +37/-8
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
Creditedrheza (@ginggilBesel)
Disclosed2025-03-31

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.

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.

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.

Changed Functions

FunctionChangeNotes
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

  • JSTests/stress/static-private-fields-dont-need-property-conditions.js
  • Source/JavaScriptCore/bytecode/Repatch.cpp

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.

Original Bug Report

The reporter's bug is still restricted on the tracker.