CVE-2025-43342
Overview
Background
- OSR exit
- On-Stack Replacement exit: the mechanism that bails out of optimized (DFG/FTL) code back into a lower tier (baseline/LLInt) when a speculative assumption fails, requiring reconstruction of interpreter state and a valid resume PC.
- Checkpoint
- A sub-instruction resume point within a single bytecode op (here op_instanceof’s getHasInstance/getPrototype/instanceof) that lets execution suspend and resume across a JS callout made in the middle of that op.
- Symbol.hasInstance
- The well-known symbol whose method a constructor exposes to customize the behavior of the instanceof operator, so ‘x instanceof C’ can invoke arbitrary JS.
- Return location (LLINT_RETURN_LOCATION)
- A per-opcode label in the LLInt where execution resumes after a call/callout returns, used by the OSR exit compiler to know where to continue in the interpreter.
- Inlining of getters
- An optimization where DFG/FTL embeds the body of an accessor getter directly into the caller, so an OSR exit while inside that inlined getter must map back to the enclosing bytecode’s return machinery.
Root Cause Analysis
op_instanceof is a bytecode operation that can call back into JavaScript at two points: fetching the Symbol.hasInstance method on the constructor, and (when the default hasInstance is used) fetching the constructor’s ‘prototype’ property. In LLInt these callouts are modeled as checkpoints (OpInstanceof::getHasInstance, getPrototype, instanceof), and when a DFG/FTL-compiled frame that inlined one of these getters OSR-exits back into the baseline/LLInt tier, execution must resume at a well-defined return location tied to the specific opcode and checkpoint.
The bug is that op_instanceof was never registered in the machinery that maps an inlined-call OSR exit back to its LLInt return PC. LLIntOpcode.h did not declare op_instanceof as an opcode with a return location, BytecodeList.rb did not declare op_instanceof_return_location, LowLevelInterpreter64.asm had no .getHasInstanceInlinedGetterOSRReturnPoint label emitting getterSetterOSRExitReturnPoint for op_instanceof, and callerReturnPC() in DFGOSRExitCompilerCommon.cpp had no case for op_instanceof, so it hit RELEASE_ASSERT_NOT_REACHED(). The invariant violated is that every bytecode capable of OSR-exiting from an inlined getter must have a corresponding LLInt return location so the interpreter can reconstruct where to continue. Because that mapping was absent, when a Symbol.hasInstance (or prototype) getter was inlined into DFG code and then forced to OSR-exit while executing that getter, the exit compiler could not find a valid return PC. The slow-path handler llint_slow_path_checkpoint_osr_exit_from_inlined_call in LLIntSlowPaths.cpp also mishandled the checkpoint states: it eagerly decoded dst/value/hasInstanceOrPrototype up front and tried to re-run the hasInstance dispatch and default-hasInstance logic inside the checkpoint resume, when in fact getHasInstance and instanceof checkpoints should never be reached via an inlined-call OSR exit (the former is not handled by a checkpoint here, the latter has no inlined calls at the last checkpoint).
The fix adds the missing op_instanceof return-location plumbing across all four layers and rewrites the slow path so only the getPrototype checkpoint does work (reading m_dst, the value operand, and the decoded prototype result, then computing defaultHasInstance), while getHasInstance and instanceof both RELEASE_ASSERT_NOT_REACHED. This restores the invariant that an inlined-getter OSR exit for op_instanceof resumes at a real return point and that only the reachable checkpoint state runs.
Attack Path
- Install a JS getter on the hasInstance/prototype slot Define an accessor via Object.defineProperty on Proxy[Symbol.hasInstance] or Proxy.prototype whose getter is a plain JS function (f20 in the tests), so that evaluating ‘Object instanceof Proxy’ must call into that getter.
- Warm up a wrapper for tier-up Repeatedly call an outer function (F1) that executes ‘Object instanceof Proxy’ in a hot loop (testLoopCount ~1e4) so JSC compiles it in DFG/FTL and inlines the small getter into the op_instanceof callout.
- Force an OSR exit from inside the inlined getter The getter triggers a bailout (in the tests, an explicit OSRExit() intrinsic; in the wild, any speculation failure such as a type check) while control is inside the inlined Symbol.hasInstance getter, so the engine must OSR-exit the op_instanceof frame back to LLInt/baseline.
- Hit the missing return-location mapping callerReturnPC() in the DFG OSR exit compiler tries to resolve the LLInt return PC for the calling op_instanceof and, before the patch, falls into RELEASE_ASSERT_NOT_REACHED() (or resumes at an invalid checkpoint), crashing the WebContent process.
- Repeatable denial of service Because the crash is reached deterministically from ordinary script, the attacker can crash the renderer on demand from a web page.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
callerReturnPCSource/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp |
modified | Adds an else-if for op_instanceof that sets jumpTarget to LLINT_RETURN_LOCATION(op_instanceof), so an inlined-getter OSR exit from op_instanceof resolves to a real LLInt return PC instead of RELEASE_ASSERT_NOT_REACHED(). |
llint_slow_path_checkpoint_osr_exit_from_inlined_call (op_instanceof case)Source/JavaScriptCore/llint/LLIntSlowPaths.cpp |
modified | Rewrites the op_instanceof checkpoint handler: getHasInstance and instanceof now both RELEASE_ASSERT_NOT_REACHED (unreachable via inlined-call exit), and only getPrototype reads m_dst/value/decoded prototype and computes JSObject::defaultHasInstance; removes the eager up-front decoding and the erroneous re-dispatch of hasInstance. |
op_instanceof opcode declarationSource/JavaScriptCore/llint/LLIntOpcode.h |
modified | Adds macro(op_instanceof) so op_instanceof participates in the LLInt opcode/return-location machinery. |
op_instanceof_return_locationSource/JavaScriptCore/bytecode/BytecodeList.rb |
added | Declares the op_instanceof_return_location pseudo-op so a return location label is generated for op_instanceof, matching the pattern used by get_by_val, in_by_val, enumerator_get_by_val, etc. |
llintOpWithMetadata(op_instanceof, ...) LLInt implementationSource/JavaScriptCore/llint/LowLevelInterpreter64.asm |
modified | Adds the .getHasInstanceInlinedGetterOSRReturnPoint label that emits getterSetterOSRExitReturnPoint(op_instanceof, size), profiles the returned value, and stores it into m_hasInstanceOrPrototype before falling through to .getPrototype; provides the actual return point for an inlined Symbol.hasInstance getter OSR exit. |
Files Changed
JSTests/stress/instanceof-osr-exit-hasInstance-getter.jsJSTests/stress/instanceof-osr-exit-prototype-getter.jsSource/JavaScriptCore/bytecode/BytecodeList.rbSource/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cppSource/JavaScriptCore/llint/LLIntOpcode.hSource/JavaScriptCore/llint/LLIntSlowPaths.cppSource/JavaScriptCore/llint/LowLevelInterpreter64.asm
Audit Directions
- Audit every checkpointed opcode for complete return-location coverageCross-reference the opcodes handled in callerReturnPC() (DFGOSRExitCompilerCommon.cpp) and llint_slow_path_checkpoint_osr_exit_from_inlined_call against the macro list in LLIntOpcode.h and the *_return_location entries in BytecodeList.rb; any opcode with checkpoints/callouts that appears in one list but not all of them is a candidate for the same missing-return-location bug.
- Verify getterSetterOSRExitReturnPoint labels exist for all inlined-getter calloutsIn LowLevelInterpreter64.asm (and the 32-bit counterpart if present) grep for getterSetterOSRExitReturnPoint and overridesHasInstance; confirm every opcode that can inline a getter (instanceof, in_by_id, get_by_val with proxies, etc.) has a corresponding *InlinedGetterOSRReturnPoint label.
- Review checkpoint handlers for eager decoding and unreachable-state re-dispatchIn LLIntSlowPaths.cpp, look for other checkpoint cases that decode dst/operands before switching on bytecodeIndex.checkpoint() or that re-execute op logic in a resume path; the fixed op_instanceof pattern shows only the truly reachable checkpoint should do work while the others RELEASE_ASSERT_NOT_REACHED.
- Hunt other instanceof/hasInstance speculation paths across tiersGrep for defaultHasInstance, implementsDefaultHasInstance, functionProtoHasInstanceSymbolFunction, and OpInstanceof across DFG/FTL/LLInt to find related speculation or checkpoint reconstruction that assumes the callout never OSR-exits from an inlined getter.