← All issues

[5] [JSC] Move DataView null vector check in IC outside of register save/restore

Severity: High | Component: JSC InlineCacheCompiler | 34669c8

Rated High because the diff fixes an IC handler that pops registers without a matching push, jumping to the polymorphic continuation with a corrupted register file and stack — downstream JIT code then executes against attacker-influenceable register state.

In emitIntrinsicGetter, a pre-push null-vector check shared a JumpList with a post-push out-of-bounds check; the shared link site pop'd registers for both, so the pre-push failure path executed restoreReusedRegistersByPopping without a matching push.

Source/JavaScriptCore/bytecode/InlineCacheCompiler.cpp

if (isResizableOrGrowableSharedTypedArrayIncludingDataView(accessCase.structure()->classInfoForCells())) {
+ m_failAndIgnore.append(failAndIgnore);
auto allocator = makeDefaultScratchAllocator(m_scratchGPR);
...
+ CCallHelpers::JumpList postPushFailAndIgnore;
if (isDataView) {
auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(...);
- failAndIgnore.append(outOfBounds);
+ postPushFailAndIgnore.append(outOfBounds);
doneCases.link(&jit);
}
- if (allocator.didReuseRegisters() && !failAndIgnore.empty()) {
- failAndIgnore.link(&jit);
+ if (allocator.didReuseRegisters() && !postPushFailAndIgnore.empty()) {
+ postPushFailAndIgnore.link(&jit);
allocator.restoreReusedRegistersByPopping(jit, preservedState);
m_failAndIgnore.append(jit.jump());
} else
- m_failAndIgnore.append(failAndIgnore);
+ m_failAndIgnore.append(postPushFailAndIgnore);

The pre-push failAndIgnore jumps are appended directly to m_failAndIgnore (no pop). A new postPushFailAndIgnore list collects post-push failures that correctly route through restoreReusedRegistersByPopping.

Misbalanced register-save/restore on a JIT IC failure path: a pre-push failure jump was routed through a post-push pop, desynchronizing the stack.

JSC caches property operations (like get_by_id for byteLength) as small machine-code stubs; on check failure they fall through to m_failAndIgnore. IntrinsicGetterAccessCase specializes built-in getters such as DataView.prototype.byteLength. ScratchRegisterAllocator::preserveReusedRegistersByPushing / restoreReusedRegistersByPopping push reused registers onto the native stack when the stub needs more scratches than free; every exit path between them must either both push and pop, or neither. loadDataViewByteLength emits inline code to read the underlying buffer length and emits an outOfBounds jump for detached/exceeded views.

The pre-push null-vector check produced a jump that ended up linked through code that popped reused registers without a matching push, leaving SP offset by the push width and reused callee/scratch registers holding the wrong values.

🔒

The escalation potential of a JIT stack desynchronization bug, and the conditions under which a misbalanced pop becomes more than a crash.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for JIT register-save discipline, with specific JSC compilation paths as starting points.

Subscribe to read more