CVE-2025-24264
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Gary Kwong, and an anonymous researcher
Key insight. The JIT direct-eval operations read VM from the caller frame but never set the VM’s top call frame via the tracer, so an allocation/throw during eval walked a garbage frame; installing JITOperationPrologueCallFrameTracer fixes the top-frame invariant.
Patch Details
diff --git a/JSTests/stress/eval-call-should-update-top-frame.js b/JSTests/stress/eval-call-should-update-top-frame.js
new file mode 100644
index 000000000000..711d3696244b
--- /dev/null
+++ b/JSTests/stress/eval-call-should-update-top-frame.js
@@ -0,0 +1,7 @@
+for (let i = 0; i < 10000; ++i) {
+ const v23 = (2 ** 31) - 1;
+ const v28 = (-1).toLocaleString(2, 31).padEnd(v23, "aa");
+ try {
+ eval(v28);
+ } catch { }
+}
diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp
index 5f72bf90c87c..c82b4c8784f7 100644
--- a/Source/JavaScriptCore/jit/JITOperations.cpp
+++ b/Source/JavaScriptCore/jit/JITOperations.cpp
@@ -2387,8 +2387,10 @@ JSC_DEFINE_JIT_OPERATION(operationPutByValSetPrivateFieldGeneric, void, (JSGloba
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppy, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2398,8 +2400,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppy, EncodedJSValue, (void* f
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrict, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2409,8 +2413,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrict, EncodedJSValue, (void* f
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppyTaintedByWithScope, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2420,8 +2426,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppyTaintedByWithScope, Encode
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrictTaintedByWithScope, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-24264 |
| Bugzilla | 285892 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Commit | 71951f425f93be30… |
| Advisory | Apple Advisory |
Terminology
- JIT operation call-frame tracer — JITOperationPrologueCallFrameTracer sets the VM’s current top call frame for the duration of a runtime operation invoked from JIT code.
- Direct eval operations — JIT helpers that execute a direct eval(); the callee frame is untrusted, so VM is taken from the caller frame.
- Top call frame — The VM’s record of the currently executing frame, consulted during stack walks and GC; a stale value causes crashes.
Root Cause Analysis
This fixes a crash / potential corruption in JavaScriptCore’s JIT direct-eval operations that failed to update the VM’s top call frame. operationCallDirectEvalSloppy/Strict (and the …TaintedByWithScope variants) run when a direct eval() is invoked from JIT code. They derive the VM from the caller frame because the callee frame may be garbage at entry. Pre-patch they computed VM& vm = calleeFrame->callerFrame()->deprecatedVM() but did NOT install a JITOperationPrologueCallFrameTracer, so the VM’s current top call frame was left pointing at the untrusted callee/garbage frame during the operation. If the operation subsequently allocates or throws — for example eval of an enormous string that triggers an out-of-memory or exception, as the regression test does — the engine walks the stack or runs GC using that stale/garbage top frame, causing a crash (and potentially memory corruption). The fix captures CallFrame* callFrame = calleeFrame->callerFrame() and constructs JITOperationPrologueCallFrameTracer tracer(vm, callFrame), setting the VM’s top call frame to the trusted caller for the duration of the operation. The restored invariant is that JIT runtime operations that can allocate or throw set the correct top call frame via the tracer. The test evals huge strings in a loop to force the allocation/throw path.
Attack Path
1. Call eval from JIT code
Run hot JS that reaches operationCallDirectEval* for a direct eval().
2. Force allocation/throw
eval an enormous string so the operation triggers OOM/exception handling.
3. Walk a stale top frame
Because no call-frame tracer was installed, stack walking / GC uses the garbage callee frame as the top frame.
4. Crash or corrupt
The stale-frame walk crashes (or corrupts state) in the WebContent process.
Impact Assessment
A crash and potential memory corruption in the WebContent process, reachable by calling eval() from JIT-compiled code and forcing an allocation/throw. The advisory rates it a crash; the missing top-frame update is the kind of engine-state inconsistency that can be developed into stronger primitives.
Audit Directions
Same file: tracer coverage
Grep JITOperations.cpp for JSC_DEFINE_JIT_OPERATION handlers that can allocate/throw but omit a JITOperationPrologue*CallFrameTracer.
Caller-frame VM derivation
Audit operations that take VM from callerFrame()->deprecatedVM() to confirm they also set the top call frame.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
operationCallDirectEvalSloppy / Strict / SloppyTaintedByWithScope / StrictTaintedByWithScope |
Source/JavaScriptCore/jit/JITOperations.cpp |
modified | Capture the caller frame and install JITOperationPrologueCallFrameTracer(vm, callFrame) so the VM’s top call frame is correct when the eval operation allocates or throws. |
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/stress/eval-call-should-update-top-frame.js b/JSTests/stress/eval-call-should-update-top-frame.js
new file mode 100644
index 000000000000..711d3696244b
--- /dev/null
+++ b/JSTests/stress/eval-call-should-update-top-frame.js
@@ -0,0 +1,7 @@
+for (let i = 0; i < 10000; ++i) {
+ const v23 = (2 ** 31) - 1;
+ const v28 = (-1).toLocaleString(2, 31).padEnd(v23, "aa");
+ try {
+ eval(v28);
+ } catch { }
+}