← All issues

[2] YARR JIT ParenContext Use-After-Free via Incomplete Stale-Pointer Clearing

Severity: High | Component: JSC YARR JIT | 2d16551

Rated High because the observable effect is a use-after-free on free-list-recycled ParenContext objects in JIT-compiled regex code reachable from web content, and the bug title explicitly claims instruction pointer control — escalation to code execution within the renderer sandbox is projected with confidence 0.92 from the incomplete-clearing mechanism shown in the diff.

The patch replaces clearInnerParenContextHeadSlots(term->parentheses.disjunction) with clearParenContextHeadSlotsInRange(m_pattern.m_body, parenthesesFrameLocation + YarrStackSpaceForBackTrackInfoParentheses, m_parenContextSizes.frameSlots()) at two call sites in YarrJIT.cpp — the FixedCount backtrack path and the Greedy/NonGreedy backtrack path. The new function walks the entire pattern tree (m_pattern.m_body) instead of only the current group's inner disjunction, and applies a frame-slot range filter (headSlot >= minFrameLocation && headSlot < maxFrameLocation) to null out every parenContextHead pointer whose frame slot falls within the restored range. The old function only cleared inner groups of the current term, missing sibling and ancestor-sibling groups.

Source/JavaScriptCore/yarr/YarrJIT.cpp

- clearInnerParenContextHeadSlots(term->parentheses.disjunction);
+ clearParenContextHeadSlotsInRange(m_pattern.m_body, parenthesesFrameLocation + YarrStackSpaceForBackTrackInfoParentheses, m_parenContextSizes.frameSlots());

Source/JavaScriptCore/yarr/YarrJIT.cpp

- void clearInnerParenContextHeadSlots(PatternDisjunction* disjunction)
+ void clearParenContextHeadSlotsInRange(PatternDisjunction* disjunction, unsigned minFrameLocation, unsigned maxFrameLocation)
{
for (auto& alternative : disjunction->m_alternatives) {
for (auto& term : alternative->m_terms) {
- if (term.type == PatternTerm::Type::ParenthesesSubpattern || term.type == PatternTerm::Type::ParentheticalAssertion) {
- if (term.type == PatternTerm::Type::ParenthesesSubpattern
- && term.quantityType != QuantifierType::FixedCount
- && term.quantityMaxCount != 1
- && !term.parentheses.isTerminal
- && !term.parentheses.isCopy)
- storeToFrame(MacroAssembler::TrustedImmPtr(nullptr), term.frameLocation + BackTrackInfoParentheses::parenContextHeadIndex());
-
- clearInnerParenContextHeadSlots(term.parentheses.disjunction);
+ if (term.type != PatternTerm::Type::ParenthesesSubpattern && term.type != PatternTerm::Type::ParentheticalAssertion)
+ continue;
+ if (term.type == PatternTerm::Type::ParenthesesSubpattern
+ && term.quantityType != QuantifierType::FixedCount
+ && term.quantityMaxCount != 1
+ && !term.parentheses.isTerminal
+ && !term.parentheses.isCopy) {
+ unsigned headSlot = term.frameLocation + BackTrackInfoParentheses::parenContextHeadIndex();
+ if (headSlot >= minFrameLocation && headSlot < maxFrameLocation)
+ storeToFrame(MacroAssembler::TrustedImmPtr(nullptr), headSlot);
}
+ clearParenContextHeadSlotsInRange(term.parentheses.disjunction, minFrameLocation, maxFrameLocation);
}
}
}

JSTests/stress/yarr-jit-paren-context-head-uaf.js

+ var re = new RegExp('((c|b)*?(y|x)+?.){3}mp');
+ // +? expands to {1,1} + *?(isCopy=true). The *? copy uses ParenContext
+ // and is a sibling of (c|b)*?. Both bugs trigger: isCopy skip + sibling
+ // scope mismatch.

Incomplete invalidation of stale pointers after a global state restore — clearing scope narrower than restore scope leaves dangling references to freed free-list objects.

YARR is WebKit's regex engine; when a pattern is hot, it JIT-compiles to native code. ParenContext is a per-group backtracking state object managed via a singly-linked free list — freed contexts are recycled for new allocations within the same match. restoreParenContext is a JIT-emitted operation that bulk-restores a range of frame slots (the JIT's stack-like storage for backtracking state) from a saved ParenContext, reinstating pointer values that were valid at save time. parenContextHead is the frame slot storing the head of a group's ParenContext chain — it points to the most recently allocated ParenContext for that group.

Non-greedy quantifiers (*?, +?) use ParenContext for backtracking state. Critically, +? internally expands to {n,n} (a FixedCount group) plus *?(isCopy=true), creating sibling groups in the pattern tree that share the parent's frame slot range. clearInnerParenContextHeadSlots (the incomplete previous fix) walked only the current term's inner disjunction subtree to null restored pointers — it was introduced by 310115@main to address the same class of bug but scoped too narrowly.

The previous fix introduced clearInnerParenContextHeadSlots to null stale parenContextHead pointers after restoreParenContext. The critical mismatch: restoreParenContext restores ALL frame slots in a global range ([parenthesesFrameLocation+4, m_parenContextSizes.frameSlots())), which includes frame slots for sibling and ancestor-sibling groups — not just the current group's inner children. Because the clearing function's tree walk was rooted at the current term's inner disjunction rather than the full pattern body, sibling and ancestor-sibling groups with parenContextHead pointers in the restored range were never nulled.

  Pattern tree:  ((c|b)*?(y|x)+?.){3}mp
                       │
                  outer group {3}
                   ├── (c|b)*?        ─ parenContextHead at frameSlot A
                   ├── (y|x)+?        ─ expands to:
                   │    ├── {1,1}       (FixedCount)
                   │    └── *?(isCopy)  ─ parenContextHead at frameSlot B
                   └── .

  restoreParenContext range: [frameSlot_base+4 .. m_parenContextSizes.frameSlots())
                             covers slots A and B (global range)

  Old clearing (scoped to current term's inner disjunction):
    backtrack (c|b)*? → clear inner of (c|b)*? → misses slot B (sibling)
    backtrack {1,1}   → clear inner of {1,1}   → misses slot A (sibling)

  New clearing (scoped to m_pattern.m_body with range filter):
    walks entire tree, nulls every parenContextHead in [min, max)

After restoration, stale parenContextHead pointers reference ParenContext objects that were freed and recycled via the free list during a later iteration. When the JIT-compiled code dereferences these stale pointers — for instance, to traverse the ParenContext chain during subsequent backtracking — it accesses memory that now belongs to a different, recycled ParenContext.

This was discovered through variant analysis of the previous incomplete fix. The researcher identified that restoreParenContext operates on a global frame range while the clearing was scoped to a local subtree, then crafted the PoC pattern ((c|b)*?(y|x)+?.){3}mp to specifically target the isCopy expansion of +? that creates sibling groups triggering the incomplete clearing.

🔒

The ownership and lifetime implications of this free-list recycling bug are analyzed in depth, including escalation potential beyond the immediate crash

Subscribe to read more

🔒

Multiple reusable audit patterns identified, with concrete starting points for variant discovery across YARR JIT backtracking paths

Subscribe to read more