CVE-2025-43541
Overview
Background
- Patchpoint / stackmap
- B3/FTL mechanism that reserves registers and records values so an OSR exit can recover the correct runtime state.
- OSR exit argument offset
- Index into the patchpoint’s children where exit arguments begin; it must account for the result value or the exit reads the wrong slot.
- Scratch vs result register
- Scratch registers are freely clobbered; reusing the result register as scratch across an exit-capable call corrupts the produced value.
Root Cause Analysis
This fixes a register-allocation / OSR-exit-argument miscompilation in the FTL lowering of DataView byteLength that led to a type-confusion-class wrong value on out-of-bounds exit. In FTLLowerDFGToB3’s typedArrayLength/byteLength patchpoint, the pre-patch code set up the patchpoint once for all typed arrays: appendSomeRegister(base), clobber(macroClobberedGPRs), numGPScratchRegisters = 2. For the DataView case it computed osrExitArgumentOffset = patchpoint->numChildren() (with no allowance for the result value) and then called jit.loadDataViewByteLength(baseGPR, resultGPR, scratch1GPR, scratch2GPR, TypeDataView) — passing resultGPR as a working register to the byteLength load even though resultGPR is also the patchpoint’s result register, and emitting the OSR exit (OutOfBounds) at an argument offset that did not account for the result. The consequence is that on the out-of-bounds OSR exit the exit machinery reads the wrong stackmap argument / a value from a register clobbered by the byteLength computation, so the DataView’s byteLength (or the exit’s recovered value) is wrong/attacker-influenced — a type-confusion-flavored miscompile.
The fix, for the DataView branch, appends base and clobbers there, raises numGPScratchRegisters to 3, computes osrExitArgumentOffset = numChildren() + 1 (accounting for the result), uses three scratch registers for loadDataViewByteLength (not resultGPR), and finally does jit.move(scratch1GPR, resultGPR) to place the length. The non-DataView path keeps 2 scratch registers.
The restored invariant is that the result register is not used as scratch across the OSR-exit-capable byteLength load and the exit argument offset correctly skips the result. The regression test resizes an ArrayBuffer to 1 to force the OOB exit on DataView.byteLength.
Attack Path
- Tier up DataView.byteLength Repeatedly read a resizable DataView’s byteLength so the FTL compiles the DataView byteLength patchpoint.
- Force an out-of-bounds exit Resize the backing ArrayBuffer so the length check fails and the OutOfBounds OSR exit fires.
- Read a clobbered/wrong value Because resultGPR doubled as scratch and the exit offset was off-by-one, the exit recovers a wrong/attacker-influenced value for byteLength.
- Escalate Use the miscompiled length as a type-confusion/OOB primitive in the WebContent process toward stronger corruption.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
FTL typedArrayLength/byteLength lowering (DataView patchpoint)Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
modified | For TypeDataView, sets numGPScratchRegisters=3, osrExitArgumentOffset=numChildren()+1 (for the result), runs loadDataViewByteLength on scratch1/2/3 instead of resultGPR, then moves scratch1 into resultGPR; the non-DataView path keeps 2 scratch registers. |
Files Changed
JSTests/stress/data-view-byte-length-oob-exit.jsSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Audit Directions
- Same lowering: register reuseAudit other typed-array/length patchpoints in FTLLowerDFGToB3.cpp for the result register passed as a scratch to a load that can OSR-exit, and for osrExitArgumentOffset that omits the result.
- loadDataViewByteLength callersGrep for loadDataViewByteLength / loadTypedArrayByteLength and verify the scratch registers are distinct from the result register.