CVE-2026-64757
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Milad Nasr and Nicholas Carlini with Claude, Anthropic
Key insight. ScopedArgumentsTable kept a separate m_length that could drift from the m_arguments buffer size, so indexing by length could exceed the allocation; deriving length from the buffer and growing the parallel arrays together (checked) removes the desync.
Patch Details
diff --git a/Source/JavaScriptCore/runtime/CachedTypes.cpp b/Source/JavaScriptCore/runtime/CachedTypes.cpp
index 252090d11c10..7c6bdf8922d0 100644
--- a/Source/JavaScriptCore/runtime/CachedTypes.cpp
+++ b/Source/JavaScriptCore/runtime/CachedTypes.cpp
@@ -1202,15 +1202,15 @@ class CachedScopedArgumentsTable : public CachedObject<ScopedArgumentsTable> {
public:
void encode(Encoder& encoder, const ScopedArgumentsTable& scopedArgumentsTable)
{
- m_length = scopedArgumentsTable.m_length;
- m_arguments.encode(encoder, scopedArgumentsTable.m_arguments.get(), m_length);
+ m_length = scopedArgumentsTable.m_arguments.size();
+ m_arguments.encode(encoder, scopedArgumentsTable.m_arguments.span().data(), m_length);
}
ScopedArgumentsTable* decode(Decoder& decoder) const
{
ScopedArgumentsTable* scopedArgumentsTable = ScopedArgumentsTable::tryCreate(decoder.vm(), m_length);
RELEASE_ASSERT(scopedArgumentsTable); // We crash here. This is unlikely to continue execution if we hit this condition when decoding UnlinkedCodeBlock.
- m_arguments.decode(decoder, scopedArgumentsTable->m_arguments.get(), m_length);
+ m_arguments.decode(decoder, scopedArgumentsTable->m_arguments.mutableSpan().data(), m_length);
return scopedArgumentsTable;
}
diff --git a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
index 0dccadb737e8..24ff2effa9e2 100644
--- a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
+++ b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
@@ -38,7 +38,6 @@ const ClassInfo ScopedArgumentsTable::s_info = { "ScopedArgumentsTable"_s, nullp
ScopedArgumentsTable::ScopedArgumentsTable(VM& vm)
: Base(vm, vm.scopedArgumentsTableStructure.get())
- , m_length(0)
, m_locked(false)
{
}
@@ -66,21 +65,20 @@ ScopedArgumentsTable* ScopedArgumentsTable::tryCreate(VM& vm, uint32_t length)
ScopedArgumentsTable* result = new (NotNull, buffer) ScopedArgumentsTable(vm);
result->finishCreation(vm);
- result->m_length = length;
- result->m_arguments = ArgumentsPtr::tryCreate(length);
- if (!result->m_arguments) [[unlikely]]
+ if (!result->m_arguments.tryGrow(length)) [[unlikely]]
return nullptr;
- result->m_watchpointSets.fill(nullptr, length);
+ if (!result->m_watchpointSets.tryGrow(length)) [[unlikely]]
+ return nullptr;
+ std::ranges::fill(result->m_watchpointSets.mutableSpan(), nullptr);
return result;
}
ScopedArgumentsTable* ScopedArgumentsTable::tryClone(VM& vm)
{
- ScopedArgumentsTable* result = tryCreate(vm, m_length);
+ ScopedArgumentsTable* result = tryCreate(vm, m_arguments.size());
if (!result) [[unlikely]]
return nullptr;
- for (unsigned i = m_length; i--;)
- result->at(i) = this->at(i);
+ result->m_arguments = m_arguments;
result->m_watchpointSets = this->m_watchpointSets;
return result;
}
@@ -88,23 +86,21 @@ ScopedArgumentsTable* ScopedArgumentsTable::tryClone(VM& vm)
ScopedArgumentsTable* ScopedArgumentsTable::trySetLength(VM& vm, uint32_t newLength)
{
if (!m_locked) [[likely]] {
- ArgumentsPtr newArguments = ArgumentsPtr::tryCreate(newLength, newLength);
- if (!newArguments) [[unlikely]]
+ size_t oldSize = m_watchpointSets.size();
+ if (!m_arguments.tryGrow(newLength))
+ return nullptr;
+ if (!m_watchpointSets.tryGrow(newLength))
return nullptr;
- for (unsigned i = std::min(m_length, newLength); i--;)
- newArguments.at(i) = this->at(i);
- m_length = newLength;
- m_arguments = WTF::move(newArguments);
- m_watchpointSets.resize(newLength);
+ if (newLength > oldSize)
+ std::ranges::fill(m_watchpointSets.mutableSpan().subspan(oldSize), nullptr);
return this;
}
-
+
ScopedArgumentsTable* result = tryCreate(vm, newLength);
if (!result) [[unlikely]]
return nullptr;
- m_watchpointSets.resize(newLength);
- for (unsigned i = std::min(m_length, newLength); i--;) {
- result->at(i) = this->at(i);
+ for (unsigned i = std::min<uint32_t>(m_arguments.size(), newLength); i--;) {
+ result->m_arguments[i] = this->m_arguments[i];
result->m_watchpointSets[i] = this->m_watchpointSets[i];
}
return result;
@@ -121,7 +117,7 @@ ScopedArgumentsTable* ScopedArgumentsTable::trySet(VM& vm, uint32_t i, ScopeOffs
return nullptr;
} else
result = this;
- result->at(i) = value;
+ result->m_arguments[i] = value;
return result;
}
diff --git a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
index fe536388831e..261ce1bdfc3a 100644
--- a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
+++ b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
@@ -29,7 +29,7 @@
#include <JavaScriptCore/ScopeOffset.h>
#include <JavaScriptCore/VM.h>
#include <wtf/Assertions.h>
-#include <wtf/CagedUniquePtr.h>
+#include <wtf/Vector.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -67,10 +67,10 @@ public:
static void destroy(JSCell*);
- uint32_t length() const { return m_length; }
+ uint32_t length() const { return m_arguments.size(); }
ScopedArgumentsTable* trySetLength(VM&, uint32_t newLength);
- ScopeOffset get(uint32_t i) const { return at(i); }
+ ScopeOffset get(uint32_t i) const { return m_arguments.at(i); }
WatchpointSet* getWatchpointSet(uint32_t i) const { return m_watchpointSets.at(i); }
void lock()
@@ -86,24 +86,17 @@ public:
static Structure* createStructure(VM&, JSGlobalObject*, JSValue prototype);
- static constexpr ptrdiff_t offsetOfLength() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_length); }
- static constexpr ptrdiff_t offsetOfArguments() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments); }
+ using ArgumentScopeBufferType = Vector<ScopeOffset>;
- typedef CagedUniquePtr<Gigacage::Primitive, ScopeOffset> ArgumentsPtr;
+ static constexpr ptrdiff_t offsetOfLength() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments) + ArgumentScopeBufferType::sizeMemoryOffset(); }
+ static constexpr ptrdiff_t offsetOfArguments() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments) + ArgumentScopeBufferType::bufferMemoryOffset(); }
private:
ScopedArgumentsTable* tryClone(VM&);
-
- ScopeOffset& at(uint32_t i) const
- {
- ASSERT_WITH_SECURITY_IMPLICATION(i < m_length);
- return m_arguments.get()[i];
- }
- uint32_t m_length;
- bool m_locked; // Being locked means that there are multiple references to this object and none of them expect to see the others' modifications. This means that modifications need to make a copy first.
- ArgumentsPtr m_arguments;
+ ArgumentScopeBufferType m_arguments;
Vector<WatchpointSet*> m_watchpointSets;
+ bool m_locked; // Being locked means that there are multiple references to this object and none of them expect to see the others' modifications. This means that modifications need to make a copy first.
};
} // namespace JSC
diff --git a/Source/WTF/wtf/Vector.h b/Source/WTF/wtf/Vector.h
index 31240327b4bb..806004a5a1de 100644
--- a/Source/WTF/wtf/Vector.h
+++ b/Source/WTF/wtf/Vector.h
@@ -765,6 +765,7 @@ public:
[[nodiscard]] size_t size() const { return m_size; }
[[nodiscard]] size_t sizeInBytes() const { return static_cast<size_t>(m_size) * sizeof(T); }
static constexpr ptrdiff_t sizeMemoryOffset() { return OBJECT_OFFSETOF(Vector, m_size); }
+ static constexpr ptrdiff_t bufferMemoryOffset() { return Base::bufferMemoryOffset(); }
[[nodiscard]] size_t capacity() const { return Base::capacity(); }
[[nodiscard]] bool isEmpty() const { return !size(); }
[[nodiscard]] std::span<const T> span() const LIFETIME_BOUND { return { data(), size() }; }
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64757 |
| Bugzilla | 315082 |
| Component | JSC |
| Bug Class | OOB |
| Severity | medium |
| Commit | 72272dcc4feb8412… |
| Advisory | Apple Advisory |
Terminology
- ScopedArgumentsTable — JSC metadata mapping arguments indices to scope offsets, with a parallel watchpoint-set array; both must have the same length.
- Length vs buffer size — A separate length field can drift from the real allocation size, so indexing by length can exceed the buffer.
- CachedTypes (bytecode cache) — Serializes JSC structures like ScopedArgumentsTable; a length/buffer mismatch there decodes into an OOB.
Root Cause Analysis
This fixes an out-of-bounds access caused by a length field that could desynchronize from the backing buffer in JSC’s ScopedArgumentsTable (and its bytecode-cache serialization). ScopedArgumentsTable stored a separate m_length alongside m_arguments (the scope-offset array) and a parallel m_watchpointSets array; various operations (tryCreate, tryClone, trySetLength) and the CachedScopedArgumentsTable encode/decode used m_length to size and index m_arguments. If m_length and the actual m_arguments buffer size diverged — e.g. an allocation grew one but not the other, or the cached m_length did not match the real arguments array — code indexing m_arguments by m_length would read or write past the buffer, an out-of-bounds access (memory corruption / crash). The fix removes the redundant m_length and derives the length from m_arguments.size(), makes m_arguments and m_watchpointSets growable together via checked tryGrow (both grown to the same new length, failing atomically), and uses spans for encode/decode (m_arguments.span().data() / mutableSpan().data()) so the serialized length always matches the buffer. tryClone now copies m_arguments/m_watchpointSets wholesale, and trySetLength grows both arrays consistently. The restored invariant is that the arguments length is always exactly the buffer size and the two parallel arrays stay the same length, so no index derived from length can exceed the allocation. INFERENCE: the exact trigger that desynchronized length and buffer is not shown in this hunk; the patch removes the separate length field that made the desync possible.
Attack Path
1. Use scoped arguments
Run JS with functions whose arguments object is captured by a scope, creating a ScopedArgumentsTable.
2. Desynchronize length and buffer
Through resize/clone/serialization, get m_length to differ from the actual m_arguments buffer size.
3. Index past the buffer
An access using the stale length reads/writes beyond the m_arguments allocation.
4. Out-of-bounds / corruption
The OOB access corrupts or crashes the WebContent process.
Impact Assessment
An out-of-bounds access in the WebContent process from a ScopedArgumentsTable whose length could diverge from its backing buffer, reachable via scoped-arguments usage (and cached bytecode). The advisory rates it a crash; length/buffer desync OOBs are controllable memory-safety bugs.
Audit Directions
Separate length vs container size
Grep JSC runtime structures for a stored m_length paralleling a resizable buffer; prefer deriving length from the container to avoid drift.
Parallel-array growth
Audit tryCreate/trySetLength/clone paths that grow one of several parallel arrays to confirm all are grown together with checked allocation.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
ScopedArgumentsTable::tryCreate / tryClone / trySetLength |
Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp |
modified | Removes the separate m_length, derives length from m_arguments.size(), and grows m_arguments and m_watchpointSets together with checked tryGrow so the two parallel arrays and the length stay consistent. |
CachedScopedArgumentsTable::encode / decode |
Source/JavaScriptCore/runtime/CachedTypes.cpp |
modified | Uses m_arguments.size() and span data for serialization so the encoded length matches the actual buffer, preventing an OOB on decode. |
Files Changed
Source Files
Source/JavaScriptCore/runtime/CachedTypes.cppSource/JavaScriptCore/runtime/ScopedArgumentsTable.cppSource/JavaScriptCore/runtime/ScopedArgumentsTable.hSource/WTF/wtf/Vector.h