Skip to content

[pull] master from ruby:master - #1388

Merged
pull[bot] merged 24 commits into
turkdevops:masterfrom
ruby:master
Sep 8, 2026
Merged

[pull] master from ruby:master#1388
pull[bot] merged 24 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Sep 8, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

eightbitraptor and others added 24 commits September 7, 2026 22:58
heap_pages_free_unused_pages called heap_page_free once per empty
page, and each call took page_pool.lock twice: once in
global_page_index_remove and once in page_pool_release.  A full drain
of N pages cost 2N lock cycles

This commit links pages into a local chain using free_next and then
releases the whole chain under a single page_pool.lock.
This commit initializes the page_pool.lock with PTHREAD_MUTEX_ERRORCHECK
in RGENGC_CHECK_MODE builds and asserts that the the mutex lock is
EDEADLK.
heap_pages_free_batch locked page_pool.lock once per page on the
non-mmap path, through global_page_index_remove.
The redirect handling only compared hosts, so an https source redirecting
to http on the same host had the request's user and password copied onto a
plaintext connection. Gem::RemoteFetcher already refuses such a redirect.

ruby/rubygems@56e1b63333

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CRT _tzname is encoded in the active code page, while zone_str()
assumed the UTF-8 converted name from GetDynamicTimeZoneInformation.
Tag strings from the _tzname fallback with the encoding of GetACP(),
and stop asserting the locale encoding on Windows, where the
assertion restated the bug.

Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org>
`update-src` truncated revision.h to force the regeneration, but the
blank file also defeated the guard in file2lastrev.rb that keeps the
existing content when no VCS is available, so `make up` on a released
tarball dropped `RUBY_REVISION` and `RUBY_FULL_REVISION`.  Removing the
timestamp alone is enough to force the regeneration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action).


Updates `taiki-e/install-action` from 2.87.5 to 2.87.6
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](taiki-e/install-action@5bf6ce0...7b8d471)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
TYPE_UCLASS only compared the built-in type before RBASIC_SET_CLASS, so
a struct could be relabelled as a struct class with a different number
of members, and its accessors then read and wrote past the allocated
slots. Apply the member count check that TYPE_STRUCT already has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Net::HTTPHeader#chunked? matched the token anywhere in
Transfer-Encoding, so a response carrying `chunked, gzip` was
chunk-framed and the bytes after the terminating chunk were left
unread on the connection.

See RFC 9112 Section 6.3:
https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3

    If a Transfer-Encoding header field is present in a response and
    the chunked transfer coding is not the final encoding, the message
    body length is determined by reading the connection until it is
    closed by the server.

ruby/net-http@e70762aaf9

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once chunked is not the final transfer coding, read_body_0 fell through
to Content-Length and framed the body by it, which leaves the client and
the server disagreeing about where the response ends.

See RFC 9112 Section 6.3:
https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3

    If a message is received with both a Transfer-Encoding and a
    Content-Length header field, the Transfer-Encoding overrides the
    Content-Length.

ruby/net-http@9fb5b93b52

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rb_ractor_terminate_all() interrupts the other Ractors and waits on a
condvar of its own for them to finish, dropping the VM lock for the wait
and taking it back after.  That was rb_vm_cond_timedwait(), whose only
caller it was; rb_vm_cond_wait() had none at all.

Move the wait to rb_ractor_sched_wait_terminate() in thread_sched.c and
give the native thread back for its duration, as a blocking region does.
Waiting natively holds whatever native thread the caller runs on, and an
M:N caller runs on one from the shared pool -- so it would wait, holding
the pool, for Ractors that need the pool to finish.  It does not happen
today, the caller being the main Ractor's main thread with a native
thread of its own, but a scheduler-blind wait in the scheduler's way is
worth keeping out of vm_sync.c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`RUBY_MN_THREADS=1` only affects threads created after it is read: the
main thread keeps a native thread of its own and is woken through its
condvar, so handing control to the main thread costs an order of
magnitude more than handing it between any two other threads (6.4us
against 0.56us for a Queue round trip here).

With `RUBY_MN_THREADS=2` the running main thread is turned into an M:N
thread in place (thread_sched_main_to_shared), rather than spawning a
thread for the program body and leaving the main native thread idle:

* the process stack becomes the main thread's coroutine context
  (coroutine_initialize_main), the way nt_start's own stack is a shared
  thread's context;
* the main native thread joins the shared pool with a stack of its own
  for its scheduling loop (nt_loop_co), which starts on the first
  transfer into it -- the main thread's first park.  From then on any
  shared native thread may resume the main thread, and this one serves
  any Ractor.

nt_start's shared loop is split out as nt_shared_loop so that both
entries share it.  The main native thread never retires, its loop being
on a stack only it could free: ractor_sched_deq takes a can_retire flag
and native_thread_dedicated_dec always lets it rejoin.

