diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 3156a65b..3c203ac2 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -317,7 +317,7 @@ jobs: with: path: | libretro/build/libretro-stage1 - key: libretro-stage1-${{ hashFiles('libretro/Makefile', 'libretro/ruby-bindings.h', 'libretro/sandbox-bindgen.rb', 'libretro/ruby-compat.patch') }} + key: libretro-stage1-${{ hashFiles('libretro/Makefile', 'libretro/ruby-bindings.h', 'libretro/sandbox-bindgen.rb', 'libretro/ruby-stack-pointer.patch', 'libretro/ruby-jump-buffer.patch', 'libretro/ruby-compat.patch') }} - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' diff --git a/libretro/Makefile b/libretro/Makefile index f6c22572..ce76cae5 100644 --- a/libretro/Makefile +++ b/libretro/Makefile @@ -135,17 +135,14 @@ $(DOWNLOADS)/crossruby/configure: $(DOWNLOADS)/crossruby/configure.ac $(RUBY) cd $(DOWNLOADS)/crossruby && $(RUBY) tool/downloader.rb -d tool -e gnu config.guess config.sub cd $(DOWNLOADS)/crossruby && $(AUTORECONF) -i -$(DOWNLOADS)/crossruby/configure.ac: $(DOWNLOADS)/ruby-12995.patch +$(DOWNLOADS)/crossruby/configure.ac: mkdir -p $(DOWNLOADS) $(CLONE) $(GITHUB)/ruby/ruby $(DOWNLOADS)/crossruby -b $(RUBY_VERSION) - cd $(DOWNLOADS)/crossruby && $(GIT) apply $(DOWNLOADS)/ruby-12995.patch + cd $(DOWNLOADS)/crossruby && $(GIT) apply ${PWD}/ruby-stack-pointer.patch + cd $(DOWNLOADS)/crossruby && $(GIT) apply ${PWD}/ruby-jump-buffer.patch cd $(DOWNLOADS)/crossruby && $(GIT) apply ${PWD}/ruby-compat.patch echo '#include "${PWD}/ruby-bindings.h"' >> $(DOWNLOADS)/crossruby/gc.c -$(DOWNLOADS)/ruby-12995.patch: - mkdir -p $(DOWNLOADS) - $(CURL) -Lo $(DOWNLOADS)/ruby-12995.patch $(GITHUB)/ruby/ruby/pull/12995.diff - # Base Ruby (targets the build machine) $(RUBY): $(DOWNLOADS)/baseruby/Makefile diff --git a/libretro/ruby-jump-buffer.patch b/libretro/ruby-jump-buffer.patch new file mode 100644 index 00000000..5097427a --- /dev/null +++ b/libretro/ruby-jump-buffer.patch @@ -0,0 +1,53 @@ +# Fixes a memory leak in WASI builds of Ruby where VM jump buffers are sometimes not freed. + +--- a/eval_intern.h ++++ b/eval_intern.h +@@ -163,6 +163,18 @@ rb_ec_tag_jump(const rb_execution_context_t *ec, enum ruby_tag_type st) + { + RUBY_ASSERT(st != TAG_NONE); + ec->tag->state = st; ++ ++#if defined(__wasm__) && !defined(__EMSCRIPTEN__) ++ /* Destroy all the jump buffers that belong to tags between the current tag ++ * and the tag we're jumping to, since jump buffers are allocated on the ++ * heap on this platform instead of on the stack like on most other ++ * platforms. */ ++ for (struct rb_vm_tag *tag = GET_EC()->tag; tag != ec->tag; tag = tag->prev) { ++ RUBY_ASSERT(tag != NULL); ++ rb_vm_tag_jmpbuf_deinit(&tag->buf); ++ } ++#endif ++ + ruby_longjmp(RB_VM_TAG_JMPBUF_GET(ec->tag->buf), 1); + } + +--- a/signal.c ++++ b/signal.c +@@ -849,6 +849,7 @@ check_stack_overflow(int sig, const uintptr_t addr, const ucontext_t *ctx) + * otherwise it can cause stack overflow again at the same + * place. */ + if ((crit = (!ec->tag->prev || !--uplevel)) != FALSE) break; ++ rb_vm_tag_jmpbuf_deinit(&ec->tag->buf); + ec->tag = ec->tag->prev; + } + reset_sigmask(sig); +--- a/vm.c ++++ b/vm.c +@@ -2729,6 +2729,7 @@ vm_exec_handle_exception(rb_execution_context_t *ec, enum ruby_tag_type state, V + if (VM_FRAME_FINISHED_P(ec->cfp)) { + rb_vm_pop_frame(ec); + ec->errinfo = (VALUE)err; ++ rb_vm_tag_jmpbuf_deinit(&ec->tag->buf); + ec->tag = ec->tag->prev; + EC_JUMP_TAG(ec, state); + } +--- a/vm_trace.c ++++ b/vm_trace.c +@@ -455,6 +455,7 @@ rb_exec_event_hooks(rb_trace_arg_t *trace_arg, rb_hook_list_t *hooks, int pop_p) + if (state) { + if (pop_p) { + if (VM_FRAME_FINISHED_P(ec->cfp)) { ++ rb_vm_tag_jmpbuf_deinit(&ec->tag->buf); + ec->tag = ec->tag->prev; + } + rb_vm_pop_frame(ec); diff --git a/libretro/ruby-stack-pointer.patch b/libretro/ruby-stack-pointer.patch new file mode 100644 index 00000000..79f4a070 --- /dev/null +++ b/libretro/ruby-stack-pointer.patch @@ -0,0 +1,48 @@ +# Fixes a bug in WASI builds of Ruby where the stack pointer is sometimes not reset after a longjmp, +# leading to leaking of parts of the stack and eventual crashes from stack buffer overflow. +# Copied from https://github.com/ruby/ruby/pull/12995. + +--- a/wasm/setjmp.c ++++ b/wasm/setjmp.c +@@ -143,9 +143,11 @@ rb_wasm_try_catch_init(struct rb_wasm_try_catch *try_catch, + try_catch->try_f = try_f; + try_catch->catch_f = catch_f; + try_catch->context = context; ++ try_catch->stack_pointer = NULL; + } + + // NOTE: This function is not processed by Asyncify due to a call of asyncify_stop_rewind ++__attribute__((noinline)) + void + rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf *target) + { +@@ -154,6 +156,10 @@ rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf + + target->state = JMP_BUF_STATE_CAPTURED; + ++ if (try_catch->stack_pointer == NULL) { ++ try_catch->stack_pointer = rb_wasm_get_stack_pointer(); ++ } ++ + switch ((enum try_catch_phase)try_catch->state) { + case TRY_CATCH_PHASE_MAIN: + // may unwind +@@ -175,6 +181,8 @@ rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf + // stop unwinding + // (but call stop_rewind to update the asyncify state to "normal" from "unwind") + asyncify_stop_rewind(); ++ // reset the stack pointer to what it was before the most recent call to try_f or catch_f ++ rb_wasm_set_stack_pointer(try_catch->stack_pointer); + // clear the active jmpbuf because it's already stopped + _rb_wasm_active_jmpbuf = NULL; + // reset jmpbuf state to be able to unwind again +--- a/wasm/setjmp.h ++++ b/wasm/setjmp.h +@@ -65,6 +65,7 @@ struct rb_wasm_try_catch { + rb_wasm_try_catch_func_t try_f; + rb_wasm_try_catch_func_t catch_f; + void *context; ++ void *stack_pointer; + int state; + }; +