CVE-2026-64784
Overview
Advisory: Apple Security Advisory
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Janggoon Lee of Out of Bounds, OpenAI Codex Security - Amy Burnett
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64784 |
| Bugzilla | 317632 |
| Component | JSC |
| Bug Class | OOB |
| Severity | medium |
| Commit | 97df94ead028cddc… |
| Advisory | Apple Advisory |
Root Cause Analysis
UnlinkedMetadataTable::finalize() computed metadata buffer offsets using unchecked unsigned arithmetic. When compiling JavaScript functions with tens of millions of bytecode instructions, the cumulative offset calculation (numberOfEntries * metadataSize) overflows the 32-bit unsigned integer, wrapping to a small value. This causes a heap allocation that is far too small for the actual metadata, and all subsequent metadata reads/writes go out of bounds. The fix replaces raw unsigned arithmetic with CheckedUint32, detects the overflow before allocating, and propagates a failure boolean up through UnlinkedCodeBlockGenerator::finalize() to BytecodeGenerator::generate(), which then returns an OutOfMemory parser error instead of crashing.
Attack Path
1. Craft oversized JS function
Attacker serves a web page that dynamically builds a JavaScript function with ~44 million repeated statements (e.g., ‘a();’.repeat(44739242)), causing the bytecode compiler to emit an extremely large instruction stream.
2. Trigger bytecode compilation
The engine invokes BytecodeGenerator::generate(), which iterates over the massive AST and emits bytecode for each statement, incrementing per-opcode metadata entry counts in UnlinkedMetadataTable.
3. Overflow in finalize()
During UnlinkedMetadataTable::finalize(), the engine computes total metadata size as offset = Σ(numberOfEntries * metadataSize). The 32-bit unsigned multiplication/addition overflows, wrapping offset to a small value.
4. Undersized allocation
The engine calls malloc(valueProfileSize + sizeof(LinkingData) + offsetTableSize + offset) with the wrapped (tiny) offset, allocating a buffer orders of magnitude too small for the real metadata.
5. OOB access on metadata use
When the engine later links or accesses metadata for any opcode, it writes or reads past the allocated buffer boundary, causing an out-of-bounds heap access and an unexpected Safari crash.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
UnlinkedMetadataTable::finalize |
Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp |
modified | Replaced unchecked unsigned offset arithmetic with CheckedUint32; added overflow checks; returns bool instead of void. |
UnlinkedMetadataTable::finalize |
Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h |
modified | Changed signature from void finalize() to [[nodiscard]] bool finalize(). |
UnlinkedCodeBlockGenerator::finalize |
Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp |
modified | Now returns bool; propagates metadataOK from m_metadata->finalize() to caller. |
UnlinkedCodeBlockGenerator::finalize |
Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h |
modified | Changed signature from void finalize() to [[nodiscard]] bool finalize(). |
BytecodeGenerator::generate |
Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp |
modified | Checks finalize() return value; returns ParserError::OutOfMemory if metadata overflowed. |
unlinked-metadata-table-finalize-overflow.js |
JSTests/stress/unlinked-metadata-table-finalize-overflow.js |
added | Regression test: 44-million-statement function must throw RangeError instead of crashing. |
Files Changed
Source Files
Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cppSource/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.hSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.cppSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.hSource/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
Test Files
JSTests/stress/unlinked-metadata-table-finalize-overflow.js
Patch Preview
diff --git a/JSTests/stress/unlinked-metadata-table-finalize-overflow.js b/JSTests/stress/unlinked-metadata-table-finalize-overflow.js
new file mode 100644
index 000000000000..4cd8dc4bd73d
--- /dev/null
+++ b/JSTests/stress/unlinked-metadata-table-finalize-overflow.js
@@ -0,0 +1,18 @@
+//@ skip if $buildType == "debug" or $memoryLimited or $addressBits <= 32
+//@ slow!
+//@ runDefault
+
+let n = 44739242;
+let s = 'a();'.repeat(n);
+let f = new Function('a', s);
+
+let caught = false;
+try {
+ f(function() { });
+} catch (e) {
+ caught = true;
+ if (!(e instanceof RangeError))
+ throw new Error("Expected RangeError but got: " + e);
+}
+if (!caught)
+ throw new Error("Expected RangeError to be thrown");
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp
index d3149828198e..9e9d552623e2 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp
+++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019-2024 Apple Inc. All rights reserved.
+ * Copyright (C) 2019-2024, 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -57,14 +57,15 @@ void UnlinkedCodeBlockGenerator::addTypeProfilerExpressionInfo(unsigned instruct
m_typeProfilerInfoMap.set(instructionOffset, range);
}
-void UnlinkedCodeBlockGenerator::finalize(std::unique_ptr<JSInstructionStream> instructions)
+bool UnlinkedCodeBlockGenerator::finalize(std::unique_ptr<JSInstructionStream> instructions)
{
ASSERT(instructions);
+ bool metadataOK = true;
{
Locker locker { m_codeBlock->cellLock() };
m_codeBlock->m_instructions = WTF::move(instructions);
m_codeBlock->allocateSharedProfiles(m_numBinaryArithProfiles, m_numUnaryArithProfiles);
- m_codeBlock->m_metadata->finalize();
+ metadataOK = m_codeBlock->m_metadata->finalize();
m_codeBlock->m_identifiers = WTF::move(m_identifiers);
m_codeBlock->m_constantRegisters = WTF::move(m_constantRegisters);
@@ -100,6 +101,7 @@ void UnlinkedCodeBlockGenerator::finalize(std::unique_ptr<JSInstructionStream> i
}
m_vm.writeBarrier(m_codeBlock.get());
m_vm.heap.reportExtraMemoryAllocated(m_codeBlock.get(), m_codeBlock->m_instructions->sizeInBytes() + m_codeBlock->metadataSizeInBytes());
+ return metadataOK;
}
UnlinkedHandlerInfo* UnlinkedCodeBlockGenerator::handlerForBytecodeIndex(BytecodeIndex bytecodeIndex, RequiredHandler requiredHandler)
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h
index b2de7a9a77c3..6096a6da0526 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h
+++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019-2024 Apple Inc. All rights reserved.
+ * Copyright (C) 2019-2024, 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -185,7 +185,7 @@ class UnlinkedCodeBlockGenerator {
void applyModification(BytecodeRewriter&);
- void finalize(std::unique_ptr<JSInstructionStream>);
+ [[nodiscard]] bool finalize(std::unique_ptr<JSInstructionStream>);
void NODELETE dump(PrintStream&) const;
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp b/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp
index 9796701f7d64..290af4d71e45 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp
+++ b/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019-2023 Apple Inc. All rights reserved.
+ * Copyright (C) 2019-2023, 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -27,6 +27,7 @@
#include "UnlinkedMetadataTable.h"
#include "BytecodeStructs.h"
+#include <wtf/CheckedArithmetic.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -98,41 +99,79 @@ void MetadataStatistics::reportMetadataStatistics()
}
#endif
-void UnlinkedMetadataTable::finalize()
+bool UnlinkedMetadataTable::finalize()
{
ASSERT(!m_isFinalized);
m_isFinalized = true;
if (!m_hasMetadata) {
MetadataTableMalloc::free(m_rawBuffer);
m_rawBuffer = nullptr;
- return;
+ return true;
}
- unsigned offset = s_offset16TableSize;
+ unsigned offset;
+ unsigned valueProfileSize;
{
+ CheckedUint32 checkedOffset = s_offset16TableSize;
Offset32* buffer = preprocessBuffer();
- for (unsigned i = 0; i < s_offsetTableEntries - 1; i++) {
+ for (unsigned i = 0; i < s_offsetTableEntries - 1 && !checkedOffset.hasOverflowed(); i++) {
unsigned numberOfEntries = buffer[i];
if (!numberOfEntries) {
- buffer[i] = offset;
+ buffer[i] = checkedOffset.value();
continue;
}
- buffer[i] = offset; // We align when we access this.
+ buffer[i] = checkedOffset.value(); // We align when we access this.
unsigned alignment = metadataAlignment(static_cast<OpcodeID>(i));
ASSERT(alignment <= s_maxMetadataAlignment);
#if CPU(ADDRESS64)
// This is only necessary for the first metadata entry, if the buffer
// is 4-byte aligned and the entry has an alignment requirement of 8
- ASSERT(offset == roundUpToMultipleOf(alignment, offset) || offset == s_offset16TableSize);
+ ASSERT(checkedOffset.value() == roundUpToMultipleOf(alignment, checkedOffset.value()) || checkedOffset.value() == s_offset16TableSize);
#endif
- offset = roundUpToMultipleOf(alignment, offset);
+ unsigned alignedOffset = roundUpToMultipleOf(alignment, checkedOffset.value());
+ if (alignedOffset < checkedOffset.value()) {
+ checkedOffset.overflowed();
+ break;
+ }
+ checkedOffset = alignedOffset;
- offset += numberOfEntries * metadataSize(static_cast<OpcodeID>(i));
+ checkedOffset += CheckedUint32(numberOfEntries) * metadataSize(static_cast<OpcodeID>(i));
#if ENABLE(METADATA_STATISTICS)
+ // In the unlikely event of an overflow, MetadataStatistics::perOpcodeCount
+ // will not be accurate. But this is OK because these stats are only used for
+ // development time analysis where overflow is not expected.
MetadataStatistics::perOpcodeCount[i] += numberOfEntries;
#endif
}
+
+ // Each computed offset is stored as Offset32 (with an additional s_offset32TableSize bias in the
+ // 32-bit layout) and totalSize() sums valueProfileSize with that biased offset as unsigned.
+ // Reject any function whose metadata cannot be addressed within those limits.
+ CheckedUint32 checkedValueProfileSize = m_numValueProfiles;
+ checkedValueProfileSize *= static_cast<unsigned>(sizeof(ValueProfile));
+
+ // On 32-bits, also guard against potential malloc size overflow in the newBuffer
+ // allocation below, where we add sizeof(LinkingData). On 64-bit, sizes are
+ // auto-casted to a 64-bit size_t that can handle the addition, and hence, does
+ // not need this padding to reserve space for the size of sizeof(LinkingData).
+ unsigned paddingFor32Bit = 0;
+ if constexpr (sizeof(size_t) == sizeof(unsigned))
+ paddingFor32Bit = sizeof(LinkingData);
+
+ if ((checkedOffset + s_offset32TableSize + checkedValueProfileSize + paddingFor32Bit).hasOverflowed()) [[unlikely]] {
+ MetadataTableMalloc::free(m_rawBuffer);
+ m_rawBuffer = nullptr;
+ m_hasMetadata = false;
+ m_is32Bit = false;
+ m_numValueProfiles = 0;
+ return false; // Failure.
+ }
+
+ ASSERT(!checkedOffset.hasOverflowed());
+ ASSERT(!checkedValueProfileSize.hasOverflowed());
+ offset = checkedOffset.value();
+ valueProfileSize = checkedValueProfileSize.value();
buffer[s_offsetTableEntries - 1] = offset;
m_is32Bit = offset > UINT16_MAX;
}
@@ -148,7 +187,6 @@ void UnlinkedMetadataTable::finalize()
});
#endif
- unsigned valueProfileSize = m_numValueProfiles * sizeof(ValueProfile);
if (m_is32Bit) {
// offset already accounts for s_offset16TableSize
uint8_t* newBuffer = reinterpret_cast_ptr<uint8_t*>(MetadataTableMalloc::malloc(valueProfileSize + sizeof(LinkingData) + s_offset32TableSize + offset));
@@ -170,6 +208,7 @@ void UnlinkedMetadataTable::finalize()
MetadataTableMalloc::free(m_rawBuffer);
m_rawBuffer = newBuffer;