← All reports

Remove relation caching from `MatchResultCache::copy()`

The style cache that outlived the elements it was still pointing at.

Component: WebCore Style Resolution | f457aa2

WebCore's MatchResultCache exists to avoid redundant selector re-matching during CSS style resolution, particularly for :has(), which requires speculative matching against descendants and siblings. When :has() matching runs it records Style::Relation entries — raw Element* pointers plus positional information such as FirstChild/LastChild — which are later applied to those elements by commitRelations() so that future DOM mutations invalidate style correctly. A cache entry, however, has no relationship to the lifetime of the elements its relations point at.

This commit removes relation caching from MatchResultCache::copy() entirely. Previously copy() deep-copied the vector of Style::Relation entries into a long-lived cache entry; if the DOM was mutated between caching and the later commitRelations() call, the raw Element* became stale, since element removal does not purge the cache. The fix drops relation caching from the FullWithMatchResultCache path rather than adding invalidation, which is viable because that path does not call resetStyleRelations() — the FirstChild/LastChild flags already persist across resolutions via copyRelations().

  Before:
  resolve(el)  ──► :has() match ──► Relation{ Element* e, FirstChild }
                                       └─► MatchResultCache::copy()  (long-lived)
  remove(e)    ──► element freed; cache entry untouched
  resolve(el)  ──► cache hit ──► commitRelations() ──► e->setFlag()   ← UAF

  After:
  MatchResultCache::copy()  ──► match result only, no relations
                                 (FirstChild/LastChild persist via copyRelations())

This closes a heap use-after-free in style resolution that is reachable from web content through :has() with a featureless positional or sibling argument. Crafted CSS or script could drive the mismatch between a cache entry's lifetime and the lifetime of the elements its cached relations point at, corrupting memory during style resolution.

The portable pattern here is a cache that deep-copies records containing raw pointers into a structure whose eviction policy is keyed on something other than those pointers' lifetimes. Narrow: audit the remaining producers and consumers of Style::Relation — anything that stores relations rather than committing them within the same resolution — and check whether the storing path has a purge hook on element removal. Wider: other style- and layout-side caches that survive a resolution cycle and hold Element*, RenderObject* or Node* members are the same shape; the question to carry is which mutation paths purge the cache and which merely mark it dirty, since :has() invalidation deliberately touches elements far from the mutation. Widest: any memoization layer whose keys and values have different lifetime owners inherits this hazard — the invariant to look for is an explicit statement of which side outlives the other, and its absence is the finding. Code-review tell: a copy() or clone method that carries a vector of structs containing bare pointers into a container described as a cache.