From: "white-axe (Hao Liu) via ruby-core" Date: 2025-10-05T18:40:13+00:00 Subject: [ruby-core:123399] [Ruby Bug#21626] Backport WASI setjmp handler memory leak fixes Issue #21626 has been reported by white-axe (Hao Liu). ---------------------------------------- Bug #21626: Backport WASI setjmp handler memory leak fixes https://bugs.ruby-lang.org/issues/21626 * Author: white-axe (Hao Liu) * Status: Open * ruby -v: ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [x86_64-linux] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- The WASI builds of Ruby 3.2, 3.3 and 3.4 currently have bugs in the handling of setjmp/longjmp that cause memory leaks. In Ruby 3.2, 3.3 and 3.4, there is a bug where the stack pointer is sometimes not reset after a longjmp, leading to leaking of part of the stack and the garbage collector being unable to garbage collect some Ruby objects since the garbage collector looks for GC roots on the stack. In Ruby 3.3 and 3.4, https://github.com/ruby/ruby/pull/8902 caused a second bug where setjmp buffers are not freed after a longjmp. The combination of these two bugs causes the WASI builds of Ruby 3.3 and 3.4 to very rapidly run out of memory when running this script. The memory leak is much less noticeable in the WASI build of Ruby 3.2. ```ruby loop do array = [0] for item in array break end end ``` To test it out, save this script as test.rb in the current working directory and then run these commands. This tests the WASI build of Ruby 3.4, but you can change the "3.4" to "3.3" to test out the build for Ruby 3.3. If you use the Ruby 3.5 build instead by replacing "3.4" with "head", it will not crash anymore. ``` curl -Lo ruby.tar.gz https://github.com/ruby/ruby.wasm/releases/download/2.7.2/ruby-3.4-wasm32-unknown-wasip1-full.tar.gz tar xzf ruby.tar.gz --strip-components=1 wasmtime --dir .::/ usr/local/bin/ruby test.rb ``` These two bugs should have been fixed in Ruby 3.5 by https://github.com/ruby/ruby/pull/12995, but the fix contained bugs that were later amended by https://github.com/ruby/ruby/pull/13026 and https://github.com/ruby/ruby/pull/13142. I'm requesting for the changes from all three of these pull requests to be backported to Ruby 3.2, 3.3 and 3.4, which could be done with the patch below. For Ruby 3.2, only the changes to wasm/setjmp.c and wasm/setjmp.h are required because the other changes are only to fix the second bug. ```patch --- a/cont.c +++ b/cont.c @@ -1509,6 +1509,51 @@ cont_restore_thread(rb_context_t *cont) rb_raise(rb_eRuntimeError, "can't call across trace_func"); } +#if defined(__wasm__) && !defined(__EMSCRIPTEN__) + if (th->ec->tag != sec->tag) { + /* find the lowest common ancestor tag of the current EC and the saved EC */ + + struct rb_vm_tag *lowest_common_ancestor = NULL; + size_t num_tags = 0; + size_t num_saved_tags = 0; + for (struct rb_vm_tag *tag = th->ec->tag; tag != NULL; tag = tag->prev) { + ++num_tags; + } + for (struct rb_vm_tag *tag = sec->tag; tag != NULL; tag = tag->prev) { + ++num_saved_tags; + } + + size_t min_tags = num_tags <= num_saved_tags ? num_tags : num_saved_tags; + + struct rb_vm_tag *tag = th->ec->tag; + while (num_tags > min_tags) { + tag = tag->prev; + --num_tags; + } + + struct rb_vm_tag *saved_tag = sec->tag; + while (num_saved_tags > min_tags) { + saved_tag = saved_tag->prev; + --num_saved_tags; + } + + while (min_tags > 0) { + if (tag == saved_tag) { + lowest_common_ancestor = tag; + break; + } + tag = tag->prev; + saved_tag = saved_tag->prev; + --min_tags; + } + + /* free all the jump buffers between the current EC's tag and the lowest common ancestor tag */ + for (struct rb_vm_tag *tag = th->ec->tag; tag != lowest_common_ancestor; tag = tag->prev) { + rb_vm_tag_jmpbuf_deinit(&tag->buf); + } + } +#endif + /* copy vm stack */ #ifdef CAPTURE_JUST_VALID_VM_STACK MEMCPY(th->ec->vm_stack, --- a/signal.c +++ b/signal.c @@ -854,6 +854,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 @@ -2829,6 +2829,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); --- 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; }; ``` -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/