From 8c7c80f4da34c38ad3acb88ea77cf7975c471935 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Sun, 16 Aug 2026 15:32:49 +0000 Subject: [PATCH 1/2] coroutine: do not write to the target after switching away coroutine_transfer() restores target->from after the switch, that is, after this coroutine is resumed -- by then it may have been resumed by someone else and target may be freed. With M:N threads it usually is: a Ruby thread parks by transferring to its shared native thread's context (thread_sched_wait_running_turn), and that thread, finding the ready queue empty, retires and frees nt and nt->nt_context. Over 40 retires in the reproducer below, the number of threads still parked on the retiring context was a median of 19 (max 61, zero in four cases); each writes to it when it is next resumed. sizeof(ucontext_t) is 968, so the write lands in a freed 1008-byte region, and once that is reused it corrupts the new owner: free(): invalid next size (fast) ruby_xfree_sized ... st_free_entries -> rb_st_free_table ractor_free_all_ports -> ractor_notify_exit -> ractor_atexit thread_start_func_2 -> co_start -> coroutine_trampoline AddressSanitizer: heap-use-after-free WRITE of size 8 in coroutine_transfer (Context.h:97), thread T3 968 bytes inside a 1008-byte region freed in nt_start -> native_thread_destroy_self, allocated in native_thread_alloc from is read only by the trampoline, when target starts, so the restore is dead by the time the switch returns. Skip it under COROUTINE_TARGET_MAY_BE_FREED, which thread_pthread.c defines; cont.c and other embedders keep the original behaviour. Only ucontext runs M:N threads today, but the lifetime rule is not specific to them, so guard every backend that has the restore. Fibers never hit this: a suspended fiber still references its target, so it cannot be collected. A native thread's context is not a Ruby object and is freed by hand. Reproducer: clang-20 -O1 --enable-shared --with-coroutine=ucontext, bootstraptest/test_ractor.rb (the case at line 2603 spawns 2000 Ractors) four times in parallel on four CPUs: 21 of 48 runs died before, 0 of 48 after. Under ASAN, the isolated case eight at a time: 120 of 120 reported before, 0 of 120 after. CI: https://github.com/ruby/ruby/actions/runs/31942932752/job/95154613098 Co-Authored-By: Claude Opus 5 (1M context) --- coroutine/asyncify/Context.h | 6 ++++++ coroutine/emscripten/Context.h | 6 ++++++ coroutine/pthread/Context.c | 4 ++++ coroutine/ucontext/Context.h | 6 ++++++ thread_pthread.c | 4 ++++ 5 files changed, 26 insertions(+) diff --git a/coroutine/asyncify/Context.h b/coroutine/asyncify/Context.h index 71791a400492b5..4e1a804923a5ef 100644 --- a/coroutine/asyncify/Context.h +++ b/coroutine/asyncify/Context.h @@ -62,7 +62,9 @@ static inline void coroutine_initialize(struct coroutine_context *context, corou static inline struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target) { if (ASYNCIFY_CORO_DEBUG) fprintf(stderr, "[%s] entry (current = %p, target = %p)\n", __func__, current, target); +#ifndef COROUTINE_TARGET_MAY_BE_FREED struct coroutine_context * previous = target->from; +#endif target->from = current; if (ASYNCIFY_CORO_DEBUG) fprintf(stderr, "[%s] current->current_sp = %p -> %p\n", __func__, current->current_sp, rb_wasm_get_stack_pointer()); @@ -77,7 +79,11 @@ static inline struct coroutine_context * coroutine_transfer(struct coroutine_con rb_wasm_set_stack_pointer(current->current_sp); +#ifndef COROUTINE_TARGET_MAY_BE_FREED + /* from is read only by coroutine_trampoline, when target starts, which has + * happened before we get here. */ target->from = previous; +#endif return target; } diff --git a/coroutine/emscripten/Context.h b/coroutine/emscripten/Context.h index 361e241685682f..c26b4b23bbde92 100644 --- a/coroutine/emscripten/Context.h +++ b/coroutine/emscripten/Context.h @@ -60,11 +60,17 @@ static inline void coroutine_initialize( static inline struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target) { +#ifndef COROUTINE_TARGET_MAY_BE_FREED struct coroutine_context * previous = target->from; +#endif target->from = current; emscripten_fiber_swap(¤t->state, &target->state); +#ifndef COROUTINE_TARGET_MAY_BE_FREED + /* from is read only by coroutine_trampoline, when target starts, which has + * happened before we get here. */ target->from = previous; +#endif return target; } diff --git a/coroutine/pthread/Context.c b/coroutine/pthread/Context.c index 38774cda0b409d..014abbd4b59b30 100644 --- a/coroutine/pthread/Context.c +++ b/coroutine/pthread/Context.c @@ -229,7 +229,11 @@ struct coroutine_context * coroutine_transfer(struct coroutine_context * current pthread_testcancel(); #endif +#ifndef COROUTINE_TARGET_MAY_BE_FREED + /* from is read only by coroutine_trampoline, when target starts, which has + * happened before we get here. */ target->from = previous; +#endif return target; } diff --git a/coroutine/ucontext/Context.h b/coroutine/ucontext/Context.h index d338d8de608c3f..0063f72b77beee 100644 --- a/coroutine/ucontext/Context.h +++ b/coroutine/ucontext/Context.h @@ -60,11 +60,17 @@ static inline void coroutine_initialize( static inline struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target) { +#ifndef COROUTINE_TARGET_MAY_BE_FREED struct coroutine_context * previous = target->from; +#endif target->from = current; swapcontext(¤t->state, &target->state); +#ifndef COROUTINE_TARGET_MAY_BE_FREED + /* from is read only by coroutine_trampoline, when target starts, which has + * happened before we get here. */ target->from = previous; +#endif return target; } diff --git a/thread_pthread.c b/thread_pthread.c index b5de62c37db882..2500e8998b8d48 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -62,6 +62,10 @@ static pthread_condattr_t *condattr_monotonic = &condattr_mono; static const void *const condattr_monotonic = NULL; #endif +/* A retiring shared native thread frees its own context while the threads it + * parked are still suspended with that context as their target. */ +#define COROUTINE_TARGET_MAY_BE_FREED 1 + #include COROUTINE_H #ifndef HAVE_SYS_EVENT_H From 83131dbf448364130928b9998d1959b9503c5d21 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Aug 2026 10:29:06 -0700 Subject: [PATCH 2/2] [ruby/digest] Fix build on i686 and c99 https://github.com/ruby/digest/commit/59e9356096 --- ext/digest/blake3/blake3_dispatch.c | 5 +++-- ext/digest/blake3/extconf.rb | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ext/digest/blake3/blake3_dispatch.c b/ext/digest/blake3/blake3_dispatch.c index 14dfbbe0c8f385..f58eebdec12dbc 100644 --- a/ext/digest/blake3/blake3_dispatch.c +++ b/ext/digest/blake3/blake3_dispatch.c @@ -19,7 +19,8 @@ #endif #if !defined(BLAKE3_ATOMICS) -#if defined(__has_include) +#if defined(__has_include) && defined(__STDC_VERSION__) && \ + __STDC_VERSION__ >= 201112L #if __has_include() && !defined(_MSC_VER) #define BLAKE3_ATOMICS 1 #else @@ -27,7 +28,7 @@ #endif /* __has_include() && !defined(_MSC_VER) */ #else #define BLAKE3_ATOMICS 0 -#endif /* defined(__has_include) */ +#endif /* defined(__has_include) && C11 */ #endif /* BLAKE3_ATOMICS */ #if BLAKE3_ATOMICS diff --git a/ext/digest/blake3/extconf.rb b/ext/digest/blake3/extconf.rb index 6d1b8f54e34bc0..05c98507ef44fd 100644 --- a/ext/digest/blake3/extconf.rb +++ b/ext/digest/blake3/extconf.rb @@ -24,6 +24,10 @@ # Extra per-object compiler flags, keyed by object basename. simd_cflags = {} +def blake3_disable(macro) + $CPPFLAGS << " -D#{macro}" +end + # Probe used to confirm the compiler both accepts +flag+ and can compile the # intrinsics the backend relies on. def blake3_have_isa?(name, flag, snippet) @@ -55,7 +59,7 @@ def blake3_have_isa?(name, flag, snippet) objs << obj simd_cflags[obj] = flag else - $defs << "-D#{no_macro}" + blake3_disable(no_macro) end end when /\A(aarch64|arm64)\z/i @@ -67,11 +71,11 @@ def blake3_have_isa?(name, flag, snippet) # No optimized backend wired up for this architecture (e.g. 32-bit x86, # ppc): build portable-only. Disabling every x86 ISA keeps the dispatcher # from referencing backends we didn't compile, and NEON is forced off. - $defs << "-DBLAKE3_NO_SSE2" - $defs << "-DBLAKE3_NO_SSE41" - $defs << "-DBLAKE3_NO_AVX2" - $defs << "-DBLAKE3_NO_AVX512" - $defs << "-DBLAKE3_USE_NEON=0" + blake3_disable("BLAKE3_NO_SSE2") + blake3_disable("BLAKE3_NO_SSE41") + blake3_disable("BLAKE3_NO_AVX2") + blake3_disable("BLAKE3_NO_AVX512") + blake3_disable("BLAKE3_USE_NEON=0") end $objs = objs.map { |o| "#{o}.#{$OBJEXT}" }