The wait in rb_ractor_terminate_all had to give its native thread back
first, or exit hung with the Ractors it waits for having nothing to run
on; that is the commit before this one.  The VM barrier waits natively
in the same way and has the same exposure, and is not touched here,
being no more reachable under `=2` than before.

Several places assumed the main thread owns the process's initial native
thread:

* native_thread_init_stack set the process stack's range for any thread
  starting on that native thread, over the pool stack an M:N thread
  already owns; the case is now keyed on th->sched.context.
* thread_sched_switch passed to_dead for a thread whose status is
  THREAD_KILLED, which the main thread already is while
  rb_ractor_terminate_all parks it.  A thread ends only through
  co_start's epilogue transfer.
* thread_sched_atfork left the forker's nt->running_th and nt->retiring
  as the parent had them; retire eligibility is decided against the
  process's main native thread, refreshed there.
* rb_thread_free_native_thread (RUBY_FREE_AT_EXIT) destroyed the native
  thread hosting the main thread even when shared, along with the
  altstack registered on the thread running it.

RUBY_FREE_AT_EXIT now leaves more behind: the main native thread and
its context and stack, since a thread is parked in its loop until the
process ends, and the main thread's own coroutine context, since the
free-at-exit path does not reach rb_threadptr_sched_free.  Leak-checker
baselines move by that much.

The OS thread name is no longer set from the Ruby thread for M:N
threads.  One shared native thread runs many Ruby threads over its life,
so the name described whichever one happened to start on it; under `=2`
that renamed the process itself, the thread group leader's comm being
what ps and pkill show.  Thread#name= was already skipped for M:N
threads for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Ractor's threads have been on the M:N scheduler since it was added,
with no way to turn that off: RUBY_MN_THREADS only ever decided whether
the main Ractor joined them.  There is no way, then, to tell an M:N bug
from a Ractor bug, and a C extension that keeps state per native thread
cannot be used from a Ractor at all.

RUBY_MN_THREADS=-1 gives every Ractor's threads a native thread of their
own, as the main Ractor's have by default.  With it the setting reads as
one ladder: -1 nothing, 0 (the default) a Ractor's threads, 1 the main
Ractor's threads too, 2 the main thread as well.

The shared pool is left unmade when nothing is M:N, rather than minting
native threads that no thread can park on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same character count clamp reaches the backward scan in str_rindex() and
the pos == 0 shortcut in rb_str_rindex(), where a pattern with wider
characters than the receiver is still compared past its last byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
String#chomp steps back by the encoding's minimum character width before
testing for a newline, which reads before the first byte of the receiver
when it holds less than one whole character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The BOM written ahead of the first character was not counted, so that
character can need 6 bytes for UTF-16 and 8 for UTF-32 against a declared
max_output of 4. Since the engine lets a transcoder write straight into
the caller's window once max_output bytes are free, a window of exactly 4
overran it:

    ec = Encoding::Converter.new("UTF-8", "UTF-16")
    ec.primitive_convert("\u{1F600}", "X" * 4092, 4092, 4)
    # -e:1: [BUG] probable buffer overflow: 4098 for 4096

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A katakana held back for a possible sound mark is flushed by the next
character, costing a designation and two data bytes before that
character's own designation and data, so one call can write 9 bytes
against a declared max_output of 5:

    ec = Encoding::Converter.new("CP51932", "CP50220")
    ec.primitive_convert("\x8e\xb6\x8e\xe0".b, "X" * 4088, 4088, 8)
    # -e:1: [BUG] probable buffer overflow: 4097 for 4096

9 is past the 8 byte inline write buffer in transcode.c, so this
transcoder now allocates one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e extraction root

`extract_tar_gz` only ran relative link targets through `File.expand_path`,
so an absolute target kept its `..` components. A target such as
`<destination_dir>/../../etc` therefore passed the prefix check and resolved
outside the extraction root.

ruby/rubygems@a34a62c3a1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Avoid quadratic parse time on long and/or chains

logop() builds a left-associative and/or chain into a right-leaning
tree, and found the insertion point by walking the whole right spine
from the top on every operator. For a chain such as
`a && a && ... && a` that walk is O(n) per operator, so parsing the
chain is O(n^2): 32k operators took seconds.

Cache the tail of the chain most recently built by logop(), so an
operator that extends the same chain reaches the insertion point in
constant time, making the parse linear. The cached tail is validated
before use (it must still be a node of the chain's type whose nd_2nd
is not another such node), so a stale entry from an earlier parse
falls back to the scan. The resulting tree is unchanged: AST and
compiled bytecode are byte-identical to before across left-associative,
parenthesized, and mixed and/or chains.
@pull pull Bot locked and limited conversation to collaborators Sep 8, 2026
@pull pull Bot added the ⤵️ pull label Sep 8, 2026
@pull
pull Bot merged commit c9764e8 into turkdevops:master Sep 8, 2026
1 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants