← All issues

Cloned SymbolTable cache per JSGlobalObject fixes DFG constant-folding of mutable variables

5980a42

Source/JavaScriptCore/bytecode/CodeBlock.cpp

- SymbolTable* clone = symbolTable->cloneScopePart(vm);
+ // We have to make sure to use a single code block for constant watchpointing.
+ SymbolTable* clone = globalObject->symbolTableCache().get(symbolTable);
+ if (!clone) {
+ clone = symbolTable->cloneScopePart(vm);
+ globalObject->symbolTableCache().set(symbolTable, clone);
+ }
if (wasCompiledWithDebuggingOpcodes())
- clone->setRareDataCodeBlock(this);
+ clone->collectDebuggerInfo(this);

Source/JavaScriptCore/runtime/SymbolTable.cpp

-void SymbolTable::setRareDataCodeBlock(CodeBlock* codeBlock) {
+void SymbolTable::collectDebuggerInfo(CodeBlock* codeBlock) {
auto& rareData = ensureRareData();
- ASSERT(!rareData.m_codeBlock);
- rareData.m_codeBlock.set(codeBlock->vm(), this, codeBlock);
+ if (!rareData.m_inferredName.isNull())
+ return;
+ rareData.m_inferredName = String::fromUTF8(codeBlock->inferredName().span());
+ ScriptExecutable* executable = codeBlock->ownerExecutable();
+ if (!executable->isHostFunction()) {
+ rareData.m_debuggerSourceID = executable->sourceID();
+ rareData.m_debuggerLineColumn = { static_cast<unsigned>(executable->firstLine()), executable->startColumn() };
+ }
}

DFG JIT uses WatchpointSet objects to guard speculative optimizations: if a variable's WatchpointSet stays IsWatched (never fired), DFG is allowed to constant-fold its value. SymbolTable stores per-variable metadata for a lexical scope, including the WatchpointSetEntry that DFG consults. When a CodeBlock is jettisoned (invalidated due to a deoptimization), it may be fully recreated on the next execution. Each recreation previously called cloneScopePart(), which allocated a brand-new SymbolTable clone with a brand-new WatchpointSet.

The bug: op_put_to_scope bytecode correctly fires the new clone's WatchpointSet, but the live JSLexicalEnvironment still held the old clone. DFG, recompiling from the environment, read the old WatchpointSet, saw IsWatched, and emitted a constant load for a variable that had already been mutated. The fix introduces a WeakGCMap<SymbolTable*, SymbolTable> on JSGlobalObject that deduplicates clones — the same clone (and thus the same WatchpointSet) is reused for all CodeBlocks derived from the same original SymbolTable.

Before (bug):
  CodeBlock v1  →  clone_A (WatchpointSet_A: IsWatched)
  JSLexicalEnv  →  holds clone_A
  CodeBlock v1 jettisoned
  CodeBlock v2  →  clone_B (WatchpointSet_B: IsWatched)
  op_put_to_scope fires WatchpointSet_B  ← wrong clone
  DFG reads env → clone_A → WatchpointSet_A still IsWatched → WRONG fold

After (fix):
  CodeBlock v1  →  cache miss → clone_A, stored in symbolTableCache
  JSLexicalEnv  →  holds clone_A
  CodeBlock v1 jettisoned
  CodeBlock v2  →  cache hit → reuse clone_A
  op_put_to_scope fires WatchpointSet_A  ← correct clone
  DFG reads env → clone_A → WatchpointSet_A: Invalidated → correct deopt

The collectDebuggerInfo replacement for setRareDataCodeBlock copies debugger metadata (inferred name, source ID, line/column) from the first CodeBlock that visits the clone, then silently returns for all subsequent callers. This first-caller-wins semantics replaces the previous ASSERT(!m_codeBlock) pattern that could not survive clone reuse.

Incorrect constant-folding of mutable let variables in DFG-compiled closures is a textbook source of JIT type confusion bugs — the compiler operates on a stale assumption about a value it no longer controls. This fix closes a concrete watchpoint lifecycle hole where WatchpointSet identity diverged from live object state across CodeBlock jettison/recreation cycles.

🔒

Cache key identity and first-writer-wins debugger info on shared clones both have audit-worthy edge cases.

Subscribe to read more