← All issues

[2] [JSC YARR] Fix integer overflow in AssemblerBuffer leading to heap overflow

Await a cross-realm promise in JSC and the next thenable's resolver landed in the wrong realm — the fast path skipped the spec's realm fix-up.

Severity: High | Component: JSC AssemblerBuffer / YARR | 7663d81

Rated High because the diff widens code-buffer length arithmetic that previously wrapped 32-bit under attacker-shaped regex pressure; the downstream putIntegralUnchecked performs an unchecked write past the real allocation, yielding a linear write-anything-the-JIT-emits primitive into adjacent heap.

Widens m_index, m_capacity, and the arithmetic in isAvailable(), putIntegral(), and grow() from unsigned to a width that does not wrap under YARR-driven code emission. Adds TooManyCaptures and FrameTooLarge YARR errors — the commit message labels these "not actually critical" (DoS and frame-bookkeeping overflows in pattern compilation).

Source/JavaScriptCore/assembler/AssemblerBuffer.h

- bool isAvailable(unsigned space)
- {
- return m_index + space <= m_capacity;
- }
+ bool isAvailable(size_t space)
+ {
+ return m_index + space <= m_capacity;
+ }

32-bit overflow in JIT code buffer length arithmetic: wrapped nextIndex bypasses the capacity check, then putIntegralUnchecked writes the emitted machine code past the real allocation.

Buffer width is widened; the isAvailable / putIntegral / grow arithmetic is recomputed in the wider type. YARR's pattern compiler gains TooManyCaptures and FrameTooLarge early bailouts so pathological patterns fail before reaching the emitter.

AssemblerBuffer is JSC's emission buffer used by every JIT backend — YARR, baseline, DFG, FTL, and Wasm BBQ/OMG — to accumulate machine code before linking. The contract is: isAvailable(n) is the precondition for putIntegralUnchecked<T>, which performs WTF::unalignedStore<T>(m_storageBuffer + m_index, value) and bumps m_index. grow() is called by putIntegral when isAvailable reports insufficient room. The buffer was sized in unsigned despite YARR being capable of driving emission size past 2^32 with adversarial patterns.

The three pre-fix arithmetic sites all evaluated in 32-bit:

isAvailable(space):   return m_index + space <= m_capacity;        // unsigned wrap
putIntegral(value):   nextIndex = m_index + sizeof(IntegralType);   // unsigned wrap, "nextIndex > capacity" guard bypassed
grow(extra):          newCapacity = m_capacity + m_capacity/2 + extra;  // unsigned wrap, undersize allocation

A YARR pattern driving emission toward 2^32 bytes wraps these expressions. After wrap, isAvailable returns true, putIntegral's nextIndex > capacity test is bypassed, and grow() resizes to a value smaller than the data actually written. putIntegralUnchecked then performs an unaligned store of the encoded machine code past the live allocation.

The write is linear and the payload is the JIT's emitted bytes — the pattern compiler controls immediates, displacements, and opcodes, all of which become attacker-influenced bits in the overflow. Heap layout adjacency dictates which structure is hit first; the JIT memory pool sits beside the rest of JSC's heap, so the write reaches metadata that affects the next code linking pass or the next JS-visible allocation.

The two YARR-side changes are non-critical bookkeeping fixes: TooManyCaptures triggers when the parsed capture count would overflow the per-term backtrack frame layout, and FrameTooLarge triggers when currentCallFrameSize would wrap during stack-slot accounting. Pre-fix these produced inconsistent backtrack frame layouts or compiler hang/crash.

This weakens the JIT memory invariant that the emitter never writes past the live buffer. The exploit primitive is heap write of attacker-controlled bytes into whatever follows the assembler data buffer in the JSC allocator.