CVE-2025-43541
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