← All reports

[JSC] ScopedArgumentsTable ScopeOffset buffer allocates from fastMalloc

JSC runtimeMemoryCorruption

Component: JSC runtime | 72272dc

Source/JavaScriptCore/runtime/ScopedArgumentsTable.h

- typedef CagedUniquePtr<Gigacage::Primitive, ScopeOffset> ArgumentsPtr;
+ using ArgumentScopeBufferType = Vector<ScopeOffset>;
...
- ArgumentsPtr m_arguments;
+ ArgumentScopeBufferType m_arguments;
Vector<WatchpointSet*> m_watchpointSets;

Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp

- ArgumentsPtr newArguments = ArgumentsPtr::tryCreate(newLength, newLength);
- if (!newArguments) [[unlikely]]
+ size_t oldSize = m_watchpointSets.size();
+ if (!m_arguments.tryGrow(newLength))
+ return nullptr;
+ if (!m_watchpointSets.tryGrow(newLength))
return nullptr;

Gigacage::Primitive is a large isolated virtual memory region JSC uses to contain typed-array and ArrayBuffer backing stores, so that an out-of-bounds write in JS-controlled typed arrays cannot easily reach unrelated heap objects — the cage boundary is a key JIT exploit mitigation. ScopedArgumentsTable is internal VM bookkeeping, not JS-reachable data: it maps arguments-object slots to ScopeOffset values that are later used directly as indices into a JSLexicalEnvironment's variable storage, without additional bounds checking, because that storage is assumed trusted. This commit moves the table's buffer out of the Primitive cage onto plain fastMalloc-backed Vector<ScopeOffset> storage, and removes a stale m_watchpointSets.resize() call in the locked branch of trySetLength that mutated a supposedly-immutable locked table.

Before:                                      After:
Gigacage::Primitive region                   Regular fastMalloc heap
  |- Float64Array backing store                ScopedArgumentsTable::m_arguments
  |- ScopedArgumentsTable::m_arguments  <-- reachable via cage OOB write
  \- other typed array buffers

Cage OOB write -> corrupt ScopeOffset ->     Cage OOB write -> typed arrays only
  unchecked index into
  JSLexicalEnvironment::variables()

Placing VM-internal indices inside the Primitive cage effectively gave a typed-array OOB write a path to attacker-controlled lexical-environment indexing. The move restores the cage's trust model: what is inside it should be JS-controlled data, not the metadata that indexes trusted storage.

Narrow: review the surrounding allocation and resize paths for regressions from the migration — Vector::tryGrow() has different growth and reallocation semantics than the old CagedUniquePtr allocation, so check whether any code implicitly relied on the caged allocator's behaviour, and verify trySetLength's growth path zero-fills only the newly added watchpoint slots. The removed m_watchpointSets.resize(newLength) in the locked branch is worth independently confirming against SymbolTable::trySetArgumentsLength rather than taking the commit message's assertion that no caller observed the old resized state. Wider and more valuable: this pattern — VM-internal metadata mistakenly placed inside a cage meant for JS-controlled data — is worth grepping for across the remaining CagedUniquePtr usages in JSC, since sibling classes could carry the same exposure. Match tell: a CagedUniquePtr<Gigacage::Primitive, T> whose T is an index, offset, or pointer type consumed without bounds checking.