Skip to content

ZJIT: NoEPEscape patchpoint after GC safepoint exit to interpreter without writing locals #988

@XrXr

Description

@XrXr
# This ZJIT bug repro requires winning a race condition.
# Run in a shell loop as following:
#
#
# $ while ./miniruby --zjit-call-threshold=1 ../test.rb; do ; done
# Seems to be working fine, exiting
# Seems to be working fine, exiting
# Seems to be working fine, exiting
# Seems to be working fine, exiting
# ../test.rb:25:in 'Object#test': undefined method 'empty?' for nil (NoMethodError)
#         from ../test.rb:39:in '<main>'
#
# As of https://github.com/ruby/ruby/commit/f6886fc1ccde667845bd691cad18f8f32f00d312
# HIR for the `test` is:
#
# bb4(v41:BasicObject, v42:Truthy, v43:NilClass|ArrayExact):
#   v47:ArrayExact = NewArray
#   v51:ArrayExact = NewArray
#   v55:ArrayExact = NewArray
#   v59:ArrayExact = NewArray
#   v63:ArrayExact = NewArray
#   v67:ArrayExact = NewArray
#   v71:ArrayExact = NewArray
#   v75:ArrayExact = NewArray
#   v79:ArrayExact = NewArray
#   v83:ArrayExact = NewArray
#   v87:ArrayExact = NewArray
#   v91:ArrayExact = NewArray
#   v95:ArrayExact = NewArray
#   PatchPoint NoSingletonClass(Array@0x105d52598)
#   PatchPoint MethodRedefined(Array@0x105d52598, class@0x8bb1, cme:0x105ef61c0)
#>> PatchPoint NoEPEscape(test)
#   PatchPoint MethodRedefined(Array@0x105d52598, empty?@0x9b, cme:0x105dbbfd0)
#   Jump bb5(v41, v42, v95)
#
# Since the NoEpEscape patchpoint uses without_locals(), the new array is not written
# to memory before exiting to the interpreter. What happens to be in memory for `b`
# is nil and the interpreter raises NoMethodError.
#
# To make this happen, we race in a different ractor to get the follow sequence of
# events:
#  1. NewArray enters GC in the main thread
#  2. main thread GC calls rb_gc_vm_barrier() and sleeps for rendezvous synchronization
#  3. sub thread calls `binding`, patching NoEPEscape(test)
#  4. sub thread joins the GC barrier and sleeps
#  5. main thread wakes up and exits through NoEPEscape(test), not writing `b` to memory
def test(a)
  while a
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b = []
    b.class
    b.class
    b.empty?
  end

  binding
end

$VERBOSE = nil # Silence ractor creation warning
Ractor.new do
  GC.stress = true
  deadline = Time.now + 1
  test(false) until Time.now > deadline
  puts("Seems to be working fine, exiting")
  Process.exit!(true)
end
test(true)

A similar situation to ruby#16558. This proves that NoEPEscape sometimes have to write locals and sometimes should not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions