ruby.wasm icon indicating copy to clipboard operation
ruby.wasm copied to clipboard

Bug with passing Procs around: "RuntimeError: unreachable"

Open richardboehme opened this issue 3 years ago • 5 comments

Hey! Sorry for not coming back to #32 yet! I was trying things out and stumbled around a really weird bug that I cannot explain.

I extracted the following snippet that reproduces the problem (at least on my machine):

import { DefaultRubyVM } from 'ruby-head-wasm-wasi/dist/browser.umd'

const main = async () => {
  // Fetch and instantiate WebAssembly binary
  const response = await fetch(
    "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@next/dist/ruby.debug+stdlib.wasm"
  );
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const { vm } = await DefaultRubyVM(module);

  window.wrapProc = (proc) => {
    return () => {
      proc.call('call')
    }
  }

  vm.eval(`
    require 'js'
    require 'json'
    
    class Test
      def initialize
      end
    end

    proc = Proc.new do 
      json = {a: 1}.to_json
      Test.new
    end
    wrapped_proc = JS::global.call('wrapProc', JS::Object.wrap(proc))
    wrapped_proc.call(:call)
  `)
};

main();

The error this produces is:

Uncaught (in promise) RuntimeError: unreachable
    at asyncify_stop_unwind (09f33406:0x674834)
    at rb_abi_guest_rb_funcallv_protect (09f33406:0x97c7)
    at __wasm_export_rb_abi_guest_rb_funcallv_protect (09f33406:0xa4dc)
    at RbAbiGuest.rbFuncallvProtect (browser.umd.js:669:33)
    at callRbMethod (browser.umd.js:1456:40)
    at RbValue.call (browser.umd.js:1343:30)
    at main.js?t=1660228644277:14:12
    at Object.reflectApply (browser.umd.js:1201:34)
    at imports.rb-js-abi-host.reflect-apply (browser.umd.js:906:23)
    at rb_js_abi_host_reflect_apply (09f33406:0x86bf)

I tried to wrap my head around this for some time now and can't find the source of the problem. The error disappears when removing for example the to_json line or the class initialization or when removing the empty initialize method of the Test class.

Let me know if more information is needed of if I can help on this somehow else!

richardboehme avatar Aug 11 '22 14:08 richardboehme

The last call frame, which hit unreachable instruction, shows that it's crashing at Asyncify.

Uncaught (in promise) RuntimeError: unreachable
    at asyncify_stop_unwind (09f33406:0x674834)

Unfortunately, Asyncify has a fixed-size buffer for unwind/rewind, and it can be exhausted when C call frame is deep, and it hits unreachable inst. In this case, the nested rb_funcallv_protect calls consume most of the buffer, I guess. For now, you need to build ruby from the source to extend the size of the buffer.

I have a plan to support segmented buffers in Asyncify to allocate buffers dynamically, but I haven't done it yet.

Let's keep this open until segmented buffer will be supported.

kateinoigakukun avatar Aug 12 '22 04:08 kateinoigakukun

Hey @kateinoigakukun, this sounds reasonable, thanks for the explanation! I tried manually changing the buffer sizes by setting WASM_SETJMP_STACK_BUFFER_SIZE to 7168, however this always leads to the following error:

Assertion failed: 0 && "unexpected state" (/src/build/src/head/wasm/setjmp.c)

I suspect it has something to do with the increased struct size that gets used there? Maybe you've already encountered the issue or I need to change something else to make it work? Do you have an idea?

richardboehme avatar Aug 12 '22 09:08 richardboehme

I suspect it has something to do with the increased struct size that gets used there? Maybe you've already encountered the issue or I need to change something else to make it work? Do you have an idea?

Yes, that's right. And the struct is usually allocated on stack space, so you need to extend the stack size also by passing -Xlinker -z -Xlinker stack-size=XXX. By default XXX is 16777216.

kateinoigakukun avatar Aug 12 '22 09:08 kateinoigakukun

Thank you. I tried increasing the stack size but it did not help. I'm still receiving the same error after increasing it by two or ten times. I changed it using the Rakefile that already includes the default 167772160.

Maybe it's not about the size the struct needs but some kind of struct memory/alignment issue? It's just a theory from somebody with some base level of C understanding but it seems like the state variable is not correct.

I enabled debug logging to see what we got and it showed me that the first call to _rb_wasm_setjmp_internal has a state of 1931506537. This seems like a random memory location interpreted as state.

richardboehme avatar Aug 12 '22 10:08 richardboehme

@richardboehme Hmm, weird. Maybe my witapi ext code doesn't care about reentrance... I'll check it.

kateinoigakukun avatar Aug 12 '22 14:08 kateinoigakukun

Hi. I tried looping the vm.wrap() function and got the same error "Runtime error: unavailable" in the latest version 0.6.0. In versions 0.5.0 and below, memory consumption increased rapidly until the application shut down with an error. I'm calling ruby functions from js and wrapping the function arguments in vm.wrap(). After executing ruby functions, there is no need to keep the arguments in memory. This would be a catastrophe if every call to the ruby function would infinitely increase the memory usage.

Dykson avatar Feb 11 '23 17:02 Dykson

The latest 2.2.0 includes several improvements for this issue.

kateinoigakukun avatar Nov 26 '23 05:11 kateinoigakukun