Skip to content

Re-design evalAsync and reject illegal API use for safety #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions ext/js/lib/js.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,24 @@ module JS
Null = JS.eval("return null")

class PromiseScheduler
Task = Struct.new(:fiber, :status, :value)

def initialize(main_fiber)
@tasks = []
@is_spinning = false
@loop_fiber =
Fiber.new do
loop do
while task = @tasks.shift
task.fiber.transfer(task.value, task.status)
end
@is_spinning = false
main_fiber.transfer
end
end
def initialize(loop)
@loop = loop
end

def await(promise)
current = Fiber.current
promise.call(
:then,
->(value) { enqueue Task.new(current, :success, value) },
->(value) { enqueue Task.new(current, :failure, value) }
->(value) { current.transfer(value, :success) },
->(value) { current.transfer(value, :failure) }
)
value, status = @loop_fiber.transfer
if @loop == current
raise "JS::Object#await can be called only from evalAsync"
end
value, status = @loop.transfer
raise JS::Error.new(value) if status == :failure
value
end

def enqueue(task)
@tasks << task
unless @is_spinning
@is_spinning = true
JS.global.queueMicrotask -> { @loop_fiber.transfer }
end
end
end

@promise_scheduler = PromiseScheduler.new Fiber.current
Expand Down Expand Up @@ -120,8 +102,8 @@ def respond_to_missing?(sym, include_private)
# This method looks like a synchronous method, but it actually runs asynchronously using fibers.
# In other words, the next line to the `await` call at Ruby source will be executed after the
# promise will be resolved. However, it does not block JavaScript event loop, so the next line
# to the `RubyVM.eval` or `RubyVM.evalAsync` (in the case when no `await` operator before the
# call expression) at JavaScript source will be executed without waiting for the promise.
# to the RubyVM.evalAsync` (in the case when no `await` operator before the call expression)
# at JavaScript source will be executed without waiting for the promise.
#
# The below example shows how the execution order goes. It goes in the order of "step N"
#
Expand Down
15 changes: 15 additions & 0 deletions ext/witapi/bindgen/rb-abi-guest.c
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,18 @@ __attribute__((export_name("rb-vm-bugreport: func() -> ()")))
void __wasm_export_rb_abi_guest_rb_vm_bugreport(void) {
rb_abi_guest_rb_vm_bugreport();
}
__attribute__((export_name("rb-gc-enable: func() -> bool")))
int32_t __wasm_export_rb_abi_guest_rb_gc_enable(void) {
bool ret = rb_abi_guest_rb_gc_enable();
return ret;
}
__attribute__((export_name("rb-gc-disable: func() -> bool")))
int32_t __wasm_export_rb_abi_guest_rb_gc_disable(void) {
bool ret = rb_abi_guest_rb_gc_disable();
return ret;
}
__attribute__((export_name("rb-set-should-prohibit-rewind: func(new-value: bool) -> bool")))
int32_t __wasm_export_rb_abi_guest_rb_set_should_prohibit_rewind(int32_t arg) {
bool ret = rb_abi_guest_rb_set_should_prohibit_rewind(arg);
return ret;
}
3 changes: 3 additions & 0 deletions ext/witapi/bindgen/rb-abi-guest.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ extern "C"
void rb_abi_guest_rb_clear_errinfo(void);
void rb_abi_guest_rstring_ptr(rb_abi_guest_rb_abi_value_t value, rb_abi_guest_string_t *ret0);
void rb_abi_guest_rb_vm_bugreport(void);
bool rb_abi_guest_rb_gc_enable(void);
bool rb_abi_guest_rb_gc_disable(void);
bool rb_abi_guest_rb_set_should_prohibit_rewind(bool new_value);
#ifdef __cplusplus
}
#endif
Expand Down
5 changes: 5 additions & 0 deletions ext/witapi/bindgen/rb-abi-guest.wit
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ rb-clear-errinfo: func()
rstring-ptr: func(value: rb-abi-value) -> string

rb-vm-bugreport: func()

rb-gc-enable: func() -> bool
rb-gc-disable: func() -> bool

rb-set-should-prohibit-rewind: func(new-value: bool) -> bool
44 changes: 43 additions & 1 deletion ext/witapi/witapi-core.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ void *rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *),
# define RB_WASM_DEBUG_LOG(...) (void)0
#endif

static bool rb_should_prohibit_rewind = false;

__attribute__((import_module("rb-js-abi-host"),
import_name("rb_wasm_throw_prohibit_rewind_exception")))
__attribute__((noreturn)) void
rb_wasm_throw_prohibit_rewind_exception(const char *c_msg, size_t msg_len);

#define RB_WASM_CHECK_REWIND_PROHIBITED(msg) \
/* \
If the unwond source and rewinding destination are same, it's acceptable \
to rewind even under nested VM operations. \
*/ \
if (rb_should_prohibit_rewind && \
(asyncify_buf != asyncify_unwound_buf || fiber_entry_point)) { \
rb_wasm_throw_prohibit_rewind_exception(msg, sizeof(msg) - 1); \
}

#define RB_WASM_LIB_RT(MAIN_ENTRY) \
{ \
\
Expand All @@ -93,24 +110,31 @@ void *rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *),
} \
\
bool new_fiber_started = false; \
void *asyncify_buf; \
void *asyncify_buf = NULL; \
extern void *rb_asyncify_unwind_buf; \
void *asyncify_unwound_buf = rb_asyncify_unwind_buf; \
asyncify_stop_unwind(); \
\
if ((asyncify_buf = rb_wasm_handle_jmp_unwind()) != NULL) { \
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_jmp_unwind") \
asyncify_start_rewind(asyncify_buf); \
continue; \
} \
if ((asyncify_buf = rb_wasm_handle_scan_unwind()) != NULL) { \
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_scan_unwind") \
asyncify_start_rewind(asyncify_buf); \
continue; \
} \
\
asyncify_buf = rb_wasm_handle_fiber_unwind(&fiber_entry_point, &arg0, \
&arg1, &new_fiber_started); \
if (asyncify_buf) { \
RB_WASM_CHECK_REWIND_PROHIBITED("rb_wasm_handle_fiber_unwind") \
asyncify_start_rewind(asyncify_buf); \
continue; \
} else if (new_fiber_started) { \
RB_WASM_CHECK_REWIND_PROHIBITED( \
"rb_wasm_handle_fiber_unwind but new fiber"); \
continue; \
} \
\
Expand Down Expand Up @@ -303,4 +327,22 @@ void rb_vm_bugreport(const void *);

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

bool rb_abi_guest_rb_gc_enable(void) { return rb_gc_enable() == Qtrue; }

VALUE rb_gc_disable_no_rest(void);
bool rb_abi_guest_rb_gc_disable(void) {
// NOTE: rb_gc_disable() is usually preferred to free up memory as much as
// possible before disabling GC. However it may trigger GC through gc_rest(),
// and triggering GC having a sandwitched JS frame is unsafe because it misses
// to mark some living objects in the frames behind the JS frame. So we use
// rb_gc_disable_no_rest(), which does not trigger GC, instead.
return rb_gc_disable_no_rest() == Qtrue;
}

bool rb_abi_guest_rb_set_should_prohibit_rewind(bool value) {
bool old = rb_should_prohibit_rewind;
rb_should_prohibit_rewind = value;
return old;
}

void Init_witapi(void) {}
7 changes: 3 additions & 4 deletions packages/npm-packages/ruby-wasm-wasi/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ function variant(basename, { browser = false } = {}) {
},
],
plugins: [
...(browser ? [
nodePolyfills(),
inject({ Buffer: ['buffer', 'Buffer']}),
] : []),
...(browser
? [nodePolyfills(), inject({ Buffer: ["buffer", "Buffer"] })]
: []),
typescript(typescriptOptions),
nodeResolve(),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export class RbAbiGuest {
rbClearErrinfo(): void;
rstringPtr(value: RbAbiValue): string;
rbVmBugreport(): void;
rbGcEnable(): boolean;
rbGcDisable(): boolean;
rbSetShouldProhibitRewind(newValue: boolean): boolean;
}

export class RbIseq {
Expand Down
17 changes: 16 additions & 1 deletion packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-abi-guest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { data_view, to_uint32, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab } from './intrinsics.js';
import { data_view, to_uint32, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab, throw_invalid_bool } from './intrinsics.js';
export class RbAbiGuest {
constructor() {
this._resource0_slab = new Slab();
Expand Down Expand Up @@ -161,6 +161,21 @@ export class RbAbiGuest {
rbVmBugreport() {
this._exports['rb-vm-bugreport: func() -> ()']();
}
rbGcEnable() {
const ret = this._exports['rb-gc-enable: func() -> bool']();
const bool0 = ret;
return bool0 == 0 ? false : (bool0 == 1 ? true : throw_invalid_bool());
}
rbGcDisable() {
const ret = this._exports['rb-gc-disable: func() -> bool']();
const bool0 = ret;
return bool0 == 0 ? false : (bool0 == 1 ? true : throw_invalid_bool());
}
rbSetShouldProhibitRewind(arg0) {
const ret = this._exports['rb-set-should-prohibit-rewind: func(new-value: bool) -> bool'](arg0 ? 1 : 0);
const bool0 = ret;
return bool0 == 0 ? false : (bool0 == 1 ? true : throw_invalid_bool());
}
}

export class RbIseq {
Expand Down
24 changes: 17 additions & 7 deletions packages/npm-packages/ruby-wasm-wasi/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ const consolePrinter = () => {

return {
addToImports(imports: WebAssembly.Imports): void {
const original = imports.wasi_snapshot_preview1.fd_write as (fd: number, iovs: number, iovsLen: number, nwritten: number) => number;
imports.wasi_snapshot_preview1.fd_write = (fd: number, iovs: number, iovsLen: number, nwritten: number): number => {
const original = imports.wasi_snapshot_preview1.fd_write as (
fd: number,
iovs: number,
iovsLen: number,
nwritten: number
) => number;
imports.wasi_snapshot_preview1.fd_write = (
fd: number,
iovs: number,
iovsLen: number,
nwritten: number
): number => {
if (fd !== 1 && fd !== 2) {
return original(fd, iovs, iovsLen, nwritten);
}

if (typeof memory === 'undefined' || typeof view === 'undefined') {
throw new Error('Memory is not set');
if (typeof memory === "undefined" || typeof view === "undefined") {
throw new Error("Memory is not set");
}
if (view.buffer.byteLength === 0) {
view = new DataView(memory.buffer);
Expand All @@ -30,7 +40,7 @@ const consolePrinter = () => {
});

let written = 0;
let str = '';
let str = "";
for (const buffer of buffers) {
str += decoder.decode(buffer);
written += buffer.byteLength;
Expand All @@ -46,8 +56,8 @@ const consolePrinter = () => {
setMemory(m: WebAssembly.Memory) {
memory = m;
view = new DataView(m.buffer);
}
}
},
};
};

export const DefaultRubyVM = async (
Expand Down
Loading