Skip to content

Commit c7c43a8

Browse files
Partially allow nested VM operations except for rewinding
1 parent 7defc63 commit c7c43a8

File tree

8 files changed

+110
-18
lines changed

8 files changed

+110
-18
lines changed

ext/witapi/bindgen/rb-abi-guest.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,8 @@ __attribute__((export_name("rb-vm-bugreport: func() -> ()")))
205205
void __wasm_export_rb_abi_guest_rb_vm_bugreport(void) {
206206
rb_abi_guest_rb_vm_bugreport();
207207
}
208+
__attribute__((export_name("rb-set-should-prohibit-rewind: func(new-value: bool) -> bool")))
209+
int32_t __wasm_export_rb_abi_guest_rb_set_should_prohibit_rewind(int32_t arg) {
210+
bool ret = rb_abi_guest_rb_set_should_prohibit_rewind(arg);
211+
return ret;
212+
}

ext/witapi/bindgen/rb-abi-guest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ extern "C"
6868
void rb_abi_guest_rb_clear_errinfo(void);
6969
void rb_abi_guest_rstring_ptr(rb_abi_guest_rb_abi_value_t value, rb_abi_guest_string_t *ret0);
7070
void rb_abi_guest_rb_vm_bugreport(void);
71+
bool rb_abi_guest_rb_set_should_prohibit_rewind(bool new_value);
7172
#ifdef __cplusplus
7273
}
7374
#endif

ext/witapi/bindgen/rb-abi-guest.wit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ rb-clear-errinfo: func()
1818
rstring-ptr: func(value: rb-abi-value) -> string
1919

2020
rb-vm-bugreport: func()
21+
22+
rb-set-should-prohibit-rewind: func(new-value: bool) -> bool

ext/witapi/witapi-core.c

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ __attribute__((import_module("asyncify"), import_name("stop_unwind"))) void
6363
asyncify_stop_unwind(void);
6464
__attribute__((import_module("asyncify"), import_name("start_rewind"))) void
6565
asyncify_start_rewind(void *buf);
66+
#define asyncify_start_rewind(buf) \
67+
asyncify_start_rewind((buf))
6668
__attribute__((import_module("asyncify"), import_name("stop_rewind"))) void
6769
asyncify_stop_rewind(void);
6870

@@ -79,6 +81,23 @@ void *rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *),
7981
# define RB_WASM_DEBUG_LOG(...) (void)0
8082
#endif
8183

84+
static bool rb_should_prohibit_rewind = false;
85+
86+
__attribute__((import_module("rb-js-abi-host"),
87+
import_name("rb_wasm_throw_prohibit_rewind_exception")))
88+
__attribute__((noreturn)) void
89+
rb_wasm_throw_prohibit_rewind_exception(const char *c_msg, size_t msg_len);
90+
91+
#define RB_WASM_CHECK_REWIND_PROHIBITED(msg) \
92+
/*
93+
If the unwond source and rewinding destination are same, it's acceptable
94+
to rewind even under nested VM operations.
95+
*/ \
96+
if (rb_should_prohibit_rewind && \
97+
(asyncify_buf != asyncify_unwound_buf || fiber_entry_point)) { \
98+
rb_wasm_throw_prohibit_rewind_exception(msg, sizeof(msg) - 1); \
99+
}
100+
82101
#define RB_WASM_LIB_RT(MAIN_ENTRY) \
83102
{ \
84103
\
@@ -93,24 +112,31 @@ void *rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *),
93112
} \
94113
\
95114
bool new_fiber_started = false; \
96-
void *asyncify_buf; \
115+
void *asyncify_buf = NULL; \
116+
extern void *rb_asyncify_unwind_buf; \
117+
void *asyncify_unwound_buf = rb_asyncify_unwind_buf; \
97118
asyncify_stop_unwind(); \
98119
\
99120
if ((asyncify_buf = rb_wasm_handle_jmp_unwind()) != NULL) { \
121+
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_jmp_unwind") \
100122
asyncify_start_rewind(asyncify_buf); \
101123
continue; \
102124
} \
103125
if ((asyncify_buf = rb_wasm_handle_scan_unwind()) != NULL) { \
126+
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_scan_unwind") \
104127
asyncify_start_rewind(asyncify_buf); \
105128
continue; \
106129
} \
107130
\
108131
asyncify_buf = rb_wasm_handle_fiber_unwind(&fiber_entry_point, &arg0, \
109132
&arg1, &new_fiber_started); \
110133
if (asyncify_buf) { \
134+
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_fiber_unwind") \
111135
asyncify_start_rewind(asyncify_buf); \
112136
continue; \
113137
} else if (new_fiber_started) { \
138+
RB_WASM_CHECK_REWIND_PROHIBITED( \
139+
"rb_wasm_handle_fiber_unwind but new fiber"); \
114140
continue; \
115141
} \
116142
\
@@ -303,4 +329,10 @@ void rb_vm_bugreport(const void *);
303329

304330
void rb_abi_guest_rb_vm_bugreport(void) { rb_vm_bugreport(NULL); }
305331

332+
bool rb_abi_guest_rb_set_should_prohibit_rewind(bool value) {
333+
bool old = rb_should_prohibit_rewind;
334+
rb_should_prohibit_rewind = value;
335+
return old;
336+
}
337+
306338
void Init_witapi(void) {}

packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class RbAbiGuest {
8080
rbClearErrinfo(): void;
8181
rstringPtr(value: RbAbiValue): string;
8282
rbVmBugreport(): void;
83+
rbSetShouldProhibitRewind(newValue: boolean): boolean;
8384
}
8485

8586
export class RbIseq {

packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { data_view, to_uint32, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab } from './intrinsics.js';
1+
import { data_view, to_uint32, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab, throw_invalid_bool } from './intrinsics.js';
22
export class RbAbiGuest {
33
constructor() {
44
this._resource0_slab = new Slab();
@@ -161,6 +161,11 @@ export class RbAbiGuest {
161161
rbVmBugreport() {
162162
this._exports['rb-vm-bugreport: func() -> ()']();
163163
}
164+
rbSetShouldProhibitRewind(arg0) {
165+
const ret = this._exports['rb-set-should-prohibit-rewind: func(new-value: bool) -> bool'](arg0 ? 1 : 0);
166+
const bool0 = ret;
167+
return bool0 == 0 ? false : (bool0 == 1 ? true : throw_invalid_bool());
168+
}
164169
}
165170

166171
export class RbIseq {

packages/npm-packages/ruby-wasm-wasi/src/index.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class RubyVM {
3838
// Wrap exported functions from Ruby VM to prohibit nested VM operation
3939
// if the call stack has sandwitched JS frames like JS -> Ruby -> JS -> Ruby.
4040
const proxyExports = (exports: RbAbi.RbAbiGuest) => {
41-
const excludedMethods: (keyof RbAbi.RbAbiGuest)[] = ["addToImports", "instantiate"];
41+
const excludedMethods: (keyof RbAbi.RbAbiGuest)[] = ["addToImports", "instantiate", "rbSetShouldProhibitRewind"];
4242
const excluded = ["constructor"].concat(excludedMethods);
4343
// wrap all methods in RbAbi.RbAbiGuest class
4444
for (const key of Object.getOwnPropertyNames(RbAbi.RbAbiGuest.prototype)) {
@@ -49,10 +49,15 @@ export class RubyVM {
4949
if (typeof value === "function") {
5050
exports[key] = (...args: any[]) => {
5151
const isNestedVMCall = this.interfaceState.hasJSFrameAfterRbFrame;
52+
let oldValue = false;
5253
if (isNestedVMCall) {
53-
throw new Error("Nested VM operation is prohibited. If you are trying to call RubyVM API environment through Ruby, please call it from JS environment directly.");
54+
oldValue = this.guest.rbSetShouldProhibitRewind(true)
5455
}
55-
return Reflect.apply(value, exports, args)
56+
const result = Reflect.apply(value, exports, args)
57+
if (isNestedVMCall) {
58+
this.guest.rbSetShouldProhibitRewind(oldValue)
59+
}
60+
return result;
5661
}
5762
}
5863
}
@@ -103,10 +108,35 @@ export class RubyVM {
103108
try {
104109
return { tag: "success", val: f(...args) };
105110
} catch (e) {
111+
if (e instanceof RbFatalError) {
112+
// RbFatalError should not be caught by Ruby because it Ruby VM
113+
// can be already in an inconsistent state.
114+
throw e;
115+
}
106116
return { tag: "failure", val: e };
107117
}
108118
};
109119
}
120+
imports["rb-js-abi-host"] = {
121+
rb_wasm_throw_prohibit_rewind_exception: (messagePtr: number, messageLen: number) => {
122+
const memory = this.instance.exports.memory as WebAssembly.Memory;
123+
const str = new TextDecoder().decode(
124+
new Uint8Array(memory.buffer, messagePtr, messageLen)
125+
);
126+
throw new RbFatalError(
127+
"Ruby APIs that may rewind the VM stack are prohibited under nested VM operation " + `(${str})\n`
128+
+ "Nested VM operation means that the call stack has sandwitched JS frames like JS -> Ruby -> JS -> Ruby "
129+
+ "caused by something like `window.rubyVM.eval(\"JS.global[:rubyVM].eval('Fiber.yield')\")`\n"
130+
+ "\n"
131+
+ "Please check your call stack and make sure that you are **not** doing any of the following inside the nested Ruby frame:\n"
132+
+ " 1. Switching fibers (e.g. Fiber#resume, Fiber.yield, and Fiber#transfer)\n"
133+
+ " Note that JS::Object#await switches fibers internally\n"
134+
+ " 2. Raising uncaught exceptions\n"
135+
+ " Please catch all exceptions inside the nested operation\n"
136+
+ " 3. Calling Continuation APIs\n"
137+
);
138+
}
139+
}
110140
// NOTE: The GC may collect objects that are still referenced by Wasm
111141
// locals because Asyncify cannot scan the Wasm stack above the JS frame.
112142
// So we need to keep track whether the JS frame is sandwitched by Ruby
@@ -621,3 +651,16 @@ export class RbError extends Error {
621651
super(message);
622652
}
623653
}
654+
655+
/**
656+
* Error class thrown by Ruby execution when it is not possible to recover.
657+
* This is usually caused when Ruby VM is in an inconsistent state.
658+
*/
659+
export class RbFatalError extends RbError {
660+
/**
661+
* @hideconstructor
662+
*/
663+
constructor(message: string) {
664+
super("Ruby Fatal Error: " + message);
665+
}
666+
}

packages/npm-packages/ruby-wasm-wasi/test/vm.test.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,29 +120,32 @@ eval:11:in \`<main>'`);
120120
expect(vm.eval(`__ENCODING__.name`).toString()).toBe("UTF-8");
121121
});
122122

123-
// FIXME: The below two tests are now failing because we cannot make it safe
124-
// to call eval within eval.
125-
// See https://github.com/ruby/ruby.wasm/issues/219#issuecomment-1551758711
126-
test.failing("Nested Fiber switch", async () => {
127-
const vm = await initRubyVM();
128-
const setVM = vm.eval(`proc { |vm| JS::RubyVM = vm }`)
129-
setVM.call("call", vm.wrap(vm))
130-
vm.eval(`
123+
test.each([
124+
`JS::RubyVM.eval('Fiber.yield')`,
125+
`
131126
$f0 = Fiber.new {
132127
JS::RubyVM.eval("$f1.resume")
133128
}
134129
$f1 = Fiber.new {}
135130
$f0.resume
136-
`)
137-
vm.eval(`puts $q.inspect`)
131+
`,
132+
`JS::RubyVM.eval("raise 'Exception from nested eval'")`
133+
])
134+
("nested VM rewinding operation should throw fatal error", async (code) => {
135+
const vm = await initRubyVM();
136+
const setVM = vm.eval(`proc { |vm| JS::RubyVM = vm }`)
137+
setVM.call("call", vm.wrap(vm))
138+
expect(() => {
139+
vm.eval(code)
140+
}).toThrowError("Ruby APIs that may rewind the VM stack are prohibited")
138141
})
139142

140-
test.failing("raise in nested eval", async () => {
143+
test("caught raise in nested eval is ok", async () => {
141144
const vm = await initRubyVM();
142145
const setVM = vm.eval(`proc { |vm| JS::RubyVM = vm }`)
143146
setVM.call("call", vm.wrap(vm))
144147
expect(() => {
145-
vm.eval(`JS::RubyVM.eval("raise 'Exception from nested eval'")`)
146-
}).toThrowError("Exception from nested eval")
148+
vm.eval(`JS::RubyVM.eval("begin; raise 'Exception from nested eval'; rescue; end")`)
149+
}).not.toThrowError()
147150
})
148151
});

0 commit comments

Comments
 (0)