Medium CVSS 9.8 webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC LLInt
Bug ClassType Confusion
Tracker296042
Fix commit8b9fc1b85151 (WebKit/WebKit) +58/-23
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedan anonymous researcher
Disclosed2025-09-15

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.

Key insight
op_instanceof was omitted from the inlined-getter OSR-exit return-location plumbing that its sibling checkpointed opcodes already had, so an OSR exit from an inlined Symbol.hasInstance/prototype getter had no valid resume point and hit an unreachable assert; the fix registers op_instanceof across all four layers and prunes the checkpoint handler to only the reachable state.

Attack Path

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

The patch establishes a reachable RELEASE_ASSERT_NOT_REACHED()/invalid-resume condition triggered deterministically from script via instanceof with a JS getter on Symbol.hasInstance or prototype, so the demonstrated primitive is a controlled, reliable crash of the WebContent (renderer) process rather than a memory-corruption primitive. Nothing in the diff shows a controllable OOB write or UAF; resuming at a wrong return PC could in principle be dangerous, but the patch and tests only evidence a crash/abort, so escalation to code execution is not supported by what the commit shows. Impact is confined to the sandboxed WebContent process; it is a denial-of-service consistent with the advisory’s ‘unexpected process crash.’

Changed Functions

FunctionChangeNotes
callerReturnPC
Source/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 declaration
Source/JavaScriptCore/llint/LLIntOpcode.h
modified Adds macro(op_instanceof) so op_instanceof participates in the LLInt opcode/return-location machinery.
op_instanceof_return_location
Source/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 implementation
Source/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.js
  • JSTests/stress/instanceof-osr-exit-prototype-getter.js
  • Source/JavaScriptCore/bytecode/BytecodeList.rb
  • Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
  • Source/JavaScriptCore/llint/LLIntOpcode.h
  • Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
  • Source/JavaScriptCore/llint/LowLevelInterpreter64.asm

Audit Directions

  • Audit every checkpointed opcode for complete return-location coverage
    Cross-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 callouts
    In 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-dispatch
    In 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 tiers
    Grep 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.

Original Bug Report

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