From e1864fd9680e8df84efa89bb6abc39d8eb58ff13 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 14:22:07 +0900 Subject: [PATCH 01/25] Show failures as diagnostics --- .github/workflows/check_misc.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 837537203b491f..b96f5fb3b8885f 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -64,7 +64,8 @@ jobs: - name: Check for bash specific substitution in configure.ac run: | - git grep -n '\${[A-Za-z_0-9]*/' -- configure.ac && exit 1 || : + git grep -n '\${[A-Za-z_0-9]*/' -- configure.ac | sed "s/^/::error::/" + test ${PIPESTATUS[0]} != 0 - name: Check for header macros run: | @@ -72,7 +73,7 @@ jobs: for header in ruby/*.h; do git grep -l -F -e $header -e HAVE_`echo $header | tr a-z./ A-Z__` -- . > /dev/null && continue fail=1 - echo $header + echo "::error::$header" done exit $fail working-directory: include From f288c7ecffb3125b6b4711d53cc7060d31c805a0 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 13:33:40 +0900 Subject: [PATCH 02/25] Check for __has_builtin fallbacks --- .github/workflows/check_misc.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index b96f5fb3b8885f..98016d6351c9af 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -78,6 +78,20 @@ jobs: exit $fail working-directory: include + - name: Check for __has_builtin functions + run: | + missing=() + for check in $(git grep -h --no-line-number -o '__has_builtin(__[_a-zA-Z0-9]*)' | sort -u); do + func="${check#__has_builtin(}" + func="${func%)}" + git -P grep -w "RBIMPL_HAS_BUILTIN_$func" > /dev/null || missing+=("-e" "$check") + done + [ ${#missing[@]} = 0 ] || { + echo "::error::Missing __has_builtin fallbacks:" + git -P grep -F "${missing[@]}" | sed 's/^/::error::/' | head + false + } + - id: now run: | date +"mon=%-m"%n"day=%-d" >> $GITHUB_OUTPUT From 37893387ffbeba0340b390c35fbcb82cc573588a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 14:46:04 +0900 Subject: [PATCH 03/25] Continue all checks even if earlier checks failed --- .github/workflows/check_misc.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 98016d6351c9af..af677aa247e974 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -66,6 +66,7 @@ jobs: run: | git grep -n '\${[A-Za-z_0-9]*/' -- configure.ac | sed "s/^/::error::/" test ${PIPESTATUS[0]} != 0 + if: always() - name: Check for header macros run: | @@ -77,6 +78,7 @@ jobs: done exit $fail working-directory: include + if: always() - name: Check for __has_builtin functions run: | @@ -91,12 +93,14 @@ jobs: git -P grep -F "${missing[@]}" | sed 's/^/::error::/' | head false } + if: always() - id: now run: | date +"mon=%-m"%n"day=%-d" >> $GITHUB_OUTPUT env: TZ: Asia/Tokyo + if: always() - id: deprecation run: | @@ -107,12 +111,14 @@ jobs: true fi continue-on-error: ${{ steps.now.outputs.mon < 12 }} + if: always() - name: Check if to generate documents id: rdoc run: | set -- $(sed 's/#.*//;/^rdoc /!d' gems/bundled_gems) { echo version=$2; echo ref=$4; } >> $GITHUB_OUTPUT + if: always() - name: Checkout rdoc uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -121,7 +127,7 @@ jobs: ref: ${{ steps.rdoc.outputs.ref }} path: .bundle/gems/rdoc-${{ steps.rdoc.outputs.version }} persist-credentials: false - if: ${{ steps.rdoc.outputs.ref != '' }} + if: ${{ always() && steps.rdoc.outputs.ref != '' }} - name: Generate rdoc scripts run: | @@ -133,11 +139,12 @@ jobs: bundle install --jobs 4 bundle exec rake generate working-directory: .bundle/gems/rdoc-${{ steps.rdoc.outputs.version }} - if: ${{ steps.rdoc.outputs.ref != '' }} + if: ${{ always() && steps.rdoc.outputs.ref != '' }} - name: Core docs coverage run: | make XRUBY=ruby RDOC_DEPENDS= RBCONFIG=update-rbconfig rdoc-coverage + if: always() - name: Generate docs id: docs @@ -146,19 +153,19 @@ jobs: echo htmlout=ruby-html-${GITHUB_SHA:0:10} >> $GITHUB_OUTPUT # Generate only when document commit/PR if: >- - ${{false + ${{always() && (false || contains(github.event.head_commit.message, '[ruby/rdoc]') || contains(github.event.head_commit.message, '[DOC]') || contains(github.event.pull_request.title, '[DOC]') || contains(github.event.pull_request.labels.*.name, 'Documentation') - }} + )}} - name: Upload docs uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: html name: ${{ steps.docs.outputs.htmlout }} - if: ${{ steps.docs.outcome == 'success' }} + if: ${{ always() && steps.docs.outcome == 'success' }} - uses: ./.github/actions/slack with: From 21ed8de7fdc0261a362d116f9abae23f803bce1e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 16:06:43 +0900 Subject: [PATCH 04/25] Add fallbacks for `__builtin_bswap128` and `__builtin_memcpy` --- configure.ac | 1 + include/ruby/internal/has/builtin.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/configure.ac b/configure.ac index 125dd098147a4a..06a4491f228726 100644 --- a/configure.ac +++ b/configure.ac @@ -2366,6 +2366,7 @@ RUBY_CHECK_BUILTIN_FUNC(__builtin_assume_aligned, [__builtin_assume_aligned((voi RUBY_CHECK_BUILTIN_FUNC(__builtin_bswap16, [__builtin_bswap16(0)]) RUBY_CHECK_BUILTIN_FUNC(__builtin_bswap32, [__builtin_bswap32(0)]) RUBY_CHECK_BUILTIN_FUNC(__builtin_bswap64, [__builtin_bswap64(0)]) +test "$ac_cv_sizeof___int128" = 0 || RUBY_CHECK_BUILTIN_FUNC(__builtin_bswap128, [__builtin_bswap128(0)]) RUBY_CHECK_BUILTIN_FUNC(__builtin_popcount, [__builtin_popcount(0)]) RUBY_CHECK_BUILTIN_FUNC(__builtin_popcountll, [__builtin_popcountll(0)]) RUBY_CHECK_BUILTIN_FUNC(__builtin_clz, [__builtin_clz(0)]) diff --git a/include/ruby/internal/has/builtin.h b/include/ruby/internal/has/builtin.h index 8e7fb173d8a3a6..e9ba6598ba1b46 100644 --- a/include/ruby/internal/has/builtin.h +++ b/include/ruby/internal/has/builtin.h @@ -57,6 +57,7 @@ #ifndef __OpenBSD__ # define RBIMPL_HAS_BUILTIN___builtin_bswap32 RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) # define RBIMPL_HAS_BUILTIN___builtin_bswap64 RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) +# define RBIMPL_HAS_BUILTIN___builtin_bswap128 RBIMPL_COMPILER_SINCE(GCC, 11, 1, 0) #endif # define RBIMPL_HAS_BUILTIN___builtin_clz RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) # define RBIMPL_HAS_BUILTIN___builtin_clzl RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) @@ -66,6 +67,7 @@ # define RBIMPL_HAS_BUILTIN___builtin_ctzl RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) # define RBIMPL_HAS_BUILTIN___builtin_ctzll RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) # define RBIMPL_HAS_BUILTIN___builtin_expect RBIMPL_COMPILER_SINCE(GCC, 3, 0, 0) +# define RBIMPL_HAS_BUILTIN___builtin_memcpy RBIMPL_COMPILER_SINCE(GCC, 3, 0, 0) // or older # define RBIMPL_HAS_BUILTIN___builtin_mul_overflow RBIMPL_COMPILER_SINCE(GCC, 5, 1, 0) # define RBIMPL_HAS_BUILTIN___builtin_mul_overflow_p RBIMPL_COMPILER_SINCE(GCC, 7, 0, 0) # define RBIMPL_HAS_BUILTIN___builtin_popcount RBIMPL_COMPILER_SINCE(GCC, 3, 6, 0) @@ -92,6 +94,7 @@ # define RBIMPL_HAS_BUILTIN___builtin_bswap16 HAVE_BUILTIN___BUILTIN_BSWAP16 # define RBIMPL_HAS_BUILTIN___builtin_bswap32 HAVE_BUILTIN___BUILTIN_BSWAP32 # define RBIMPL_HAS_BUILTIN___builtin_bswap64 HAVE_BUILTIN___BUILTIN_BSWAP64 +# define RBIMPL_HAS_BUILTIN___builtin_bswap128 HAVE_BUILTIN___BUILTIN_BSWAP128 # define RBIMPL_HAS_BUILTIN___builtin_clz HAVE_BUILTIN___BUILTIN_CLZ # define RBIMPL_HAS_BUILTIN___builtin_clzl HAVE_BUILTIN___BUILTIN_CLZL # define RBIMPL_HAS_BUILTIN___builtin_clzll HAVE_BUILTIN___BUILTIN_CLZLL From bb847847ae7f5c909d74e083fdfd6eb3082768e8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 28 Jul 2026 12:53:31 +0900 Subject: [PATCH 05/25] Enable long path names on Windows without the registry value Windows applies the `longPathAware` manifest setting only when `LongPathsEnabled` is set in the registry, so paths longer than 260 characters still fail on a stock machine. Set the undocumented `IsLongPathAwareProcess` bit in the PEB at startup instead, which is what `RtlAreLongPathsEnabled` reads and what Go's runtime does since 1.23. [Bug #18947] [Bug #18923] Co-Authored-By: Claude Opus 5 --- NEWS.md | 6 ++++++ test/ruby/test_dir.rb | 16 ++++++++++++++++ win32/win32.c | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/NEWS.md b/NEWS.md index ca413c616f6ece..6925455351b7c1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -244,6 +244,11 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det * SunOS 4 (Solaris, i.e. SunOS 5, is unaffected) * BSD/OS (BSDi) +* Windows 10 1703 or later no longer needs the `LongPathsEnabled` registry + value to use paths longer than 260 characters. Each path component is still + limited to 255 characters, and a child process still starts with the + `MAX_PATH` limited current directory. [[Bug #18947]] + ## Compatibility issues * `Kernel#at_exit` and `END {}` now raise `Ractor::IsolationError` when called @@ -327,6 +332,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT +[Bug #18947]: https://bugs.ruby-lang.org/issues/18947 [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #9779]: https://bugs.ruby-lang.org/issues/9779 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 diff --git a/test/ruby/test_dir.rb b/test/ruby/test_dir.rb index edb5210af16313..2ac8b1dc1386f0 100644 --- a/test/ruby/test_dir.rb +++ b/test/ruby/test_dir.rb @@ -590,6 +590,22 @@ def test_glob_legacy_short_name assert_empty(entries - Dir.glob("#{wild}/Common*", File::FNM_SHORTNAME), bug10819) end + def test_glob_long_path + bug18923 = '[Bug #18923]' + # the whole path is longer than MAX_PATH, while each component is + # within the 255 chars limit of NTFS + deep = File.join(@root, "a" * 200, "b" * 200) + begin + FileUtils.mkdir_p(deep) + rescue SystemCallError + omit "long path names are not available" + end + file = File.join(deep, "c.txt") + File.write(file, "") + assert_equal([file], Dir.glob(File.join(@root, "**", "*.txt")), bug18923) + assert_equal(["c.txt"], Dir.children(deep), bug18923) + end + def test_home_windows setup_envs(%w[HOME USERPROFILE HOMEDRIVE HOMEPATH]) diff --git a/win32/win32.c b/win32/win32.c index 4f2504781466b1..62a296da64fc20 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -843,6 +843,44 @@ static int w32_cmdvector(const WCHAR *, char ***, UINT, rb_encoding *); // // Initialization stuff // + +/* License: Ruby's */ +/* Enable long path names for this process, whatever the registry value + * HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled is. The + * loader sets the same PEB bit for longPathAware in the manifest, but only + * when that registry value is set, so the manifest alone is not enough on a + * stock machine. Undocumented, and taken from initLongPathSupport in Go's + * runtime , whose maintainers have + * proposed to drop it . */ +static void +init_long_path_support(void) +{ + /* PEB.BitField, and the IsLongPathAwareProcess bit in it */ + enum {peb_bit_field_offset = 3, is_long_path_aware_process = 0x80}; + typedef long (WINAPI version_func)(OSVERSIONINFOW *); + typedef void *(WINAPI peb_func)(void); + version_func *pRtlGetVersion; + peb_func *pRtlGetCurrentPeb; + OSVERSIONINFOW osvi; + BYTE *bit_field; + + pRtlGetVersion = (version_func *)get_proc_address("ntdll.dll", "RtlGetVersion", NULL); + if (!pRtlGetVersion) return; + memset(&osvi, 0, sizeof(osvi)); + osvi.dwOSVersionInfoSize = sizeof(osvi); + if (pRtlGetVersion(&osvi)) return; + + /* the PEB bit is honored since Windows 10 1703 (10.0.15063) */ + if (osvi.dwMajorVersion < 10) return; + if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && + osvi.dwBuildNumber < 15063) return; + + pRtlGetCurrentPeb = (peb_func *)get_proc_address("ntdll.dll", "RtlGetCurrentPeb", NULL); + if (!pRtlGetCurrentPeb) return; + bit_field = (BYTE *)pRtlGetCurrentPeb() + peb_bit_field_offset; + *bit_field |= is_long_path_aware_process; +} + /* License: Ruby's */ void rb_w32_sysinit(int *argc, char ***argv) @@ -854,6 +892,7 @@ rb_w32_sysinit(int *argc, char ***argv) SetErrorMode(SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX); get_version(); + init_long_path_support(); // // subvert cmd.exe's feeble attempt at command line parsing From b81e0d488838287ab2477d3288da4e6b4a134f9c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 6 Aug 2026 17:03:52 +0900 Subject: [PATCH 06/25] Set the long path bit for embedded interpreters too `rb_w32_sysinit` is documented as being for `ruby(1)`, so a program which embeds libruby may skip it and keep the 260 character limit while `ruby.exe` does not. Call the same initialization from `ruby_setup`, which every embedder reaches through `ruby_init`. Co-Authored-By: Claude Opus 5 --- NEWS.md | 7 ++++--- eval.c | 3 +++ internal.h | 5 +++++ win32/win32.c | 16 ++++++++++++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6925455351b7c1..c3599f0dbcbef4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -245,9 +245,10 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det * BSD/OS (BSDi) * Windows 10 1703 or later no longer needs the `LongPathsEnabled` registry - value to use paths longer than 260 characters. Each path component is still - limited to 255 characters, and a child process still starts with the - `MAX_PATH` limited current directory. [[Bug #18947]] + value to use paths longer than 260 characters. This applies to any process + running the interpreter, including a program which embeds libruby. Each path + component is still limited to 255 characters, and a child process still + starts with the `MAX_PATH` limited current directory. [[Bug #18947]] ## Compatibility issues diff --git a/eval.c b/eval.c index 8c61ceb6a7ee77..e23be408ada3c9 100644 --- a/eval.c +++ b/eval.c @@ -76,6 +76,9 @@ ruby_setup(void) */ #if defined(__linux__) && defined(PR_SET_THP_DISABLE) prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0); +#endif +#if defined(_WIN32) + rb_w32_init_long_paths(); #endif Init_BareVM(); rb_vm_encoded_insn_data_table_init(); diff --git a/internal.h b/internal.h index a08fab10dc2355..fe9ad3518f0bc4 100644 --- a/internal.h +++ b/internal.h @@ -64,6 +64,11 @@ /* internal/symbol.h */ #define rb_sym_intern_ascii_cstr(...) rb_nonexistent_symbol(__VA_ARGS__) +#ifdef _WIN32 +/* win32/win32.c */ +void rb_w32_init_long_paths(void); +#endif + /* MRI debug support */ diff --git a/win32/win32.c b/win32/win32.c index 62a296da64fc20..4bf16bc0187af2 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -851,19 +851,27 @@ static int w32_cmdvector(const WCHAR *, char ***, UINT, rb_encoding *); * when that registry value is set, so the manifest alone is not enough on a * stock machine. Undocumented, and taken from initLongPathSupport in Go's * runtime , whose maintainers have - * proposed to drop it . */ -static void -init_long_path_support(void) + * proposed to drop it . + * + * Called from rb_w32_sysinit, which runs before the VM exists, and from + * ruby_setup, so that a program which embeds libruby without calling + * ruby_sysinit gets it as well. */ +void +rb_w32_init_long_paths(void) { /* PEB.BitField, and the IsLongPathAwareProcess bit in it */ enum {peb_bit_field_offset = 3, is_long_path_aware_process = 0x80}; typedef long (WINAPI version_func)(OSVERSIONINFOW *); typedef void *(WINAPI peb_func)(void); + static int done = 0; version_func *pRtlGetVersion; peb_func *pRtlGetCurrentPeb; OSVERSIONINFOW osvi; BYTE *bit_field; + if (done) return; + done = 1; + pRtlGetVersion = (version_func *)get_proc_address("ntdll.dll", "RtlGetVersion", NULL); if (!pRtlGetVersion) return; memset(&osvi, 0, sizeof(osvi)); @@ -892,7 +900,7 @@ rb_w32_sysinit(int *argc, char ***argv) SetErrorMode(SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX); get_version(); - init_long_path_support(); + rb_w32_init_long_paths(); // // subvert cmd.exe's feeble attempt at command line parsing From 771de37f8b2fbe994b2c309149f0d4db6b703a36 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 19 Aug 2026 14:49:23 +0900 Subject: [PATCH 07/25] Set the long path bit only in ruby_setup `ruby_setup` runs for `ruby.exe` too, so calling this from `rb_w32_sysinit` as well was redundant. Nothing between the two touches a path long enough to care, including the wildcard expansion in `w32_cmdvector`, which does not expand a result over 260 characters either way. Co-Authored-By: Claude Opus 5 --- win32/win32.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/win32/win32.c b/win32/win32.c index 4bf16bc0187af2..d6d4acbb817ad2 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -853,9 +853,8 @@ static int w32_cmdvector(const WCHAR *, char ***, UINT, rb_encoding *); * runtime , whose maintainers have * proposed to drop it . * - * Called from rb_w32_sysinit, which runs before the VM exists, and from - * ruby_setup, so that a program which embeds libruby without calling - * ruby_sysinit gets it as well. */ + * Called from ruby_setup, not from rb_w32_sysinit, which a program that + * embeds libruby is not expected to call. */ void rb_w32_init_long_paths(void) { @@ -863,15 +862,11 @@ rb_w32_init_long_paths(void) enum {peb_bit_field_offset = 3, is_long_path_aware_process = 0x80}; typedef long (WINAPI version_func)(OSVERSIONINFOW *); typedef void *(WINAPI peb_func)(void); - static int done = 0; version_func *pRtlGetVersion; peb_func *pRtlGetCurrentPeb; OSVERSIONINFOW osvi; BYTE *bit_field; - if (done) return; - done = 1; - pRtlGetVersion = (version_func *)get_proc_address("ntdll.dll", "RtlGetVersion", NULL); if (!pRtlGetVersion) return; memset(&osvi, 0, sizeof(osvi)); @@ -900,7 +895,6 @@ rb_w32_sysinit(int *argc, char ***argv) SetErrorMode(SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX); get_version(); - rb_w32_init_long_paths(); // // subvert cmd.exe's feeble attempt at command line parsing From 0b233f3da759091fbc0537c94bdfe8ce415bfc66 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Aug 2026 16:40:28 +0900 Subject: [PATCH 08/25] Use ISEQ_BODY macro --- iseq.c | 4 +-- jit.c | 66 ++++++++++++++++++++++++------------------------- proc.c | 2 +- vm.c | 4 +-- vm_insnhelper.c | 6 ++--- vm_trace.c | 2 +- yjit.c | 26 +++++++++---------- zjit.c | 30 +++++++++++----------- 8 files changed, 70 insertions(+), 70 deletions(-) diff --git a/iseq.c b/iseq.c index 3d1fdcfa22f7b7..fb536da5f1a447 100644 --- a/iseq.c +++ b/iseq.c @@ -2951,9 +2951,9 @@ rb_iseq_disasm_recursive(const rb_iseq_t *iseq, VALUE indent) rb_str_modify_expand(str, header_minlen - l); memset(RSTRING_END(str), '=', header_minlen - l); } - if (iseq->body->builtin_attrs) { + if (ISEQ_BODY(iseq)->builtin_attrs) { #define disasm_builtin_attr(str, iseq, attr) \ - if (iseq->body->builtin_attrs & BUILTIN_ATTR_ ## attr) { \ + if (ISEQ_BODY(iseq)->builtin_attrs & BUILTIN_ATTR_ ## attr) { \ rb_str_cat2(str, " " #attr); \ } disasm_builtin_attr(str, iseq, LEAF); diff --git a/jit.c b/jit.c index fa672f1feef23a..d815fb909ccf2c 100644 --- a/jit.c +++ b/jit.c @@ -70,7 +70,7 @@ const shape_id_t rb_invalid_shape_id = INVALID_SHAPE_ID; unsigned int rb_iseq_encoded_size(const rb_iseq_t *iseq) { - return iseq->body->iseq_size; + return ISEQ_BODY(iseq)->iseq_size; } // Get the PC for a given index in an iseq @@ -78,8 +78,8 @@ VALUE * rb_iseq_pc_at_idx(const rb_iseq_t *iseq, uint32_t insn_idx) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - RUBY_ASSERT_ALWAYS(insn_idx < iseq->body->iseq_size); - VALUE *encoded = iseq->body->iseq_encoded; + RUBY_ASSERT_ALWAYS(insn_idx < ISEQ_BODY(iseq)->iseq_size); + VALUE *encoded = ISEQ_BODY(iseq)->iseq_encoded; VALUE *pc = &encoded[insn_idx]; return pc; } @@ -245,21 +245,21 @@ rb_optimized_call(VALUE recv, rb_execution_context_t *ec, int argc, VALUE *argv, unsigned int rb_jit_iseq_builtin_attrs(const rb_iseq_t *iseq) { - return iseq->body->builtin_attrs; + return ISEQ_BODY(iseq)->builtin_attrs; } // Relaxed memory ordering, but called by the JIT with VM lock and barrier. void rb_jit_iseq_mark_ep_escape_recorded(const rb_iseq_t *iseq) { - rbimpl_atomic_store(&iseq->body->jit_ep_escape_recorded, 1, RBIMPL_ATOMIC_RELAXED); + rbimpl_atomic_store(&ISEQ_BODY(iseq)->jit_ep_escape_recorded, 1, RBIMPL_ATOMIC_RELAXED); } // Whether an EP escape of this iseq has been reported to the enabled JIT. bool rb_jit_iseq_ep_escape_recorded_p(const rb_iseq_t *iseq) { - return rbimpl_atomic_load(&iseq->body->jit_ep_escape_recorded, RBIMPL_ATOMIC_RELAXED) != 0; + return rbimpl_atomic_load(&ISEQ_BODY(iseq)->jit_ep_escape_recorded, RBIMPL_ATOMIC_RELAXED) != 0; } int @@ -283,109 +283,109 @@ rb_get_def_iseq_ptr(rb_method_definition_t *def) const rb_iseq_t * rb_get_iseq_body_local_iseq(const rb_iseq_t *iseq) { - return iseq->body->local_iseq; + return ISEQ_BODY(iseq)->local_iseq; } const rb_iseq_t * rb_get_iseq_body_parent_iseq(const rb_iseq_t *iseq) { - return iseq->body->parent_iseq; + return ISEQ_BODY(iseq)->parent_iseq; } unsigned int rb_get_iseq_body_local_table_size(const rb_iseq_t *iseq) { - return iseq->body->local_table_size; + return ISEQ_BODY(iseq)->local_table_size; } VALUE * rb_get_iseq_body_iseq_encoded(const rb_iseq_t *iseq) { - return iseq->body->iseq_encoded; + return ISEQ_BODY(iseq)->iseq_encoded; } unsigned rb_get_iseq_body_stack_max(const rb_iseq_t *iseq) { - return iseq->body->stack_max; + return ISEQ_BODY(iseq)->stack_max; } enum rb_iseq_type rb_get_iseq_body_type(const rb_iseq_t *iseq) { - return iseq->body->type; + return ISEQ_BODY(iseq)->type; } bool rb_get_iseq_flags_has_lead(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_lead; + return ISEQ_BODY(iseq)->param.flags.has_lead; } bool rb_get_iseq_flags_has_opt(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_opt; + return ISEQ_BODY(iseq)->param.flags.has_opt; } bool rb_get_iseq_flags_has_kw(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_kw; + return ISEQ_BODY(iseq)->param.flags.has_kw; } bool rb_get_iseq_flags_has_post(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_post; + return ISEQ_BODY(iseq)->param.flags.has_post; } bool rb_get_iseq_flags_has_kwrest(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_kwrest; + return ISEQ_BODY(iseq)->param.flags.has_kwrest; } bool rb_get_iseq_flags_anon_kwrest(const rb_iseq_t *iseq) { - return iseq->body->param.flags.anon_kwrest; + return ISEQ_BODY(iseq)->param.flags.anon_kwrest; } bool rb_get_iseq_flags_has_rest(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_rest; + return ISEQ_BODY(iseq)->param.flags.has_rest; } bool rb_get_iseq_flags_ruby2_keywords(const rb_iseq_t *iseq) { - return iseq->body->param.flags.ruby2_keywords; + return ISEQ_BODY(iseq)->param.flags.ruby2_keywords; } bool rb_get_iseq_flags_has_block(const rb_iseq_t *iseq) { - return iseq->body->param.flags.has_block; + return ISEQ_BODY(iseq)->param.flags.has_block; } bool rb_get_iseq_flags_ambiguous_param0(const rb_iseq_t *iseq) { - return iseq->body->param.flags.ambiguous_param0; + return ISEQ_BODY(iseq)->param.flags.ambiguous_param0; } bool rb_get_iseq_flags_accepts_no_kwarg(const rb_iseq_t *iseq) { - return iseq->body->param.flags.accepts_no_kwarg; + return ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg; } bool rb_get_iseq_flags_forwardable(const rb_iseq_t *iseq) { - return iseq->body->param.flags.forwardable; + return ISEQ_BODY(iseq)->param.flags.forwardable; } // This is defined only as a named struct inside rb_iseq_constant_body. @@ -396,31 +396,31 @@ typedef struct rb_iseq_param_keyword rb_iseq_param_keyword_struct; const rb_iseq_param_keyword_struct * rb_get_iseq_body_param_keyword(const rb_iseq_t *iseq) { - return iseq->body->param.keyword; + return ISEQ_BODY(iseq)->param.keyword; } unsigned rb_get_iseq_body_param_size(const rb_iseq_t *iseq) { - return iseq->body->param.size; + return ISEQ_BODY(iseq)->param.size; } int rb_get_iseq_body_param_lead_num(const rb_iseq_t *iseq) { - return iseq->body->param.lead_num; + return ISEQ_BODY(iseq)->param.lead_num; } int rb_get_iseq_body_param_opt_num(const rb_iseq_t *iseq) { - return iseq->body->param.opt_num; + return ISEQ_BODY(iseq)->param.opt_num; } const VALUE * rb_get_iseq_body_param_opt_table(const rb_iseq_t *iseq) { - return iseq->body->param.opt_table; + return ISEQ_BODY(iseq)->param.opt_table; } struct rb_control_frame_struct * @@ -615,12 +615,12 @@ void rb_iseq_reset_jit_func(const rb_iseq_t *iseq) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - iseq->body->jit_entry = NULL; - iseq->body->jit_exception = NULL; + ISEQ_BODY(iseq)->jit_entry = NULL; + ISEQ_BODY(iseq)->jit_exception = NULL; // Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint, // we'd like to re-compile ISEQs that haven't been converted to trace_* insns. - iseq->body->jit_entry_calls = 0; - iseq->body->jit_exception_calls = 0; + ISEQ_BODY(iseq)->jit_entry_calls = 0; + ISEQ_BODY(iseq)->jit_exception_calls = 0; } // Callback data for rb_jit_for_each_iseq diff --git a/proc.c b/proc.c index 599b68ca7f2140..586d02139c4215 100644 --- a/proc.c +++ b/proc.c @@ -2230,7 +2230,7 @@ rb_hash_proc(st_index_t hash, VALUE prc) } } else { - hash = rb_st_hash_uint(hash, (st_index_t)proc->block.as.captured.code.iseq->body); + hash = rb_st_hash_uint(hash, (st_index_t)ISEQ_BODY(proc->block.as.captured.code.iseq)); } break; case block_type_ifunc: diff --git a/vm.c b/vm.c index 7fece4685228b6..8c6f6e8f07c1ef 100644 --- a/vm.c +++ b/vm.c @@ -483,7 +483,7 @@ rb_yjit_threshold_hit(const rb_iseq_t *iseq, uint64_t entry_calls) // Record the number of calls at the beginning of the interval if (entry_calls + YJIT_CALL_COUNT_INTERV == rb_yjit_call_threshold) { - iseq->body->yjit_calls_at_interv = yjit_total_entry_hits; + ISEQ_BODY(iseq)->yjit_calls_at_interv = yjit_total_entry_hits; } // Try to estimate the total time taken (total number of calls) to reach 20 calls to this ISEQ @@ -494,7 +494,7 @@ rb_yjit_threshold_hit(const rb_iseq_t *iseq, uint64_t entry_calls) return true; } - uint64_t num_calls = yjit_total_entry_hits - iseq->body->yjit_calls_at_interv; + uint64_t num_calls = yjit_total_entry_hits - ISEQ_BODY(iseq)->yjit_calls_at_interv; // Reject ISEQs that don't get called often enough if (num_calls > rb_yjit_cold_threshold) { diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 0e84feb298c20d..b5297bba22750e 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -3180,9 +3180,9 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling, VM_ASSERT(cc == calling->cc); if (vm_call_iseq_optimizable_p(ci, cc)) { - if ((iseq->body->builtin_attrs & BUILTIN_ATTR_SINGLE_NOARG_LEAF) && ruby_vm_c_events_enabled == 0) { - VM_ASSERT(iseq->body->builtin_attrs & BUILTIN_ATTR_LEAF); - vm_cc_bf_set(cc, (void *)iseq->body->iseq_encoded[1]); + if ((ISEQ_BODY(iseq)->builtin_attrs & BUILTIN_ATTR_SINGLE_NOARG_LEAF) && ruby_vm_c_events_enabled == 0) { + VM_ASSERT(ISEQ_BODY(iseq)->builtin_attrs & BUILTIN_ATTR_LEAF); + vm_cc_bf_set(cc, (void *)ISEQ_BODY(iseq)->iseq_encoded[1]); CC_SET_FASTPATH(cc, vm_call_single_noarg_leaf_builtin, true); } else { diff --git a/vm_trace.c b/vm_trace.c index 16ff2417e11b97..4fe5f0740d0c40 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -1411,7 +1411,7 @@ rb_tracepoint_enable_for_target(VALUE tpval, VALUE target, VALUE target_line) rb_hash_aset(tp->local_target_set, (VALUE)iseq, Qtrue); if ((tp->events & (RUBY_EVENT_CALL | RUBY_EVENT_RETURN)) && - iseq->body->builtin_attrs & BUILTIN_ATTR_SINGLE_NOARG_LEAF) { + ISEQ_BODY(iseq)->builtin_attrs & BUILTIN_ATTR_SINGLE_NOARG_LEAF) { rb_clear_bf_ccs(); } diff --git a/yjit.c b/yjit.c index e10c10f26d992b..823ae30cfd19e4 100644 --- a/yjit.c +++ b/yjit.c @@ -198,8 +198,8 @@ void * rb_iseq_get_yjit_payload(const rb_iseq_t *iseq) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - if (iseq->body) { - return iseq->body->yjit_payload; + if (ISEQ_BODY(iseq)) { + return ISEQ_BODY(iseq)->yjit_payload; } else { // Body is NULL when constructing the iseq. @@ -211,9 +211,9 @@ void rb_iseq_set_yjit_payload(const rb_iseq_t *iseq, void *payload) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - RUBY_ASSERT_ALWAYS(iseq->body); - RUBY_ASSERT_ALWAYS(NULL == iseq->body->yjit_payload); - iseq->body->yjit_payload = payload; + RUBY_ASSERT_ALWAYS(ISEQ_BODY(iseq)); + RUBY_ASSERT_ALWAYS(NULL == ISEQ_BODY(iseq)->yjit_payload); + ISEQ_BODY(iseq)->yjit_payload = payload; } // This is defined only as a named struct inside rb_iseq_constant_body. @@ -227,11 +227,11 @@ ID rb_get_symbol_id(VALUE namep); static bool invokebuiltin_delegate_leave_p(const rb_iseq_t *iseq) { - int insn1 = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[0]); - if ((int)iseq->body->iseq_size != insn_len(insn1) + insn_len(BIN(leave))) { + int insn1 = rb_vm_insn_addr2opcode((void *)ISEQ_BODY(iseq)->iseq_encoded[0]); + if ((int)ISEQ_BODY(iseq)->iseq_size != insn_len(insn1) + insn_len(BIN(leave))) { return false; } - int insn2 = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_len(insn1)]); + int insn2 = rb_vm_insn_addr2opcode((void *)ISEQ_BODY(iseq)->iseq_encoded[insn_len(insn1)]); return (insn1 == BIN(opt_invokebuiltin_delegate) || insn1 == BIN(opt_invokebuiltin_delegate_leave)) && insn2 == BIN(leave); } @@ -241,7 +241,7 @@ const struct rb_builtin_function * rb_yjit_builtin_function(const rb_iseq_t *iseq) { if (invokebuiltin_delegate_leave_p(iseq)) { - return (const struct rb_builtin_function *)iseq->body->iseq_encoded[1]; + return (const struct rb_builtin_function *)ISEQ_BODY(iseq)->iseq_encoded[1]; } else { return NULL; @@ -347,9 +347,9 @@ num_digits(int integer) char * rb_yjit_iseq_inspect(const rb_iseq_t *iseq) { - const char *label = RSTRING_PTR(iseq->body->location.label); + const char *label = RSTRING_PTR(ISEQ_BODY(iseq)->location.label); const char *path = RSTRING_PTR(rb_iseq_path(iseq)); - int lineno = iseq->body->location.code_location.beg_pos.lineno; + int lineno = ISEQ_BODY(iseq)->location.code_location.beg_pos.lineno; const size_t size = strlen(label) + strlen(path) + num_digits(lineno) + 3; char *buf = ZALLOC_N(char, size); @@ -392,10 +392,10 @@ rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit uintptr_t code_ptr = (uintptr_t)rb_yjit_iseq_gen_entry_point(iseq, ec, jit_exception); if (jit_exception) { - iseq->body->jit_exception = (rb_jit_func_t)code_ptr; + ISEQ_BODY(iseq)->jit_exception = (rb_jit_func_t)code_ptr; } else { - iseq->body->jit_entry = (rb_jit_func_t)code_ptr; + ISEQ_BODY(iseq)->jit_entry = (rb_jit_func_t)code_ptr; } } } diff --git a/zjit.c b/zjit.c index f915b4a8b5a525..2dcb20b55422c8 100644 --- a/zjit.c +++ b/zjit.c @@ -76,10 +76,10 @@ rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, ec, jit_exception); if (jit_exception) { - iseq->body->jit_exception = (rb_jit_func_t)code_ptr; + ISEQ_BODY(iseq)->jit_exception = (rb_jit_func_t)code_ptr; } else { - iseq->body->jit_entry = (rb_jit_func_t)code_ptr; + ISEQ_BODY(iseq)->jit_entry = (rb_jit_func_t)code_ptr; } } } @@ -94,11 +94,11 @@ rb_zjit_profile_enable(const rb_iseq_t *iseq) const void *const *insn_table = rb_vm_get_insns_address_table(); unsigned int insn_idx = 0; - while (insn_idx < iseq->body->iseq_size) { - int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]); + while (insn_idx < ISEQ_BODY(iseq)->iseq_size) { + int insn = rb_vm_insn_addr2opcode((void *)ISEQ_BODY(iseq)->iseq_encoded[insn_idx]); int zjit_insn = vm_bare_insn_to_zjit_insn(insn); if (insn != zjit_insn) { - iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[zjit_insn]; + ISEQ_BODY(iseq)->iseq_encoded[insn_idx] = (VALUE)insn_table[zjit_insn]; } insn_idx += insn_len(insn); } @@ -112,11 +112,11 @@ rb_zjit_profile_disable(const rb_iseq_t *iseq) const void *const *insn_table = rb_vm_get_insns_address_table(); unsigned int insn_idx = 0; - while (insn_idx < iseq->body->iseq_size) { - int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]); + while (insn_idx < ISEQ_BODY(iseq)->iseq_size) { + int insn = rb_vm_insn_addr2opcode((void *)ISEQ_BODY(iseq)->iseq_encoded[insn_idx]); int bare_insn = vm_zjit_insn_to_bare_insn(insn); if (insn != bare_insn) { - iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[bare_insn]; + ISEQ_BODY(iseq)->iseq_encoded[insn_idx] = (VALUE)insn_table[bare_insn]; } insn_idx += insn_len(insn); } @@ -134,11 +134,11 @@ void rb_zjit_iseq_insn_set(const rb_iseq_t *iseq, unsigned int insn_idx, enum ruby_vminsn_type bare_insn) { #if RUBY_DEBUG - int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]); + int insn = rb_vm_insn_addr2opcode((void *)ISEQ_BODY(iseq)->iseq_encoded[insn_idx]); RUBY_ASSERT(vm_zjit_insn_to_bare_insn(insn) == (int)bare_insn); #endif const void *const *insn_table = rb_vm_get_insns_address_table(); - iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[bare_insn]; + ISEQ_BODY(iseq)->iseq_encoded[insn_idx] = (VALUE)insn_table[bare_insn]; } // Get profiling information for ISEQ @@ -146,8 +146,8 @@ void * rb_iseq_get_zjit_payload(const rb_iseq_t *iseq) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - if (iseq->body) { - return iseq->body->zjit_payload; + if (ISEQ_BODY(iseq)) { + return ISEQ_BODY(iseq)->zjit_payload; } else { // Body is NULL when constructing the iseq. @@ -160,9 +160,9 @@ void rb_iseq_set_zjit_payload(const rb_iseq_t *iseq, void *payload) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - RUBY_ASSERT_ALWAYS(iseq->body); - RUBY_ASSERT_ALWAYS(NULL == iseq->body->zjit_payload); - iseq->body->zjit_payload = payload; + RUBY_ASSERT_ALWAYS(ISEQ_BODY(iseq)); + RUBY_ASSERT_ALWAYS(NULL == ISEQ_BODY(iseq)->zjit_payload); + ISEQ_BODY(iseq)->zjit_payload = payload; } void From b30732221bb297cd2fc2544a81b7764b47a50cff Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 19:09:25 +0900 Subject: [PATCH 09/25] Check dependencies in check_misc.yml The current `make check-depends` command completes in about 10 seconds, there is little need to run it in a separate workflow. --- .github/workflows/check_dependencies.yml | 42 ------------------------ .github/workflows/check_misc.yml | 3 ++ 2 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/check_dependencies.yml diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml deleted file mode 100644 index 3e8c355ffdffe1..00000000000000 --- a/.github/workflows/check_dependencies.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Check Dependencies -on: - push: - paths-ignore: - - 'doc/**' - - '**/man/*' - - '**.md' - - '**.rdoc' - - '**/.document' - - '.*.yml' - pull_request: - merge_group: - -concurrency: - group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} - cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} - -permissions: - contents: read - -jobs: - update-deps: - name: Dependency checks (ubuntu-latest) - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 - with: - ruby-version: '3.1' - bundler: none - - - run: ruby tool/mkdepend.rb --scope=all --sources --check - - - uses: ./.github/actions/slack - with: - label: Dependencies need to update - SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() }} diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index af677aa247e974..601363d8304ade 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -113,6 +113,9 @@ jobs: continue-on-error: ${{ steps.now.outputs.mon < 12 }} if: always() + - run: make check-depends + if: always() + - name: Check if to generate documents id: rdoc run: | From 319849b57e4c4ac67e874b41e8115286f30b8664 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 15 Aug 2026 01:19:18 +0900 Subject: [PATCH 10/25] [ruby/openssl] pkey: avoid using {DH,DSA,RSA}#public_key in docs and tests As documented, the #public_key methods on OpenSSL::PKey::{DH,DSA,RSA} are obsolete, as their behavior is inconsistent and better alternatives are available. Remove unnecessary uses from RDoc comments for other methods. Also add dedicated tests for these methods to confirm the current behavior. https://github.com/ruby/openssl/commit/13d5616d17 --- ext/openssl/ossl.c | 6 +++--- ext/openssl/ossl_ns_spki.c | 2 +- ext/openssl/ossl_pkey.c | 4 ++-- ext/openssl/ossl_x509cert.c | 4 ++-- test/openssl/test_ns_spki.rb | 12 ++++++------ test/openssl/test_pkey_dsa.rb | 24 ++++++++++++++++++------ test/openssl/test_pkey_rsa.rb | 27 +++++++++++++++++---------- 7 files changed, 49 insertions(+), 30 deletions(-) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 6438d96fdf9519..d14265c09329c6 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -754,7 +754,7 @@ ossl_crypto_fixed_length_secure_compare(VALUE dummy, VALUE str1, VALUE str2) * cert.not_before = Time.now * cert.not_after = Time.now + 3600 * - * cert.public_key = key.public_key + * cert.public_key = key * cert.subject = name * * === Certificate Extensions @@ -836,7 +836,7 @@ ossl_crypto_fixed_length_secure_compare(VALUE dummy, VALUE str1, VALUE str2) * ca_cert.not_before = Time.now * ca_cert.not_after = Time.now + 86400 * - * ca_cert.public_key = ca_key.public_key + * ca_cert.public_key = ca_key * ca_cert.subject = ca_name * ca_cert.issuer = ca_name * @@ -878,7 +878,7 @@ ossl_crypto_fixed_length_secure_compare(VALUE dummy, VALUE str1, VALUE str2) * csr = OpenSSL::X509::Request.new * csr.version = 0 * csr.subject = name - * csr.public_key = key.public_key + * csr.public_key = key * csr.sign key, OpenSSL::Digest.new('SHA1') * * A CSR is saved to disk and sent to the CA for signing. diff --git a/ext/openssl/ossl_ns_spki.c b/ext/openssl/ossl_ns_spki.c index eb9933948ff748..eb4fce59611400 100644 --- a/ext/openssl/ossl_ns_spki.c +++ b/ext/openssl/ossl_ns_spki.c @@ -336,7 +336,7 @@ ossl_spki_verify(VALUE self, VALUE key) * key = OpenSSL::PKey::RSA.new 2048 * spki = OpenSSL::Netscape::SPKI.new * spki.challenge = "RandomChallenge" - * spki.public_key = key.public_key + * spki.public_key = key * spki.sign(key, OpenSSL::Digest.new('SHA256')) * #send a request containing this to a server generating a certificate * === Verifying an SPKI request diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c index 5cbc0d195f4ccb..7528d2d2638f34 100644 --- a/ext/openssl/ossl_pkey.c +++ b/ext/openssl/ossl_pkey.c @@ -1192,7 +1192,7 @@ ossl_pkey_compare(VALUE self, VALUE other) * signature = pkey.sign("SHA256", data, signopts) * * # Creates a copy of the RSA key pkey, but without the private components - * pub_key = pkey.public_key + * pub_key = OpenSSL::PKey.read(pkey.public_to_der) * puts pub_key.verify("SHA256", signature, data, signopts) # => true */ static VALUE @@ -1350,7 +1350,7 @@ ossl_pkey_verify(int argc, VALUE *argv, VALUE self) * signature = pkey.sign_raw("SHA256", hash, signopts) * * # Creates a copy of the RSA key pkey, but without the private components - * pub_key = pkey.public_key + * pub_key = OpenSSL::PKey.read(pkey.public_to_der) * puts pub_key.verify_raw("SHA256", signature, hash, signopts) # => true */ static VALUE diff --git a/ext/openssl/ossl_x509cert.c b/ext/openssl/ossl_x509cert.c index b403871949541b..f0a3a81578b321 100644 --- a/ext/openssl/ossl_x509cert.c +++ b/ext/openssl/ossl_x509cert.c @@ -922,7 +922,7 @@ Init_ossl_x509cert(void) * root_ca.serial = 1 * root_ca.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-lang/CN=Ruby CA" * root_ca.issuer = root_ca.subject # root CA's are "self-signed" - * root_ca.public_key = root_key.public_key + * root_ca.public_key = root_key * root_ca.not_before = Time.now * root_ca.not_after = root_ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity * ef = OpenSSL::X509::ExtensionFactory.new @@ -943,7 +943,7 @@ Init_ossl_x509cert(void) * cert.serial = 2 * cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-lang/CN=Ruby certificate" * cert.issuer = root_ca.subject # root CA is the issuer - * cert.public_key = key.public_key + * cert.public_key = key * cert.not_before = Time.now * cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60 # 1 years validity * ef = OpenSSL::X509::ExtensionFactory.new diff --git a/test/openssl/test_ns_spki.rb b/test/openssl/test_ns_spki.rb index 04844292897a08..2607a8c0b3a77d 100644 --- a/test/openssl/test_ns_spki.rb +++ b/test/openssl/test_ns_spki.rb @@ -21,17 +21,17 @@ def test_build_data key2 = Fixtures.pkey("rsa-2") spki = OpenSSL::Netscape::SPKI.new spki.challenge = "RandomString" - spki.public_key = key1.public_key + spki.public_key = key1 spki.sign(key1, OpenSSL::Digest.new('SHA256')) - assert(spki.verify(spki.public_key)) - assert(spki.verify(key1.public_key)) - assert(!spki.verify(key2.public_key)) + assert_true(spki.verify(spki.public_key)) + assert_true(spki.verify(OpenSSL::PKey.read(key1.public_to_der))) + assert_false(spki.verify(OpenSSL::PKey.read(key2.public_to_der))) der = spki.to_der spki = OpenSSL::Netscape::SPKI.new(der) assert_equal("RandomString", spki.challenge) - assert_equal(key1.public_key.to_der, spki.public_key.to_der) - assert(spki.verify(spki.public_key)) + assert_equal(key1.public_to_der, spki.public_key.public_to_der) + assert_true(spki.verify(spki.public_key)) assert_not_nil(spki.to_text) end diff --git a/test/openssl/test_pkey_dsa.rb b/test/openssl/test_pkey_dsa.rb index 1ec0bf0b4d4bf8..fb692854a1f2d5 100644 --- a/test/openssl/test_pkey_dsa.rb +++ b/test/openssl/test_pkey_dsa.rb @@ -14,16 +14,15 @@ def test_private assert_equal true, key.private? key2 = OpenSSL::PKey::DSA.new(key.to_der) assert_equal true, key2.private? - key3 = key.public_key + key3 = OpenSSL::PKey::DSA.new(key.public_to_der) assert_equal false, key3.private? - key4 = OpenSSL::PKey::DSA.new(key3.to_der) - assert_equal false, key4.private? end def test_new - key = OpenSSL::PKey::DSA.new(2048) - pem = key.public_key.to_pem - OpenSSL::PKey::DSA.new pem + key = OpenSSL::PKey::DSA.new(1024) + assert_predicate(key, :private?) + assert_equal(1024, key.p.num_bits) + assert_equal(160, key.q.num_bits) end def test_new_break @@ -233,6 +232,19 @@ def test_params assert_nil(pubkey.params["priv_key"]) end + def test_public_key + key = Fixtures.pkey("dsa2048") + pub = key.public_key + assert_not_predicate(pub, :private?) + assert_predicate(pub, :public?) + assert_equal(key.p, pub.p) + assert_equal(key.q, pub.q) + assert_equal(key.g, pub.g) + assert_equal(key.pub_key, pub.pub_key) + assert_nil(pub.priv_key) + assert_equal(key.public_to_der, pub.to_der) + end + def test_dup key = Fixtures.pkey("dsa2048") key2 = key.dup diff --git a/test/openssl/test_pkey_rsa.rb b/test/openssl/test_pkey_rsa.rb index d56d19bd9722ee..31c3c52b344eca 100644 --- a/test/openssl/test_pkey_rsa.rb +++ b/test/openssl/test_pkey_rsa.rb @@ -20,13 +20,9 @@ def test_private key2 = OpenSSL::PKey::RSA.new(key.to_der) assert_true(key2.private?) - # public key - key3 = key.public_key - assert_false(key3.private?) - # Generated by public key DER - key4 = OpenSSL::PKey::RSA.new(key3.to_der) - assert_false(key4.private?) + key3 = OpenSSL::PKey::RSA.new(key.public_to_der) + assert_false(key3.private?) if !openssl?(3, 0, 0) # Generated by RSA#set_key @@ -290,11 +286,11 @@ def test_export # key has only n, e and d key.set_key(orig.n, orig.e, orig.d) - assert_equal orig.public_key.export, key.export + assert_equal pub.export, key.export # key has only n, e, d, p and q key.set_factors(orig.p, orig.q) - assert_equal orig.public_key.export, key.export + assert_equal pub.export, key.export # key has n, e, d, p, q, dmp1, dmq1 and iqmp key.set_crt_params(orig.dmp1, orig.dmq1, orig.iqmp) @@ -315,11 +311,11 @@ def test_to_der # key has only n, e and d key.set_key(orig.n, orig.e, orig.d) - assert_equal orig.public_key.to_der, key.to_der + assert_equal pub.to_der, key.to_der # key has only n, e, d, p and q key.set_factors(orig.p, orig.q) - assert_equal orig.public_key.to_der, key.to_der + assert_equal pub.to_der, key.to_der # key has n, e, d, p, q, dmp1, dmq1 and iqmp key.set_crt_params(orig.dmp1, orig.dmq1, orig.iqmp) @@ -548,6 +544,17 @@ def test_get_param assert_equal(key.iqmp, key.get_param("rsa-coefficient1")) end + def test_public_key + key = Fixtures.pkey("rsa-1") + pub = key.public_key + assert_not_predicate(pub, :private?) + assert_predicate(pub, :public?) + assert_equal(key.n, pub.n) + assert_equal(key.e, pub.e) + assert_nil(pub.d) + assert_equal(key.public_to_der, pub.to_der) + end + def test_dup key = Fixtures.pkey("rsa-1") key2 = key.dup From d9c8673dea43825640ddfea29962a2100d034049 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Aug 2026 17:35:21 +0900 Subject: [PATCH 11/25] Add comment for local_table --- iseq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iseq.c b/iseq.c index fb536da5f1a447..b428ce102b59bb 100644 --- a/iseq.c +++ b/iseq.c @@ -543,7 +543,7 @@ rb_iseq_memsize(const rb_iseq_t *iseq) size += sizeof(struct rb_iseq_constant_body); size += body->iseq_size * sizeof(VALUE); size += body->insns_info.size * (sizeof(struct iseq_insn_info_entry) + sizeof(unsigned int)); - size += body->local_table_size * sizeof(ID); + size += body->local_table_size * sizeof(ID); // body->local_table size += ISEQ_MBITS_BUFLEN(body->iseq_size) * ISEQ_MBITS_SIZE; if (body->catch_table) { size += iseq_catch_table_bytes(body->catch_table->size); From 08ccbae4bd280cf36af13c19fc87c625a0321261 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Aug 2026 17:35:43 +0900 Subject: [PATCH 12/25] Add missing lvar_states in rb_iseq_memsize --- iseq.c | 1 + 1 file changed, 1 insertion(+) diff --git a/iseq.c b/iseq.c index b428ce102b59bb..a09f2b0983163f 100644 --- a/iseq.c +++ b/iseq.c @@ -544,6 +544,7 @@ rb_iseq_memsize(const rb_iseq_t *iseq) size += body->iseq_size * sizeof(VALUE); size += body->insns_info.size * (sizeof(struct iseq_insn_info_entry) + sizeof(unsigned int)); size += body->local_table_size * sizeof(ID); // body->local_table + if (body->lvar_states) size += body->local_table_size * sizeof(enum lvar_state); size += ISEQ_MBITS_BUFLEN(body->iseq_size) * ISEQ_MBITS_SIZE; if (body->catch_table) { size += iseq_catch_table_bytes(body->catch_table->size); From b61e03cc8cd4791df72c260ed4108019f5ac758d Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Aug 2026 17:41:45 +0900 Subject: [PATCH 13/25] Add missing outer_variables in rb_iseq_memsize --- iseq.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iseq.c b/iseq.c index a09f2b0983163f..5a12633dd78b1c 100644 --- a/iseq.c +++ b/iseq.c @@ -552,6 +552,8 @@ rb_iseq_memsize(const rb_iseq_t *iseq) size += (body->param.opt_num + 1) * sizeof(VALUE); size += param_keyword_size(body->param.keyword); + if (body->outer_variables) size += rb_id_table_memsize(body->outer_variables); + /* body->is_entries */ size += ISEQ_IS_SIZE(body) * sizeof(union iseq_inline_storage_entry); From f7f375748850a908fb06dc185c7e62afad89cd9c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 19 Aug 2026 21:34:40 +0900 Subject: [PATCH 14/25] Compare full version info of rustc to avoid E0514 --- defs/jit.mk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/defs/jit.mk b/defs/jit.mk index 2c1e819684939b..3ebf3858979407 100644 --- a/defs/jit.mk +++ b/defs/jit.mk @@ -81,12 +81,12 @@ rustc-version-check: target/.rustc-version target/.rustc-version: PHONY $(eval prev_version := $(if $(wildcard $@),$(shell cat $@))) - $(eval curr_version := $(shell $(RUSTC) -V | cut -d' ' -f2)) + $(eval curr_version := $(shell $(RUSTC) -V)) $(eval clean := $(filter-out $(prev_version),$(curr_version))) - $(if $(clean),$(ECHO) Cleaning $(@D) for rustc $(curr_version)) + $(if $(clean),$(ECHO) "Cleaning $(@D) for $(curr_version)") $(if $(clean),$(Q)$(RMALL) $(@D)) $(if $(clean),$(Q)$(MAKEDIRS) $(@D)) - $(if $(clean),$(Q)echo $(curr_version) > $@) + $(if $(clean),$(Q)echo "$(curr_version)" > $@) # For Darwin only: a list of symbols that we want the glommed Rust static lib to export. # Unfortunately, using wildcard like '_rb_*' with -exported-symbol does not work, at least From bd19faee64efeac6b8133e354659893a68f7c066 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 20:59:06 +0100 Subject: [PATCH 15/25] MMTK: Reserve slot room for one embedded field The shape_id capacity bits come from the slot size, and the shape verifier asserts that the capacity is more than zero. The default GC satisfies this because its smallest slot is 32 bytes. MMTk allocated exact-fit slots, so an empty imemo_fields got a 16-byte slot and zero capacity bits. Boot then failed at the first class freeze: shape.c:1346: Assertion Failed: rb_shape_verify_consistency:shape_id_capacity > 0 --- gc/mmtk/mmtk.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 935918a3358531..ec8ab173bedaae 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -704,6 +704,15 @@ rb_mmtk_align_obj_size(size_t object_size) return (object_size + MMTk_MIN_OBJ_ALIGN - 1) & ~((size_t)MMTk_MIN_OBJ_ALIGN - 1); } +static inline size_t +rb_mmtk_total_obj_size(size_t payload_size) +{ + if (payload_size < sizeof(struct RBasic) + sizeof(VALUE)) { + payload_size = sizeof(struct RBasic) + sizeof(VALUE); + } + return rb_mmtk_align_obj_size(payload_size + sizeof(VALUE)); +} + bool rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE flags, VALUE klass, struct rb_gc_zjit_fastpath *fastpath) @@ -711,7 +720,7 @@ rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE fl #if USE_ZJIT struct objspace *objspace = objspace_ptr; - size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE)); + size_t total_size = rb_mmtk_total_obj_size(alloc_size); size_t object_size = total_size - sizeof(VALUE); size_t value_size_shift = sizeof(VALUE) == 8 ? 3 : 2; @@ -1026,7 +1035,7 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags } // Layout: [hidden size header (sizeof(VALUE))][payload (alloc_size)] - size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE)); + size_t total_size = rb_mmtk_total_obj_size(alloc_size); size_t object_size = total_size - sizeof(VALUE); MMTk_AllocationSemantics semantics = total_size > objspace->max_non_los_default_alloc_bytes ? MMTK_ALLOCATION_SEMANTICS_LOS @@ -1084,7 +1093,7 @@ rb_gc_impl_size_slot_size(void *objspace_ptr, size_t size) rb_bug("rb_gc_impl_size_slot_size: size too large (size=%"PRIuSIZE")", size); } - return rb_mmtk_align_obj_size(size + sizeof(VALUE)) - sizeof(VALUE); + return rb_mmtk_total_obj_size(size) - sizeof(VALUE); } bool From 35b651600e67fc33a96eb51be067d0abbff40303 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 21:01:39 +0100 Subject: [PATCH 16/25] Allow EC-less threads in rb_vm_ep_in_heap_p MMTk marks objects on GC worker threads. Those threads have no execution context, so GET_EC() inside this VM_CHECK_MODE helper failed an assertion when imemo_env marking checked VM_ENV_ESCAPED_P: vm_core.h:2243: Assertion Failed: rb_current_execution_context:!expect_ec || ec != NULL --- vm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vm.c b/vm.c index 8c6f6e8f07c1ef..ff1ec24b93138e 100644 --- a/vm.c +++ b/vm.c @@ -276,8 +276,8 @@ vm_ep_in_heap_p_(const rb_execution_context_t *ec, const VALUE *ep) int rb_vm_ep_in_heap_p(const VALUE *ep) { - const rb_execution_context_t *ec = GET_EC(); - if (ec->vm_stack == NULL) return TRUE; + const rb_execution_context_t *ec = rb_current_execution_context(false); + if (ec == NULL || ec->vm_stack == NULL) return TRUE; return vm_ep_in_heap_p_(ec, ep); } #endif From 83187c9c25df213444f6859fb3e368a4e054a1d9 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 21:01:47 +0100 Subject: [PATCH 17/25] MMTK: report shref edges as always protected The shareable constraint checker treats a false result from rb_gc_impl_shref_marked_p as a missing shref record and raises a bug. Shref records keep an unshareable object alive past the local GC of its owner. MMTk only has a single objspace, so doesn't run local GC. So we can just assume everything is allowed to stay alive. --- gc/mmtk/mmtk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index ec8ab173bedaae..7895a098781d0f 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -1849,7 +1849,7 @@ bool rb_gc_impl_shref_marked_p(void *objspace_ptr, VALUE obj) { /* With a single objspace there is no cross-objspace pinning to track. */ - return false; + return true; } size_t From c3e04e3bb67bdf8bd78a436f9d05b1dea8edda6f Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 21:21:38 +0100 Subject: [PATCH 18/25] MMTK: Allow debug cc checks on EC-less GC threads MMTk marks call caches on GC worker threads. Those threads have no execution context, so two debug helpers failed: * vm_cc_check_cme took the VM lock, and the lock records the current ractor. Skip the lock when the thread has no execution context. We can assume this is safe because the only threads with no EC are MMTk GC worker threads, and they're only working when the mutator is stopped. * RUBY_ASSERT_vm_locking and its variants assume the lock owner is the current ractor, but this is meaningless in a MMTk GC worker thread, so let's just pass the assertions. --- vm_callinfo.h | 17 ++++++++++++++--- vm_sync.c | 12 +++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/vm_callinfo.h b/vm_callinfo.h index 5c7cc6757e9a03..631021d49845b8 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -647,13 +647,24 @@ vm_ccs_p(const struct rb_class_cc_entries *ccs) return ccs->debug_sig == ~(VALUE)ccs; } +static inline bool +vm_cc_check_cme_unlocked(const struct rb_callcache *cc, const rb_callable_method_entry_t *cme) +{ + return vm_cc_cme(cc) == cme || + (cme->def->iseq_overload && vm_cc_cme(cc) == rb_vm_lookup_overloaded_cme(cme)); +} + static inline bool vm_cc_check_cme(const struct rb_callcache *cc, const rb_callable_method_entry_t *cme) { bool valid; - RB_VM_LOCKING_NO_BARRIER() { - valid = vm_cc_cme(cc) == cme || - (cme->def->iseq_overload && vm_cc_cme(cc) == rb_vm_lookup_overloaded_cme(cme)); + if (rb_current_execution_context(false) == NULL) { + valid = vm_cc_check_cme_unlocked(cc, cme); + } + else { + RB_VM_LOCKING_NO_BARRIER() { + valid = vm_cc_check_cme_unlocked(cc, cme); + } } if (valid) { return true; diff --git a/vm_sync.c b/vm_sync.c index d6eb47dfe725cf..6b988c1596f3a3 100644 --- a/vm_sync.c +++ b/vm_sync.c @@ -17,10 +17,16 @@ vm_locked(rb_vm_t *vm) } #if RUBY_DEBUG > 0 +static bool +vm_lock_assertable_p(void) +{ + return rb_current_execution_context(false) != NULL; +} + void RUBY_ASSERT_vm_locking(void) { - if (rb_multi_ractor_p()) { + if (vm_lock_assertable_p() && rb_multi_ractor_p()) { rb_vm_t *vm = GET_VM(); VM_ASSERT(vm_locked(vm)); } @@ -29,7 +35,7 @@ RUBY_ASSERT_vm_locking(void) void RUBY_ASSERT_vm_locking_with_barrier(void) { - if (rb_multi_ractor_p()) { + if (vm_lock_assertable_p() && rb_multi_ractor_p()) { rb_vm_t *vm = GET_VM(); VM_ASSERT(vm_locked(vm)); @@ -43,7 +49,7 @@ RUBY_ASSERT_vm_locking_with_barrier(void) void RUBY_ASSERT_vm_unlocking(void) { - if (rb_multi_ractor_p()) { + if (vm_lock_assertable_p() && rb_multi_ractor_p()) { rb_vm_t *vm = GET_VM(); VM_ASSERT(!vm_locked(vm)); } From d40f16fbe216048274275e07ca2f3d43cd796161 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 21:30:22 +0100 Subject: [PATCH 19/25] Ractor: Assert live status in root scan callers only ractor_mark_unshareable_parts asserts that its Ractor is not terminated. This is only relevant in the multi-objspace root scan, where a terminated Ractor has left the set and zombie_objspaces keeps its possessions alive. With a single objspace the wrapper mark function also calls this helper, and a terminated Ractor wrapper stays reachable on the heap. MMTk hits this assertion: ractor.c:259: Assertion Failed: ractor_mark_unshareable_parts:!rb_ractor_status_p(r, ractor_terminated) --- ractor.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ractor.c b/ractor.c index 3969e7d071a9f6..c6b6c32b8242b6 100644 --- a/ractor.c +++ b/ractor.c @@ -252,11 +252,7 @@ ractor_mark_unshareable_parts(rb_ractor_t *r) // mark the received messages (the structures the owner mutates guard themselves) ractor_sync_mark(r); - /* Structures the owner mutates while running follow. Only the root scan calls - * this: a local GC for itself, a global GC for the whole set under the barrier. A - * terminated Ractor has left the set; zombie_objspaces covers it instead. */ - VM_ASSERT(r == rb_current_ractor_raw(false) || rb_gc_during_global_gc_p()); - VM_ASSERT(!rb_ractor_status_p(r, ractor_terminated)); + /* Structures the owner mutates while running follow. */ rb_hook_list_mark(&r->pub.hooks); if (r->pub.targeted_hooks.num_entries) { @@ -334,6 +330,11 @@ rb_ractor_mark_local_roots(rb_ractor_t *r) rb_gc_mark(r->loc); rb_gc_mark(r->name); + /* Only the root scan calls this: a local GC for itself, a global GC for the whole + * set under the barrier. A terminated Ractor has left the set; zombie_objspaces + * covers it instead. */ + VM_ASSERT(r == rb_current_ractor_raw(false) || rb_gc_during_global_gc_p()); + VM_ASSERT(!rb_ractor_status_p(r, ractor_terminated)); ractor_mark_unshareable_parts(r); /* This Ractor's rb_gc_register_mark_object pins, treated conservatively: a local GC From 38dbe924f502011747f6a7880c3a7f44f569298b Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 13 Aug 2026 21:36:48 +0100 Subject: [PATCH 20/25] Don't write a shape into a dead weak table key The generic fields weak table callback reset the shape of a key on ST_DELETE so that the later free path skips a second table removal. A key can already be T_NONE here. MMTk debug builds zero the whole slot of a dead obj_free candidate in rb_mmtk_call_obj_free, and that work packet is unordered with this table walk: both run in the VMRefClosure bucket. When the zero wins the race, the shape write failed verification with a layout mismatch. --- gc.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gc.c b/gc.c index 4db541967aeb18..9e7410a404e264 100644 --- a/gc.c +++ b/gc.c @@ -4722,7 +4722,9 @@ vm_weak_table_gen_fields_foreach(st_data_t key, st_data_t value, st_data_t data) // set the shape on it so that the GC finalizer won't try to remove // it again. A "root shape" indicates to the GC that this object // has no fields on it, hence it won't be in the gen fields table. - RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); + if (BUILTIN_TYPE((VALUE)key) != T_NONE) { + RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); + } return ST_DELETE; case ST_REPLACE: { From 5d3ac10186f18a2007991d0840e83d983b37969d Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Fri, 14 Aug 2026 11:34:02 +0100 Subject: [PATCH 21/25] Allow shapes to carry zero embedded capacity rb_shape_verify_consistency asserted that every checked object has room for at least one embedded field. The default GC always satisfies this because its smallest slot is 32 bytes so this assertion never fires. MMTk gives an empty imemo_fields a 16-byte slot, so its shape carries zero capacity bits, and boot failed on the first class freeze. Instead of forcing MMTk to have a minimum object size, we should just allow shapes to support a capacity of 0. --- gc/mmtk/mmtk.c | 15 +++------------ shape.c | 1 - 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 7895a098781d0f..a7aec6e68338e7 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -704,15 +704,6 @@ rb_mmtk_align_obj_size(size_t object_size) return (object_size + MMTk_MIN_OBJ_ALIGN - 1) & ~((size_t)MMTk_MIN_OBJ_ALIGN - 1); } -static inline size_t -rb_mmtk_total_obj_size(size_t payload_size) -{ - if (payload_size < sizeof(struct RBasic) + sizeof(VALUE)) { - payload_size = sizeof(struct RBasic) + sizeof(VALUE); - } - return rb_mmtk_align_obj_size(payload_size + sizeof(VALUE)); -} - bool rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE flags, VALUE klass, struct rb_gc_zjit_fastpath *fastpath) @@ -720,7 +711,7 @@ rb_gc_impl_zjit_new_obj_fastpath(void *objspace_ptr, size_t alloc_size, VALUE fl #if USE_ZJIT struct objspace *objspace = objspace_ptr; - size_t total_size = rb_mmtk_total_obj_size(alloc_size); + size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE)); size_t object_size = total_size - sizeof(VALUE); size_t value_size_shift = sizeof(VALUE) == 8 ? 3 : 2; @@ -1035,7 +1026,7 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags } // Layout: [hidden size header (sizeof(VALUE))][payload (alloc_size)] - size_t total_size = rb_mmtk_total_obj_size(alloc_size); + size_t total_size = rb_mmtk_align_obj_size(alloc_size + sizeof(VALUE)); size_t object_size = total_size - sizeof(VALUE); MMTk_AllocationSemantics semantics = total_size > objspace->max_non_los_default_alloc_bytes ? MMTK_ALLOCATION_SEMANTICS_LOS @@ -1093,7 +1084,7 @@ rb_gc_impl_size_slot_size(void *objspace_ptr, size_t size) rb_bug("rb_gc_impl_size_slot_size: size too large (size=%"PRIuSIZE")", size); } - return rb_mmtk_total_obj_size(size) - sizeof(VALUE); + return rb_mmtk_align_obj_size(size + sizeof(VALUE)) - sizeof(VALUE); } bool diff --git a/shape.c b/shape.c index e9037acf6a3ccf..17df38ddd548f2 100644 --- a/shape.c +++ b/shape.c @@ -1343,7 +1343,6 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) if (rb_shape_verify_capacity_consistency_p(obj)) { attr_index_t shape_id_capacity = rb_shape_embedded_capacity(shape_id); - RUBY_ASSERT(shape_id_capacity > 0); size_t shape_id_slot_size = shape_id_capacity * sizeof(VALUE) + sizeof(struct RBasic); size_t actual_slot_size = rb_gc_obj_slot_size(obj); From 0293d6d6193d5e98575852679d56b74cca586c35 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 13 Aug 2026 20:22:02 -0700 Subject: [PATCH 22/25] ZJIT: Pass JIT-to-JIT call args on the stack --- zjit/src/backend/lir.rs | 64 +++++++++++++++++++++++++-------------- zjit/src/codegen.rs | 21 ++++++++++--- zjit/src/codegen_tests.rs | 21 +++++++++++-- zjit/src/hir.rs | 20 +----------- zjit/src/hir/opt_tests.rs | 36 +++++++++++++++------- zjit/src/hir/tests.rs | 15 ++++++--- 6 files changed, 113 insertions(+), 64 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index b62bdc86bac69d..e7b8a5fc655e78 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -250,7 +250,7 @@ pub use crate::backend::current::{ mem_base_reg, Reg, EC, CFP, SP, - NATIVE_BASE_PTR, + NATIVE_BASE_PTR, NATIVE_STACK_PTR, C_ARG_OPNDS, C_RET_OPND, }; @@ -2575,23 +2575,9 @@ impl Assembler if self.basic_blocks[block_id.0].is_dummy() { continue; } let params = self.basic_blocks[block_id.0].parameters.clone(); - // JIT-to-JIT entries that would need more argument registers should - // be unreachable because can_direct_send() refuses to call them. - // Keep compiling the function body, but make the unsupported entry - // abort if control ever reaches it. TODO: Remove this (Shopify/ruby#916) - if params.len() > C_ARG_OPNDS.len() { - let insert_pos = self.basic_blocks[block_id.0].insns.iter() - .position(|insn| matches!(insn, Insn::FrameSetup { .. })) - .or_else(|| self.basic_blocks[block_id.0].insns.iter().position(|insn| matches!(insn, Insn::Label(_))).map(|idx| idx + 1)) - .unwrap_or(0); - self.basic_blocks[block_id.0].insns.insert(insert_pos, Insn::Abort); - self.basic_blocks[block_id.0].insn_ids.insert(insert_pos, None); - continue; - } - // Rewrite VRegs to physical registers before sequentialization // so the parcopy algorithm can detect physical register conflicts. - let reg_copies: Vec> = params.iter().enumerate() + let reg_copies: Vec> = params.iter().take(C_ARG_OPNDS.len()).enumerate() .map(|(i, param)| parcopy::RegisterCopy:: { source: C_ARG_OPNDS[i], destination: Self::rewritten_opnd(*param, intervals, regs), @@ -2602,7 +2588,7 @@ impl Assembler debug_assert!(reg_copies.iter().all(|c| !c.source.is_vreg() && !c.destination.is_vreg()), "parcopy must operate on physical registers, not VRegs"); let sequentialized = parcopy::sequentialize_register(®_copies, Opnd::Reg(SCRATCH_REG)); - let moves: Vec = sequentialized + let mut moves: Vec = sequentialized .iter() .map(|copy| match copy.source { Opnd::Value(_) => Insn::LoadInto { @@ -2616,9 +2602,26 @@ impl Assembler }) .collect(); - // Find the position after FrameSetup to insert moves + // Parameters beyond the argument registers are passed on the native + // stack, as specified by the C ABI: handle_caller_saved_regs() in the + // caller pushes them right below its frame, so with the return + // address and the saved frame pointer in between ([Self::frame_size] + // bytes on both platforms), they sit right above this frame's + // NATIVE_BASE_PTR. Load them after the register moves above so that + // no argument register is clobbered before its copy is done. + for (stack_idx, param) in params.iter().enumerate().skip(C_ARG_OPNDS.len()) + .map(|(i, param)| (i - C_ARG_OPNDS.len(), param)) { + let src = Opnd::mem(64, NATIVE_BASE_PTR, Self::frame_size() + stack_idx as i32 * SIZEOF_VALUE_I32); + moves.push(Insn::Mov { dest: Self::rewritten_opnd(*param, intervals, regs), src }); + } + + // Find the position after FrameSetup to insert moves. They must come + // after FrameSetup (not before) because spilled destinations and + // stack-passed parameter sources are NATIVE_BASE_PTR-relative, and + // NATIVE_BASE_PTR points at the caller's frame until FrameSetup. let insert_pos = self.basic_blocks[block_id.0].insns.iter() .position(|insn| matches!(insn, Insn::FrameSetup { .. })) + .map(|idx| idx + 1) .or_else(|| self.basic_blocks[block_id.0].insns.iter().position(|insn| matches!(insn, Insn::Label(_))).map(|idx| idx + 1)) .unwrap_or(0); @@ -5143,18 +5146,26 @@ mod tests { } #[test] - fn test_resolve_ssa_entry_params_too_many_abort() { + fn test_resolve_ssa_entry_params_beyond_arg_regs_use_stack() { let mut asm = Assembler::new(); let block = asm.new_block(hir::BlockId(0), true, 0); asm.set_current_block(block); let label = asm.new_label("bb0"); asm.write_label(label); - for _ in 0..=C_ARG_OPNDS.len() { + let params: Vec = (0..=C_ARG_OPNDS.len()).map(|_| { let param = asm.new_vreg(64); asm.basic_blocks[block.0].add_parameter(param); + param + }).collect(); + // Use every parameter so they are all live and get allocations. + let mut acc = params[0]; + for ¶m in ¶ms[1..] { + let out = asm.new_vreg(64); + asm.basic_blocks[block.0].push_insn(Insn::Add { left: acc, right: param, out }); + acc = out; } - asm.basic_blocks[block.0].push_insn(Insn::CRet(Opnd::UImm(0))); + asm.basic_blocks[block.0].push_insn(Insn::CRet(acc)); let live_in = asm.analyze_liveness(); asm.number_instructions(0); @@ -5165,7 +5176,16 @@ mod tests { asm.resolve_ssa(&intervals, ®s); - assert!(matches!(asm.basic_blocks[block.0].insns[1], Insn::Abort)); + // The parameter that doesn't fit in argument registers is loaded from + // the caller's outgoing-argument area, right above this frame's + // return address and saved frame pointer. + let insns = &asm.basic_blocks[block.0].insns; + assert!(!insns.iter().any(|insn| matches!(insn, Insn::Abort)), "no entry should abort"); + let expected_src = Opnd::mem(64, NATIVE_BASE_PTR, Assembler::frame_size()); + assert!( + insns.iter().any(|insn| matches!(insn, Insn::Mov { src, .. } if *src == expected_src)), + "expected a load from {expected_src:?}, got: {insns:?}" + ); } fn build_critical_edge() -> (Assembler, Opnd, Opnd, Opnd, Opnd, Opnd, BlockId, BlockId, BlockId) { diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 9f4ea3050ab65b..ef221f1ec1270e 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -22,7 +22,7 @@ use crate::state::ZJITState; use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_fallback_counter_for_optimized_method_type}; use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Counter::{compile_time_ns, exit_compile_error}}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; -use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; +use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; use crate::hir::{self, iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason, qualified_method_name}; use crate::hir_type::{types, Type}; @@ -3937,15 +3937,26 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result C_ARG_OPNDS.len() { - return Err(SendDirectFailure::new(TooManyArgsForLir)); - } // IseqCall stores the JIT entry index and argc as u16. if u16::try_from(send_argc).is_err() { @@ -2992,12 +2980,6 @@ fn can_direct_invoke_block_iseq(iseq: IseqPtr, argc: usize) -> Result<(), SendFa if argc == 1 && !unsafe { rb_get_iseq_flags_ambiguous_param0(iseq) } { return Err(InvokeBlockNotSpecialized); } - // The JIT-to-JIT call in gen_invoke_block_iseq_direct passes captured self plus - // each argument in C argument registers. - // TODO: Support passing arguments on the stack in C calls - if 1 + argc > C_ARG_OPNDS.len() { - return Err(TooManyArgsForLir); - } if crate::codegen::block_iseq_may_throw(iseq) { return Err(InvokeBlockNotSpecialized); } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 82037629c170a9..fcc833aa2052d0 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4217,10 +4217,10 @@ mod hir_opt_tests { } #[test] - fn test_yield_with_too_many_args_for_lir_falls_back() { + fn test_yield_with_stack_args_specializes() { // Captured self plus eight args don't fit in C argument registers (6 on x86_64, - // 8 on arm64), so the profiled invokeblock specialization must not emit - // InvokeBlockIseqDirect. + // 8 on arm64); the profiled invokeblock specialization still emits + // InvokeBlockIseqDirect, passing the overflow arguments on the stack. let result = eval(" def foo = yield(1, 2, 3, 4, 5, 6, 7, 8) def test = foo { |a, b, c, d, e, f, g, h| a + b + c + d + e + f + g + h } @@ -4247,15 +4247,24 @@ mod hir_opt_tests { v20:Fixnum[6] = Const Value(6) v22:Fixnum[7] = Const Value(7) v24:Fixnum[8] = Const Value(8) - v26:BasicObject = InvokeBlock v10, v12, v14, v16, v18, v20, v22, v24 # SendFallbackReason: Too many arguments for LIR + v26:CPtr = GetEP 0 + v27:CInt64 = LoadField v26, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 + v28:CInt64[3] = Const CInt64(3) + v29:CInt64 = IntAnd v27, v28 + v30:CInt64[1] = GuardBitEquals v29, CInt64(1) recompile + v31:CInt64[-4] = Const CInt64(-4) + v32:CInt64 = IntAnd v27, v31 + v33:CPtr = LoadField v32, :code_iseq@0x1001 + v34:CPtr[CPtr(0x1002)] = GuardBitEquals v33, CPtr(0x1002) recompile + v35:BasicObject = InvokeBlockIseqDirect (0x1002), v32, v10, v12, v14, v16, v18, v20, v22, v24 CheckInterrupts - Return v26 + Return v35 "); } #[test] - fn test_inlined_yield_with_too_many_args_for_lir_falls_back() { - // Same as test_yield_with_too_many_args_for_lir_falls_back, but for the guard-free + fn test_inlined_yield_with_stack_args_specializes() { + // Same as test_yield_with_stack_args_specializes, but for the guard-free // yield dispatch inside an inlined callee whose caller passes a literal block. let result = eval(" def foo = yield(1, 2, 3, 4, 5, 6, 7, 8) @@ -4286,10 +4295,14 @@ mod hir_opt_tests { v35:Fixnum[6] = Const Value(6) v37:Fixnum[7] = Const Value(7) v39:Fixnum[8] = Const Value(8) - v41:BasicObject = InvokeBlock v25, v27, v29, v31, v33, v35, v37, v39 # SendFallbackReason: Too many arguments for LIR + v41:CPtr = GetEP 0 + v42:CInt64 = LoadField v41, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 + v43:CInt64[-4] = Const CInt64(-4) + v44:CInt64 = IntAnd v42, v43 + v45:BasicObject = InvokeBlockIseqDirect (0x1068), v44, v25, v27, v29, v31, v33, v35, v37, v39 CheckInterrupts PopInlineFrame - Return v41 + Return v45 "); } @@ -5295,8 +5308,9 @@ mod hir_opt_tests { v37:Fixnum[60] = Const Value(60) v39:Fixnum[70] = Const Value(70) v41:Fixnum[80] = Const Value(80) - v43:BasicObject = Send v52, :target, v27, v29, v31, v33, v35, v37, v39, v41 # SendFallbackReason: Too many arguments for LIR - v45:ArrayExact = NewArray v53, v56, v43 + PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) + v59:BasicObject = SendDirect v52, 0x0, :target (0x1038), jit_entry_idx=7, v27, v29, v31, v33, v35, v37, v39, v41 + v45:ArrayExact = NewArray v53, v56, v59 CheckInterrupts Return v45 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 444e81f0751fa7..ed819e7a34a65f 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -293,11 +293,18 @@ mod snapshot_tests { v23:Fixnum[7] = Const Value(7) v25:Fixnum[8] = Const Value(8) v26:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15, v17, v19, v21, v23, v25], locals: [] } - v27:BasicObject = Send v6, :foo, v11, v13, v15, v17, v19, v21, v23, v25 # SendFallbackReason: Too many arguments for LIR - v28:Any = Snapshot FrameState { pc: 0x1010, stack: [v27], locals: [] } - PatchPoint NoTracePoint + PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) + v34:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile + v35:Any = Snapshot FrameState { pc: 0x1008, stack: [v34, v11, v13, v19, v21, v17, v15, v23, v25], locals: [] } + v64:Fixnum[0] = Const Value(0) + v37:Any = Snapshot FrameState { pc: 0x1008, stack: [], locals: [] } + PushInlineFrame :foo, v34 (0x1048), num_args=8 + v58:Any = Snapshot FrameState { pc: 0x1070, stack: [v19, v21, v17, v15, v11, v13, v23, v25], locals: [five=v11, six=v13, a=v19, b=v21, c=v17, d=v15, e=v23, f=v25, ID(0)=v64], caller: v37 } + v59:ArrayExact = NewArray v19, v21, v17, v15, v11, v13, v23, v25 + v60:Any = Snapshot FrameState { pc: 0x1078, stack: [v59], locals: [five=v11, six=v13, a=v19, b=v21, c=v17, d=v15, e=v23, f=v25, ID(0)=v64], caller: v37 } CheckInterrupts - Return v27 + PopInlineFrame + Return v59 "); } } From 64b9d2f84ec56278e32fff23e1b6cd20e04f8a9f Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 17 Aug 2026 17:10:13 -0700 Subject: [PATCH 23/25] ZJIT: Allow STP with an identical register pair Storing the same register twice with STP is well-defined, unlike LDP, whose destination registers must be distinct. CPushPair hits this when two adjacent stack-passed arguments are the same zero immediate (both lowered to xzr, e.g. two literal false arguments) or the same register, so drop the same-register assertion from the STP encoders. The assertion was inherited from YJIT, whose backend only ever stored pairs of distinct caller-saved registers. --- zjit/src/asm/arm64/mod.rs | 19 ++++++++++++++++--- zjit/src/codegen_tests.rs | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/zjit/src/asm/arm64/mod.rs b/zjit/src/asm/arm64/mod.rs index b53f1cf6733664..6301f5e4a59c63 100644 --- a/zjit/src/asm/arm64/mod.rs +++ b/zjit/src/asm/arm64/mod.rs @@ -886,12 +886,15 @@ pub fn stlxr(cb: &mut CodeBlock, rs: A64Opnd, rt: A64Opnd, rn: A64Opnd) { } /// STP (signed offset) - store a pair of registers to memory +/// +/// Unlike LDP, STP with the same source register twice is well-defined +/// (e.g. `stp xzr, xzr` is a common way to zero 16 bytes), so the pair is +/// not asserted to be distinct. pub fn stp(cb: &mut CodeBlock, rt1: A64Opnd, rt2: A64Opnd, rn: A64Opnd) { let bytes: [u8; 4] = match (rt1, rt2, rn) { (A64Opnd::Reg(rt1), A64Opnd::Reg(rt2), A64Opnd::Mem(rn)) => { assert!(rt1.num_bits == rt2.num_bits, "Expected source registers to be the same size"); assert!(imm_fits_bits(rn.disp.into(), 10), "The displacement must be 10 bits or less."); - assert_ne!(rt1.reg_no, rt2.reg_no, "Behavior is unpredictable with pairs of the same register"); RegisterPair::stp(rt1.reg_no, rt2.reg_no, rn.base_reg_no, rn.disp as i16, rt1.num_bits).into() }, @@ -902,12 +905,13 @@ pub fn stp(cb: &mut CodeBlock, rt1: A64Opnd, rt2: A64Opnd, rn: A64Opnd) { } /// STP (pre-index) - store a pair of registers to memory, update the base pointer before loading it +/// +/// Unlike LDP, STP with the same source register twice is well-defined; see [stp]. pub fn stp_pre(cb: &mut CodeBlock, rt1: A64Opnd, rt2: A64Opnd, rn: A64Opnd) { let bytes: [u8; 4] = match (rt1, rt2, rn) { (A64Opnd::Reg(rt1), A64Opnd::Reg(rt2), A64Opnd::Mem(rn)) => { assert!(rt1.num_bits == rt2.num_bits, "Expected source registers to be the same size"); assert!(imm_fits_bits(rn.disp.into(), 10), "The displacement must be 10 bits or less."); - assert_ne!(rt1.reg_no, rt2.reg_no, "Behavior is unpredictable with pairs of the same register"); RegisterPair::stp_pre(rt1.reg_no, rt2.reg_no, rn.base_reg_no, rn.disp as i16, rt1.num_bits).into() }, @@ -918,12 +922,13 @@ pub fn stp_pre(cb: &mut CodeBlock, rt1: A64Opnd, rt2: A64Opnd, rn: A64Opnd) { } /// STP (post-index) - store a pair of registers to memory, update the base pointer after loading it +/// +/// Unlike LDP, STP with the same source register twice is well-defined; see [stp]. pub fn stp_post(cb: &mut CodeBlock, rt1: A64Opnd, rt2: A64Opnd, rn: A64Opnd) { let bytes: [u8; 4] = match (rt1, rt2, rn) { (A64Opnd::Reg(rt1), A64Opnd::Reg(rt2), A64Opnd::Mem(rn)) => { assert!(rt1.num_bits == rt2.num_bits, "Expected source registers to be the same size"); assert!(imm_fits_bits(rn.disp.into(), 10), "The displacement must be 10 bits or less."); - assert_ne!(rt1.reg_no, rt2.reg_no, "Behavior is unpredictable with pairs of the same register"); RegisterPair::stp_post(rt1.reg_no, rt2.reg_no, rn.base_reg_no, rn.disp as i16, rt1.num_bits).into() }, @@ -1822,6 +1827,14 @@ mod tests { assert_snapshot!(cb.hexdump(), @"8a2d8da8"); } + #[test] + fn test_stp_pre_same_reg() { + // Unlike LDP, storing the same register twice is well-defined. + let cb = compile(|cb| stp_pre(cb, X31, X31, A64Opnd::new_mem(64, X31, -16))); + assert_disasm_snapshot!(cb.disasm(), @" 0x0: stp xzr, xzr, [sp, #-0x10]!"); + assert_snapshot!(cb.hexdump(), @"ff7fbfa9"); + } + #[test] fn test_str_post() { let cb = compile(|cb| str_post(cb, X10, A64Opnd::new_mem(64, X11, -16))); diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 611ece6a8c33ea..ebcad96fb9840a 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -742,6 +742,22 @@ fn test_send_direct_with_stack_args() { assert_snapshot!(assert_compiles("test"), @"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"); } +#[test] +fn test_send_direct_with_equal_stack_args() { + // A pair of adjacent stack-passed arguments that are the same zero immediate + // (false) or the same register lowers to an STP with an identical register + // pair on arm64, e.g. `stp xzr, xzr`, which the assembler used to reject. + // On arm64, c_args[8] and c_args[9] (arguments h and i below) form a pair. + set_call_threshold(2); + eval(" + def callee(a, b, c, d, e, f, g, h, i, j) = [a, b, c, d, e, f, g, h, i, j] + def test(x = 9) = [callee(1, 2, 3, 4, 5, 6, 7, false, false, 10), callee(1, 2, 3, 4, 5, 6, 7, x, x, 10)] + test + test + "); + assert_snapshot!(assert_compiles("test"), @"[[1, 2, 3, 4, 5, 6, 7, false, false, 10], [1, 2, 3, 4, 5, 6, 7, 9, 9, 10]]"); +} + #[test] fn test_yield_inline_invocation_live_stack_below_args() { // A live value sits on the stack below the yield args; the no-receiver-slot SP math From 4a6b7fa6cbde939ff14873dd0774692a557cc5f7 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 18 Aug 2026 11:27:59 -0700 Subject: [PATCH 24/25] ZJIT: Rename stack-args tests after ABI registers --- zjit/src/codegen_tests.rs | 6 +++--- zjit/src/hir/opt_tests.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index ebcad96fb9840a..e51fdfc8093908 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -713,7 +713,7 @@ fn test_yield_inline_invocation_with_args() { } #[test] -fn test_yield_with_stack_args() { +fn test_yield_with_more_args_than_abi_registers() { // `self` + eight yield args don't fit in C argument registers (6 on x86_64, 8 on // arm64), so the direct block invocation passes the overflow arguments on the // native stack. @@ -728,7 +728,7 @@ fn test_yield_with_stack_args() { } #[test] -fn test_send_direct_with_stack_args() { +fn test_send_direct_with_more_args_than_abi_registers() { // `self` + ten args don't fit in C argument registers (6 on x86_64, 8 on arm64), // so the JIT-to-JIT call passes the overflow arguments on the native stack, and // the callee's JIT entry loads them from above its frame. @@ -743,7 +743,7 @@ fn test_send_direct_with_stack_args() { } #[test] -fn test_send_direct_with_equal_stack_args() { +fn test_send_direct_with_equal_args_beyond_abi_registers() { // A pair of adjacent stack-passed arguments that are the same zero immediate // (false) or the same register lowers to an STP with an identical register // pair on arm64, e.g. `stp xzr, xzr`, which the assembler used to reject. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index fcc833aa2052d0..f05eb4f592bd04 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4217,7 +4217,7 @@ mod hir_opt_tests { } #[test] - fn test_yield_with_stack_args_specializes() { + fn test_specialize_yield_with_more_args_than_abi_registers() { // Captured self plus eight args don't fit in C argument registers (6 on x86_64, // 8 on arm64); the profiled invokeblock specialization still emits // InvokeBlockIseqDirect, passing the overflow arguments on the stack. @@ -4263,8 +4263,8 @@ mod hir_opt_tests { } #[test] - fn test_inlined_yield_with_stack_args_specializes() { - // Same as test_yield_with_stack_args_specializes, but for the guard-free + fn test_specialize_inlined_yield_with_more_args_than_abi_registers() { + // Same as test_specialize_yield_with_more_args_than_abi_registers, but for the guard-free // yield dispatch inside an inlined callee whose caller passes a literal block. let result = eval(" def foo = yield(1, 2, 3, 4, 5, 6, 7, 8) From e1887322aee2ef8d434a700a3c697b26a81e4e8f Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 18 Aug 2026 11:34:42 -0700 Subject: [PATCH 25/25] ZJIT: Extract c_arg_location() for C-ABI argument locations --- zjit/src/backend/lir.rs | 53 +++++++++++++++++++++++++---------------- zjit/src/codegen.rs | 22 ++++++++--------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index e7b8a5fc655e78..cf5f1a40d751f7 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -256,6 +256,27 @@ pub use crate::backend::current::{ pub static JIT_PRESERVED_REGS: &[Opnd] = &[CFP, SP, EC]; +/// Where the C calling convention passes an argument. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum CArgLocation { + /// In one of the argument registers. + Reg(Opnd), + /// In the stack slot that the caller reserves at the bottom of its frame. + /// The slot address depends on the reader: the caller writes it relative + /// to the native SP at the call, and the callee reads it from above its + /// return address and saved frame pointer. + StackSlot(usize), +} + +/// Return where the C calling convention passes argument `idx`. +pub fn c_arg_location(idx: usize) -> CArgLocation { + if idx < C_ARG_OPNDS.len() { + CArgLocation::Reg(C_ARG_OPNDS[idx]) + } else { + CArgLocation::StackSlot(idx - C_ARG_OPNDS.len()) + } +} + // Memory operand base #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] pub enum MemBase @@ -2575,20 +2596,23 @@ impl Assembler if self.basic_blocks[block_id.0].is_dummy() { continue; } let params = self.basic_blocks[block_id.0].parameters.clone(); - // Rewrite VRegs to physical registers before sequentialization - // so the parcopy algorithm can detect physical register conflicts. - let reg_copies: Vec> = params.iter().take(C_ARG_OPNDS.len()).enumerate() + // Rewrite VRegs to physical registers or stack slots before sequentialization + // so the parcopy algorithm can detect conflicts between them. + let copies: Vec> = params.iter().enumerate() .map(|(i, param)| parcopy::RegisterCopy:: { - source: C_ARG_OPNDS[i], + source: match c_arg_location(i) { + CArgLocation::Reg(reg) => reg, + CArgLocation::StackSlot(slot) => Opnd::mem(64, NATIVE_BASE_PTR, Self::frame_size() + slot as i32 * SIZEOF_VALUE_I32), + }, destination: Self::rewritten_opnd(*param, intervals, regs), }) .filter(|copy| copy.source != copy.destination) .collect(); - debug_assert!(reg_copies.iter().all(|c| !c.source.is_vreg() && !c.destination.is_vreg()), - "parcopy must operate on physical registers, not VRegs"); - let sequentialized = parcopy::sequentialize_register(®_copies, Opnd::Reg(SCRATCH_REG)); - let mut moves: Vec = sequentialized + debug_assert!(copies.iter().all(|c| !c.source.is_vreg() && !c.destination.is_vreg()), + "parcopy must operate on physical locations, not VRegs"); + let sequentialized = parcopy::sequentialize_register(&copies, Opnd::Reg(SCRATCH_REG)); + let moves: Vec = sequentialized .iter() .map(|copy| match copy.source { Opnd::Value(_) => Insn::LoadInto { @@ -2602,19 +2626,6 @@ impl Assembler }) .collect(); - // Parameters beyond the argument registers are passed on the native - // stack, as specified by the C ABI: handle_caller_saved_regs() in the - // caller pushes them right below its frame, so with the return - // address and the saved frame pointer in between ([Self::frame_size] - // bytes on both platforms), they sit right above this frame's - // NATIVE_BASE_PTR. Load them after the register moves above so that - // no argument register is clobbered before its copy is done. - for (stack_idx, param) in params.iter().enumerate().skip(C_ARG_OPNDS.len()) - .map(|(i, param)| (i - C_ARG_OPNDS.len(), param)) { - let src = Opnd::mem(64, NATIVE_BASE_PTR, Self::frame_size() + stack_idx as i32 * SIZEOF_VALUE_I32); - moves.push(Insn::Mov { dest: Self::rewritten_opnd(*param, intervals, regs), src }); - } - // Find the position after FrameSetup to insert moves. They must come // after FrameSetup (not before) because spilled destinations and // stack-passed parameter sources are NATIVE_BASE_PTR-relative, and diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index ef221f1ec1270e..e39822242b523d 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -22,7 +22,7 @@ use crate::state::ZJITState; use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_fallback_counter_for_optimized_method_type}; use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Counter::{compile_time_ns, exit_compile_error}}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; -use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; +use crate::backend::lir::{self, Assembler, CArgLocation, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; use crate::hir::{self, iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason, qualified_method_name}; use crate::hir_type::{types, Type}; @@ -3943,16 +3943,16 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result reg, + CArgLocation::StackSlot(slot) => { + // The stub runs before any frame setup, so stack-passed arguments + // sit right above the return address (x86_64) or right at the + // native SP (arm64, where the return address is in a register). + let ret_addr_bytes = if cfg!(target_arch = "x86_64") { SIZEOF_VALUE_I32 } else { 0 }; + asm.load_into(scratch_reg, Opnd::mem(64, NATIVE_STACK_PTR, ret_addr_bytes + slot as i32 * SIZEOF_VALUE_I32)); + scratch_reg + } }; asm.store( Opnd::mem(64, SP, -local_size_and_idx_to_bp_offset(local_size, arg_idx) * SIZEOF_VALUE_I32),