CVE-2026-64784
Overview
Background
- UnlinkedMetadataTable
- JavaScriptCore structure (
Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp) holding per-opcode metadata for a code block.finalize()lays out each opcode class’s metadata into one contiguous buffer, computing per-entry offsets and the total size, then allocating that buffer. - Offset table layout
- Offsets start at
s_offset16TableSize; for each opcode class the code aligns the runningoffsetand advances it bynumberOfEntries * metadataSize(opcode). The final size also addss_offset32TableSize,valueProfileSize(m_numValueProfiles * sizeof(ValueProfile)), andsizeof(LinkingData). - `CheckedUint32`
- WTF checked-arithmetic type (
<wtf/CheckedArithmetic.h>) that tracks overflow viahasOverflowed()andoverflowed(). - `UnlinkedCodeBlockGenerator::finalize` / `BytecodeGenerator::generate`
- Callers that drive metadata finalization during bytecode generation;
generatecan surface aParserError.
Root Cause Analysis
UnlinkedMetadataTable::finalize() accumulated the metadata buffer layout in a plain unsigned offset, computing offset = roundUpToMultipleOf(alignment, offset) and offset += numberOfEntries * metadataSize(...) across all opcode classes, then valueProfileSize = m_numValueProfiles * sizeof(ValueProfile) separately, and finally allocated the buffer with MetadataTableMalloc::malloc(valueProfileSize + sizeof(LinkingData) + s_offset32TableSize + offset). All of this was 32-bit unsigned arithmetic with no overflow detection. For a sufficiently large function (enormous metadata counts), the running offset, the alignment round-up, the numberOfEntries * metadataSize product, or the final sum could wrap around UINT32_MAX. A wrapped total produced a buffer far smaller than the metadata actually requires, while the stored per-opcode offsets still indexed at their true (large) positions — a classic size-computation integer overflow leading to a heap buffer that is then written and read out of bounds.
The patch converts the layout math to CheckedUint32. The per-opcode loop runs only while ... && !checkedOffset.hasOverflowed(), the alignment round-up is validated (if (alignedOffset < checkedOffset.value()) { checkedOffset.overflowed(); break; }), and the advance uses checkedOffset += CheckedUint32(numberOfEntries) * metadataSize(...). valueProfileSize becomes a CheckedUint32, and a 32-bit-only paddingFor32Bit = sizeof(LinkingData) is folded in. If (checkedOffset + s_offset32TableSize + checkedValueProfileSize + paddingFor32Bit).hasOverflowed(), finalize() frees the buffer, resets state, and returns false. The signature changes to [[nodiscard]] bool finalize(); UnlinkedCodeBlockGenerator::finalize propagates the result, and BytecodeGenerator::generate turns a false into ParserError(ParserError::OutOfMemory) (surfaced to script as a RangeError). The regression test builds a Function with ~44.7M statements to drive the metadata size past 32 bits and expects a RangeError.
UnlinkedMetadataTable::finalize() computed the metadata buffer’s size and per-opcode offsets with unchecked 32-bit arithmetic, so a huge function could wrap the total, allocate an undersized buffer, and leave the real offsets pointing out of bounds — fixed by doing the layout in CheckedUint32 and failing finalization on overflow.Attack Path
- Delivery Attacker runs script on a victim page (web-reachable) that can construct a very large function body.
- Inflate metadata
Generate a function with an enormous number of metadata-bearing opcodes (e.g.
new Functionwith tens of millions of calls), driving the per-opcode offset accumulation toward and pastUINT32_MAX. - Overflow the size
During
finalize(), the runningoffset/final sum wraps, so the allocation size is computed far smaller than required. - Undersized allocation
MetadataTableMalloc::mallocreturns a buffer too small for the metadata whose stored offsets still address the true large positions. - Primitive Subsequent metadata writes/reads use the true (unwrapped) offsets against the undersized buffer, giving a heap out-of-bounds access (the advisory’s “out-of-bounds access”); controlled corruption beyond the crash is inferred, not shown by the diff.
Impact Assessment
addressBits <= 32/memory-limited configs), so it is resource-intensive but web-triggerable.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp |
modified | |
forSource/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp |
modified | |
fJSTests/stress/unlinked-metadata-table-finalize-overflow.js |
modified |
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
- Other metadata/offset layoutsReview sibling size computations in bytecode/metadata (value-profile sizing,
MetadataTable::link, instruction-stream sizing) for unchecked 32-bit accumulation. - Allocation-size mathAudit other
*Malloc::malloc(a + b + c)sites where the operands derive from script-controlled counts withoutCheckedUint32. - `[[nodiscard]]` adoptionLook for other finalize/allocate helpers whose failure was previously discarded, where a wrapped size silently produced an undersized buffer.
Patch
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp
index e4f5af853969..22f6b8bbaaac 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
@@ -51,14 +51,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_jumpTargets = WTF::move(m_jumpTargets);
m_codeBlock->m_identifiers = WTF::move(m_identifiers);
@@ -95,6 +96,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 174c2ca40646..5f624ea106dd 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
@@ -193,7 +193,7 @@ public:
void applyModification(BytecodeRewriter&, JSInstructionStreamWriter&);
- void finalize(std::unique_ptr<JSInstructionStream>);
+ [[nodiscard]] bool finalize(std::unique_ptr<JSInstructionStream>);
void 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;
}
+ return true;
}
UnlinkedMetadataTable::~UnlinkedMetadataTable()
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h b/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h
index b5ea1b7b9583..d2ae301643a7 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h
+++ b/Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018-2023 Apple Inc. All rights reserved.
+ * Copyright (C) 2018-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
@@ -81,7 +81,7 @@ public:
size_t sizeInBytesForGC();
- void finalize();
+ [[nodiscard]] bool finalize();
RefPtr<MetadataTable> link();
diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
index 4848f4df2c32..ecaebac0fd5a 100644
--- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
+++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008-2023 Apple Inc. All rights reserved.
+ * Copyright (C) 2008-2023, 2026 Apple Inc. All rights reserved.
* Copyright (C) 2008 Cameron Zwarich <cwzwarich@uwaterloo.ca>
* Copyright (C) 2012 Igalia, S.L.
*
@@ -360,7 +360,8 @@ ParserError BytecodeGenerator::generate(unsigned& size)
RELEASE_ASSERT(m_codeBlock->numCalleeLocals() < static_cast<unsigned>(FirstConstantRegisterIndex));
size = instructions().size();
- m_codeBlock->finalize(m_writer.finalize());
+ if (!m_codeBlock->finalize(m_writer.finalize())) [[unlikely]]
+ return ParserError(ParserError::OutOfMemory);
// We limit total bytecode sequence size to int32_t so that we can use int32_t jump offsets.
// Also, this allows us to use one bit of bytecode for some flag, including "ignore-result-flag".
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");
Regression Test / PoC
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");