# VM stack and register desync

Codegen or bytecode handlers that push/pop/cache stack state inconsistently across branches, leaving the VM stack unbalanced

_Area: Memory corruption and native code. Seen in 4 disclosed reports; one confirmed bounty: $10,000; latest 2026-06-19. Updated 2026-11-02._

## How it works
A VM or interpreter keeps a stack pointer (or register index) that every opcode assumes is in a known state. Codegen and handlers must leave that state balanced: push exactly what the parent expects, pop exactly what was pushed, and restore any cursor after use. The assumption that breaks is "the stack pointer is where the next opcode thinks it is." When one branch pushes a value nobody asked for, skips a push that was expected, or moves a pointer without restoring it, later opcodes silently read or write a neighboring slot. That turns a bookkeeping slip into type confusion, out-of-bounds access, or a write to an attacker-influenced address.

## Where it shows up
- Codegen `switch`/`case` arms that `return` early instead of `break`, skipping a trailing `push(val)` [E2](https://bugbountylibrary.com/r/h1%3A226200.md).
- `push()` called unconditionally where a `val` flag says whether a value was requested [E4](https://bugbountylibrary.com/r/h1%3A187536.md).
- Stack-pointer arithmetic `sp -= n` with only a lower-bound check and no upper-bound check on a user-controlled `n` [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md).
- Recursive C iterators that advance a cursor field without a bounds check or without restoring it, then reuse that field as a length [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md).
- Sibling functions that differ in whether they restore state (compare `each_leaf` vs `each_child`) [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md).
- Splat/array-concat opcodes (`OP_ARYCAT`, `ary_concat`) consuming a register the compiler did not intend [E2](https://bugbountylibrary.com/r/h1%3A226200.md)[E4](https://bugbountylibrary.com/r/h1%3A187536.md).

## How to review
For every codegen case and every handler, ask: does this path leave the value stack exactly as the caller expects? Check that each branch either pushes the value the parent requested or pushes nothing, and that early exits do not skip required pushes [E2](https://bugbountylibrary.com/r/h1%3A226200.md)[E4](https://bugbountylibrary.com/r/h1%3A187536.md). For pointer arithmetic, require both bounds: a negative or oversized count must be rejected, not just clamped below [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md). For recursive iterators, require that any cursor advanced is bounds-checked and restored before it is used as a length, and that a field is not both cursor and length source [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md). The invariant: after any opcode or codegen arm, the stack pointer and register index match what the next consumer assumes.

## How finders spotted it
- Read `gen_assignment` in codegen.c, noticed the default case `return;` skips the final `push(val)`, then tested an `OP_SCALL` LHS with a splat to see which register `OP_ARYCAT` actually used [E2](https://bugbountylibrary.com/r/h1%3A226200.md).
- Fuzzed unusual syntax; a `-0` literal inside `case/when` with splat produced a warning and crash, then used lldb to trace the null deref to `ary_concat` and found the `push()` imbalance [E4](https://bugbountylibrary.com/r/h1%3A187536.md).
- Code review of `ext/oj/fast.c`: noticed `doc_each_child` increments `doc->where` without a bounds check and lacks the `doc->where--` restore present in `each_leaf`, then traced the next-entry `memcpy` using `wlen = doc->where - doc->where_path` into a fixed stack buffer [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md).
- Assessed as reverse engineering and manual audit of a self-contained CharString interpreter, where pointer arithmetic is easy to spot by hand but hard to fuzz [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md).

## Variants seen
- Missing push on an early-return codegen path, leaving a stale register [E2](https://bugbountylibrary.com/r/h1%3A226200.md).
- Extra push when no value was requested, desyncing the stack [E4](https://bugbountylibrary.com/r/h1%3A187536.md).
- Negative user-controlled count moving the stack pointer past the top into the native frame [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md).
- Cursor advanced without bounds check or restore, reused as a length for a copy [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md).

## Impact and severity
E1 gave arbitrary read/write and RCE, chained with a kernel info leak and privilege escalation into a full iOS chain [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md). E2's stale register yielded an arbitrary memory write, chained with string corruption to code execution [E2](https://bugbountylibrary.com/r/h1%3A226200.md). E3 overflowed a stack buffer and aborted the process [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md). E4 caused a null-pointer dereference crash [E4](https://bugbountylibrary.com/r/h1%3A187536.md). Impact rises when the desync produces a controllable write rather than a crash, and when it is chained with an info leak or second primitive [E1](https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md)[E2](https://bugbountylibrary.com/r/h1%3A226200.md).

## Seen in real software
Open-source packages and projects, and products, where this pattern was found. If the target runs one, compare the version it runs with the fix.
- **oj (rubygems)** < 3.17.3, fixed in 3.17.3: Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input (CVE-2026-54592). Report: https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md
- **Apple Safari** (libType1Scaler.dylib DoType1InterpretCharString) iOS 14.1 and previous, macOS 10.15.6 and previous, fixed in iOS 14.2 and macOS 10.15.7: CVE-2020-27930: Safari RCE in Type 1 fonts handled by libType1Scaler.dylib (CVE-2020-27930). Report: https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md

## Evidence
- E1: https://bugbountylibrary.com/r/p0%3ACVE-2020-27930.md
- E2: https://bugbountylibrary.com/r/h1%3A226200.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-3m6q-jj5j-38c9.md
- E4: https://bugbountylibrary.com/r/h1%3A187536.md
