Skip to content

ZJIT: Linear scan's parcopy miscompiles cycles among spilled vregs #1032

Description

@k0kubun

On resolve_ssa, when there's a cycle among spilled vregs, parcopy would break it using a scratch reg like this:

Mov  SCRATCH_REG, [Stack[0]]   ; cycle parked here
Mov  [Stack[0]], [Stack[1]]    ; scratch_split: Mov SCRATCH_REG, [Stack[1]]
                               ;                Mov [Stack[0]], SCRATCH_REG
Mov  [Stack[1]], SCRATCH_REG   ; reads the clobbered value

as shown above, moving one stack slot to another also needs a scratch reg, and on x86_64 it currently uses the same SCRATCH_REG, clobbering the parked value.

When we implemented the previous register allocator with register spill support ruby#14936, this was not a problem because:

  1. We designed it so that only a single pass (scratch_split) is allowed to use scratch registers. No other passes like resolve_ssa could clobber them by sharing the same scratch register.
  2. We used to use separate pools of stack slots for spilled block params and spilled vregs. So we never created a cycle by passing spilled vregs to spilled block params.

The current register allocator ruby#16295 got rid of (1), but I think we should have never left the design of resolving ParallelMov in scratch_split. It makes such a miscomp easy to rule out, it should reduce the number of scratch registers required for arm64, and it's easier to break such cycles in scratch_split which has two scratch regs for x86_64.

Fixing that for x86_64 with the current design would mean that you either use an extra stack slot, which results in slower code, or use two scratch regs in resolve_ssa, which makes it even more fragile and less maintainable.

Repro

def test(a, b, n)
  i = 0
  while i < n
    a, b = b, a
    i += 1
  end
  [a, b]
end
test(10, 20, 4)
test(10, 20, 4)
p test(10, 20, 5)
$ ./miniruby -v ~/tmp/a.rb
ruby 4.1.0dev (2026-08-21T15:07:09Z master d32793fe8e) +PRISM [x86_64-linux]
[20, 10]

$ ./miniruby -v --zjit-call-threshold=2 ~/tmp/a.rb
ruby 4.1.0dev (2026-08-21T15:07:09Z master d32793fe8e) +ZJIT dev_nodebug +PRISM [x86_64-linux]
[20, 20]

This was introduced as a latent bug in ruby#16295. ruby#17853 was the first revision that surfaced the problem for this repro.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions