Released August 17, 2026

CVE-2026-64784

CVSS 4.3 severity medium CWE-125CWE-787 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

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

Key insight. Size accounting for the metadata table used raw unsigned 32-bit arithmetic, so a function with tens of millions of metadata entries overflowed the total-size computation and under-allocated. The fix reworks the math with CheckedUint32 and rejects the compile (RangeError) on overflow rather than proceeding with a corrupt table.

Patch Details

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;

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-64784
Bugzilla 317632
Component JSC
Bug Class IntegerOverflow
Severity medium
Commit 97df94ead028cddc…
Advisory Apple Advisory

Terminology

  • 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

1. 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.

2. Trigger bytecode finalization

Compiling the function calls UnlinkedMetadataTable::finalize(), which sums per-opcode metadata offsets and sizes as 32-bit unsigned.

3. Overflow the size computation

The accumulated offset/size wraps past 2^32, so the metadata buffer is malloc’d far too small.

4. 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

A heap out-of-bounds write in JavaScriptCore driven by a very large but legal-looking function. OOB writes in JSC’s own allocations are a classic exploitation primitive: the attacker controls the overflow magnitude and the adjacent heap layout, making this a strong step toward arbitrary write and code execution in the WebContent process.

Audit Directions

Unsigned size arithmetic in bytecode/JIT

Audit 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 derivation

Wherever an allocation size and the indices into it are computed separately, confirm both use overflow-checked math so they cannot diverge.

Changed Functions

Function File Change Note
UnlinkedMetadataTable::finalize Source/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::finalize Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp modified Now returns bool and propagates the metadata finalize() success/failure to the caller.
BytecodeGenerator::generate Source/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

Source Files

  • Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp
  • Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h
  • Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.cpp
  • Source/JavaScriptCore/bytecode/UnlinkedMetadataTable.h
  • Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp

Test Files

  • JSTests/stress/unlinked-metadata-table-finalize-overflow.js

Regression Test

The fix ships this proof-of-concept / regression test:

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");