← 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.

🔒

The ownership story behind this cache key — and why a string that looks rooted on the stack can be freed mid-eval — is dissected in depth, along with the escalation potential beyond a crash.

Subscribe to read more

🔒

Four reusable audit patterns identified covering raw-pointer cache keys, `JSString::value()` lifetime assumptions, rope mutation as a GC-reachability change, and `DeferGC` as a sentinel for unsafe pointers — each with concrete grep starting points.

Subscribe to read more