← All issues

[5] [JSC] Defer GC across direct eval cache key construction

Severity: High | Component: JSC interpreter | 32f1bfb

Rated High because the diff inserts a DeferGC across an interval where DirectEvalCodeCache::CacheLookupKey holds a raw StringImpl* extracted from a rope fiber; without it, GC during parser/profiler allocations can sweep the unreferenced fiber, leaving the cache key (and subsequent insertion) dereferencing freed memory.

JSC::eval() wraps the cache-miss compilation path in DeferGC. The key continues to be constructed as programStr.data.impl(); the deferral keeps the rope fiber alive across all GC-safepoint operations between key construction and cache insertion.

Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp

+ DeferGC deferGC(vm);
CacheLookupKey cacheKey { programStr.data.impl(), callerBytecodeIndex };
...
// profiler hook, LiteralParser, collectClosureVariablesUnderTDZ, parser, insert

Raw StringImpl* lifetime escape: the eval cache key references a rope fiber whose only root is the on-stack rope, across GC-safepoint parser allocations.

DeferGC brackets the cache-miss branch from key construction through cache insertion. No reference is taken on the StringImpl directly; the deferral suppresses sweeping while the unreferenced fiber remains in play.

DirectEvalCodeCache memoizes compiled DirectEvalExecutables per (StringImpl*, BytecodeIndex) to make repeated eval(sameString) at the same call site cheap. JSString::value() returns a StringViewWithUnderlyingString; for ropes, this resolves the rope and the resulting view aliases the contents of one of the rope's fiber JSStrings rather than holding an independent RefPtr<StringImpl>.

Pre-fix, cacheKey.impl was the raw StringImpl* taken from programStr.data.impl(). After resolution, the rope's internal layout can mutate (rope flattening, substring sharing), and the fiber whose StringImpl lives in the key may become unreachable from the on-stack rope. The cache-miss branch then performs source-profiler hook, LiteralParser allocations for the sloppy-JSON eval fast path, collectClosureVariablesUnderTDZ, full parser allocations, and finally cache insertion — every step a potential GC safepoint.

A GC at any safepoint can sweep the now-unreachable fiber, decrement its StringImpl refcount to zero, and free the underlying buffer. cacheKey.impl is now dangling. Cache insertion subsequently hashes and stores it — the dangling pointer is read for hashing/equality against existing keys and then persisted as the new entry's key, so future lookups dereference freed memory.

This weakens the lifetime invariant of cache keys derived from JS-visible string content. The exploit primitive is heap UAF on WTF::StringImpl, with both an immediate read (hashing) and a long-lived store of the dangling pointer in the cache.