Released December 12, 2025

CVE-2025-43541

CVSS 4.3 severity medium CWE-843 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative

Key insight. The DataView byteLength lowering used the result register as a scratch register across the out-of-bounds OSR exit and mis-set the exit argument offset, so the exit could recover a clobbered/wrong length.

Patch Details

diff --git a/JSTests/stress/data-view-byte-length-oob-exit.js b/JSTests/stress/data-view-byte-length-oob-exit.js
new file mode 100644
index 000000000000..5c350321b492
--- /dev/null
+++ b/JSTests/stress/data-view-byte-length-oob-exit.js
@@ -0,0 +1,18 @@
+const crash_point= 0x41414141;
+const loop_start = crash_point&0xffffff00;
+const loop_end   = crash_point+1;
+for (let i=0; i<5000; i++){
+    const get_byte_length = arr => arr.byteLength;
+    const ab = new ArrayBuffer(99, {maxByteLength: 99});
+    const dv = new DataView(ab, 1, 44);
+    for (let counter = loop_start; counter < loop_end; counter++) {
+        if (counter == loop_end-2) {
+          ab.resize(1);
+          get_byte_length(1);
+        }else{
+          try {
+            get_byte_length(dv);
+          } catch (e) {}
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index 7e662ba3b9a0..7b533ce85a9b 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -5948,12 +5948,12 @@ IGNORE_CLANG_WARNINGS_END
 #else
             PatchpointValue* patchpoint = m_out.patchpoint(Int32);
 #endif
-            patchpoint->appendSomeRegister(base);
-            patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
-            patchpoint->numGPScratchRegisters = 2;
-
             if (typedArrayType.has_value() && typedArrayType.value() == TypeDataView) {
-                unsigned osrExitArgumentOffset = patchpoint->numChildren();
+                patchpoint->appendSomeRegister(base);
+                patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
+                patchpoint->numGPScratchRegisters = 3;
+
+                unsigned osrExitArgumentOffset = patchpoint->numChildren() + /* result */ 1;
                 OSRExitDescriptor* exitDescriptor = appendOSRExitDescriptor(jsValueValue(base), m_node);
                 patchpoint->appendColdAnys(buildExitArguments(exitDescriptor, m_origin.forExit, jsValueValue(base)));
 
@@ -5969,19 +5969,24 @@ IGNORE_CLANG_WARNINGS_END
                     GPRReg baseGPR = params[1].gpr();
                     GPRReg scratch1GPR = params.gpScratch(0);
                     GPRReg scratch2GPR = params.gpScratch(1);
+                    GPRReg scratch3GPR = params.gpScratch(2);
 
                     RefPtr<OSRExitHandle> handle = exitDescriptor->emitOSRExitLater(*state, OutOfBounds, origin, params, nodeIndex, osrExitArgumentOffset);
                     RefPtr<FTL::JITCode> jitCode = state->jitCode;
 
-                    auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(baseGPR, resultGPR, scratch1GPR, scratch2GPR, TypeDataView);
+                    auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(baseGPR, scratch1GPR, scratch2GPR, scratch3GPR, TypeDataView);
                     jit.addLinkTask([=, protectedJitCode = jitCode, outOfBoundsJump = outOfBounds](LinkBuffer& linkBuffer) {
                         linkBuffer.link(outOfBoundsJump, linkBuffer.locationOf<NoPtrTag>(handle->label));
                     });
                     doneCases.link(&jit);
+                    jit.move(scratch1GPR, resultGPR);
                 });
                 return patchpoint;
             }
 
+            patchpoint->appendSomeRegister(base);
+            patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
+            patchpoint->numGPScratchRegisters = 2;
             patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) {
                 JIT_COMMENT(jit, "typedArrayLength");
                 AllowMacroScratchRegisterUsage allowScratch(jit);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43541
Bugzilla 301257
Component JSC
Bug Class TypeConfusion
Severity medium
Commit ef1aba9e847aa7b6…
Advisory Apple Advisory

Terminology

  • 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

1. Tier up DataView.byteLength

Repeatedly read a resizable DataView’s byteLength so the FTL compiles the DataView byteLength patchpoint.

2. Force an out-of-bounds exit

Resize the backing ArrayBuffer so the length check fails and the OutOfBounds OSR exit fires.

3. 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.

4. Escalate

Use the miscompiled length as a type-confusion/OOB primitive in the WebContent process toward stronger corruption.

Impact Assessment

An FTL miscompilation in the WebContent process where a DataView byteLength OSR exit can recover a wrong, attacker-influenced value — a type-confusion-class primitive. JIT length/type miscompiles are historically strong stepping stones to arbitrary read/write in JSC; the advisory rates it a crash but the class is high-value.

Audit Directions

Same lowering: register reuse

Audit 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 callers

Grep for loadDataViewByteLength / loadTypedArrayByteLength and verify the scratch registers are distinct from the result register.

Changed Functions

Function File Change Note
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

Source Files

  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

Test Files

  • JSTests/stress/data-view-byte-length-oob-exit.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/data-view-byte-length-oob-exit.js b/JSTests/stress/data-view-byte-length-oob-exit.js
new file mode 100644
index 000000000000..5c350321b492
--- /dev/null
+++ b/JSTests/stress/data-view-byte-length-oob-exit.js
@@ -0,0 +1,18 @@
+const crash_point= 0x41414141;
+const loop_start = crash_point&0xffffff00;
+const loop_end   = crash_point+1;
+for (let i=0; i<5000; i++){
+    const get_byte_length = arr => arr.byteLength;
+    const ab = new ArrayBuffer(99, {maxByteLength: 99});
+    const dv = new DataView(ab, 1, 44);
+    for (let counter = loop_start; counter < loop_end; counter++) {
+        if (counter == loop_end-2) {
+          ab.resize(1);
+          get_byte_length(1);
+        }else{
+          try {
+            get_byte_length(dv);
+          } catch (e) {}
+        }
+    }
+}
\ No newline at end of file