← All issues

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

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.

🔒

Detailed analysis of how a 4 GiB JIT-buffer size limit was being enforced, and what could happen when that enforcement folded under arithmetic wrap.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for integer-overflow-in-code-emission and parser-driven frame-layout accumulators, with concrete starting points across JSC's JIT pipelines.

Subscribe to read more