CVE-2026-64784
Overview
Background
- Unlinked metadata table
- JSC stores per-opcode metadata for a code block in a flat buffer whose per-opcode offsets are accumulated during finalize().
- CheckedUint32
- A 32-bit unsigned type that traps arithmetic overflow instead of wrapping — the fix’s tool for catching the bad size math.
- Undersized allocation
- When the total-size computation wraps 32 bits, malloc returns a buffer smaller than the offsets that index into it, so populating metadata writes out of bounds.
Root Cause Analysis
This fixes a 32-bit unsigned integer overflow in JavaScriptCore’s unlinked metadata table finalization that led to an undersized allocation and out-of-bounds writes. UnlinkedMetadataTable::finalize() computed per-opcode metadata offsets by accumulating unsigned arithmetic: offset = roundUpToMultipleOf(alignment, offset); offset += numberOfEntries * metadataSize(…); and then sized the final buffer as valueProfileSize + sizeof(LinkingData) + s_offset32TableSize + offset. For a function with an enormous amount of metadata (the test builds a function body with ~44.7 million calls), these unsigned computations overflow 32 bits, so the malloc’d buffer is far smaller than the offsets that index into it, and populating the metadata writes out of bounds — heap corruption behind the ‘unexpected Safari crash’ (OOB) impact.
The fix converts the accounting to CheckedUint32: it detects overflow in the offset accumulation, the alignment rounding, and the valueProfileSize and final size (including a 32-bit-only sizeof(LinkingData) padding guard), and on overflow frees the buffer, resets metadata state, and returns false. finalize() now returns bool; UnlinkedCodeBlockGenerator::finalize() propagates it ([[nodiscard]]), and BytecodeGenerator::generate() returns ParserError::OutOfMemory (surfaced as a RangeError) instead of proceeding with a corrupt table.
The restored invariant is that the metadata table’s total addressable size fits in the unsigned layout, or compilation is rejected. Fully established by the diff; the JSC component and the test filename (unlinked-metadata-table-finalize-overflow.js) confirm it.
Attack Path
- Generate an enormous function The page builds a function whose body contains a huge number of operations (e.g. tens of millions of calls), producing a very large metadata table.
- Trigger bytecode finalization Compiling the function calls UnlinkedMetadataTable::finalize(), which sums per-opcode metadata offsets and sizes as 32-bit unsigned.
- Overflow the size computation The accumulated offset/size wraps past 2^32, so the metadata buffer is malloc’d far too small.
- Out-of-bounds write Writing the metadata entries indexes beyond the undersized buffer, corrupting the heap and crashing (now rejected with a RangeError/OutOfMemory).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
UnlinkedMetadataTable::finalizeSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp |
modified | Reworks offset/size accounting with CheckedUint32 (alignment rounding, per-opcode sizes, valueProfileSize, and final buffer size with a 32-bit LinkingData padding guard); on overflow frees the buffer, resets state, and returns false instead of allocating an undersized table. |
UnlinkedCodeBlockGenerator::finalizeSource/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp |
modified | Now returns bool and propagates the metadata finalize() success/failure to the caller. |
BytecodeGenerator::generateSource/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp |
modified | Checks finalize()'s result and returns ParserError::OutOfMemory (surfaced as RangeError) on overflow instead of continuing with a corrupt metadata table. |
UnlinkedMetadataTable::finalize / UnlinkedCodeBlockGenerator::finalize (declarations)Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h |
modified | Signatures change to [[nodiscard]] bool finalize() so callers must handle the overflow-failure result. |
Files Changed
JSTests/stress/unlinked-metadata-table-finalize-overflow.jsSource/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cppSource/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.hSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.cppSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.hSource/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
Audit Directions
- Unsigned size arithmetic in bytecode/JITAudit other buffer-size and offset accumulations in JSC’s bytecode and JIT layers for raw unsigned (non-Checked) arithmetic that a large program could overflow.
- Allocation-size vs. index derivationWherever an allocation size and the indices into it are computed separately, confirm both use overflow-checked math so they cannot diverge.
Patch
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;