diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml
index 72b3c200edb4fb..648c642bd7ccbb 100644
--- a/.github/workflows/zjit-macos.yml
+++ b/.github/workflows/zjit-macos.yml
@@ -98,7 +98,7 @@ jobs:
rustup install ${{ matrix.rust_version }} --profile minimal
rustup default ${{ matrix.rust_version }}
- - uses: taiki-e/install-action@5bf6ce016fd2e72eefc647cbca1e4213f65955b8 # v2.87.5
+ - uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml
index b0475e62dab611..15427f8b8a5130 100644
--- a/.github/workflows/zjit-ubuntu.yml
+++ b/.github/workflows/zjit-ubuntu.yml
@@ -152,7 +152,7 @@ jobs:
ruby-version: '3.1'
bundler: none
- - uses: taiki-e/install-action@5bf6ce016fd2e72eefc647cbca1e4213f65955b8 # v2.87.5
+ - uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
diff --git a/NEWS.md b/NEWS.md
index 8478a47caa3637..4e9038baf77d17 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -356,6 +356,52 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
* `ObjectSpace.define_finalizer` on another Ractor's object raises
`Ractor::IsolationError`.
+### M:N thread scheduler
+
+* The scheduler scales with the number of waiters and of Ractors, where it
+ used to walk a list or take one lock for all of them:
+
+ * A timed wait sits in a hierarchical timer wheel rather than on a list
+ sorted by deadline, which was inserted into by a linear scan.
+ * An fd stays armed in the backend between waits, instead of being added
+ before each wait and removed after each wake.
+ * The io-wait bookkeeping is sharded by fd, rather than serialized on one
+ lock across every fd.
+ * A timed wait on an fd rides the scheduler instead of going to a blocking
+ region, which cost a native thread handoff per wait.
+ * A context switch, and leaving or rejoining the shared pool, no longer take
+ the scheduler's global lock.
+
+* The `RUBY_MN_THREADS` environment variable now runs from no M:N scheduling
+ at all to all of it. `-1` is new: a Ractor's threads have been M:N since
+ the scheduler was added, with no way to turn that off. `0` and `1` are
+ unchanged.
+
+ | | main thread | the main Ractor's other threads | a Ractor's threads |
+ |---|---|---|---|
+ | `-1` | 1:1 | 1:1 | 1:1 |
+ | `0` or unset | 1:1 | 1:1 | M:N |
+ | `1` | 1:1 | M:N | M:N |
+ | `2` | M:N | M:N | M:N |
+
+* `RUBY_MN_THREADS=2` is new. The main thread is resumed like any other M:N
+ thread rather than woken on a native thread of its own, which costs an order
+ of magnitude more. It pays off when the main thread drives the work, and
+ does nothing for one that only starts other threads and waits.
+
+ The main thread is then no longer bound to one OS thread, which is what the
+ M:N scheduler already meant for every other thread:
+
+ * A C extension that keeps state per OS thread has to call
+ `rb_thread_lock_native_thread()`.
+ * What must run on the process's initial thread does not work at all,
+ pinning included: macOS AppKit and CFRunLoop, and hosts that embed Ruby
+ and return into their own main loop.
+
+* The OS thread name is no longer set from the Ruby thread for M:N threads:
+ one native thread runs many of them over its life. `Thread#name=` was
+ already skipped for the same reason.
+
## JIT
[Bug #18947]: https://bugs.ruby-lang.org/issues/18947
diff --git a/common.mk b/common.mk
index 7b63cbd96db34a..4fbd986118dbf6 100644
--- a/common.mk
+++ b/common.mk
@@ -1520,9 +1520,10 @@ after-update:: extract-extlibs
after-update:: extract-gems
after-update:: update-default-gemspecs
+# Do not remove or empty revision.h itself, whose content file2lastrev.rb
+# keeps when the source tree has no VCS.
update-src::
- $(Q) $(RM) $(REVISION_H) revision.h "$(srcdir)/$(REVISION_H)" "$(srcdir)/revision.h"
- $(Q) exit > "$(srcdir)/revision.h"
+ $(Q) $(RM) $(REVISION_H) "$(srcdir)/$(REVISION_H)"
# $(REVISION_H) can have been made already in this run, as a prerequisite
# of the included dependency file, and make does not make it twice.
diff --git a/enc/trans/iso2022.trans b/enc/trans/iso2022.trans
index bc42bbc19c3c05..6e426d51686242 100644
--- a/enc/trans/iso2022.trans
+++ b/enc/trans/iso2022.trans
@@ -544,7 +544,7 @@ rb_cp50220_encoder = {
TRANSCODE_TABLE_INFO,
1, /* input_unit_length */
3, /* max_input */
- 5, /* max_output */
+ 9, /* max_output */
asciicompat_encoder, /* asciicompat_type */
3, iso2022jp_init, iso2022jp_init, /* state_size, state_init, state_fini */
NULL, NULL, NULL, fun_so_cp50220_encoder,
diff --git a/enc/trans/utf_16_32.trans b/enc/trans/utf_16_32.trans
index 632c8808efc65a..4a0168b17f00b1 100644
--- a/enc/trans/utf_16_32.trans
+++ b/enc/trans/utf_16_32.trans
@@ -521,7 +521,7 @@ rb_to_UTF_16 = {
TRANSCODE_TABLE_INFO,
1, /* input_unit_length */
4, /* max_input */
- 4, /* max_output */
+ 6, /* max_output */
asciicompat_encoder, /* asciicompat_type */
1, state_init, NULL, /* state_size, state_init, state_fini */
NULL, NULL, NULL, fun_so_to_utf_16
@@ -533,7 +533,7 @@ rb_to_UTF_32 = {
TRANSCODE_TABLE_INFO,
1, /* input_unit_length */
4, /* max_input */
- 4, /* max_output */
+ 8, /* max_output */
asciicompat_encoder, /* asciicompat_type */
1, state_init, NULL, /* state_size, state_init, state_fini */
NULL, NULL, NULL, fun_so_to_utf_32
diff --git a/file.c b/file.c
index 167cc32933b823..7f0967e5c18719 100644
--- a/file.c
+++ b/file.c
@@ -1505,12 +1505,28 @@ rb_stat(VALUE file, struct stat *st)
}
/*
+ * :markup: markdown
+ *
* call-seq:
- * File.stat(filepath) -> stat
+ * File.stat(path) -> file_stat
*
- * Returns a File::Stat object for the file at +filepath+ (see File::Stat):
+ * Returns a new File::Stat object for the entry at `path`.
+ * Follows [symbolic links](file/symbolic_links.md);
+ * therefore if the entry is a symbolic link,
+ * the returned object contains information for the target entry, not the symbolic link:
*
- * File.stat('t.txt').class # => File::Stat
+ * ```ruby
+ * filepath = 'README.md'
+ * linkpath = 'foo'
+ * File.symlink(filepath, linkpath)
+ * # Method File.stat follows the symlink, so the birthtimes are the same.
+ * File.stat(filepath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * File.stat(linkpath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * # Method File.lstat does not follow the symlink, so the birthtimes are different.
+ * File.lstat(filepath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * File.lstat(linkpath).birthtime # => 2026-09-04 10:29:45.884317953 -0500
+ * File.unlink(linkpath) # Clean up.
+ * ```
*
*/
@@ -1579,24 +1595,24 @@ lstat_without_gvl(const char *path, struct stat *st)
* :markup: markdown
*
* call-seq:
- * File.lstat(path) -> new_stat
+ * File.lstat(path) -> file_stat
*
- * Returns a File::Stat object for the entry at `path`;
- * does not follow symbolic links,
- * and therefore returns the stat object for `path`,
- * regardless of whether it is a symbolic link:
+ * Returns a new File::Stat object for the entry at `path`.
+ * Does not follow [symbolic links](file/symbolic_links.md);
+ * therefore the returned object contains information for the entry at `path`,
+ * regardless of whether is a symbolic link:
*
* ```ruby
- * File.write('t.tmp', '')
- * sleep(1)
- * File.symlink('t.tmp', 'link')
- * file = File.new('link', 'r')
- * # Method stat: follows link to 't.tmp'.
- * file.stat.ctime # => 2026-06-13 15:05:16.996527996 -0500
- * # Method lstat; does not follow link.
- * file.lstat.ctime # => 2026-06-13 15:05:17.997527947 -0500
- * File.delete('t.tmp')
- * File.delete('link')
+ * filepath = 'README.md'
+ * linkpath = 'foo'
+ * File.symlink(filepath, linkpath)
+ * # Method File.stat follows the symlink, so the birthtimes are the same.
+ * File.stat(filepath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * File.stat(linkpath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * # Method File.lstat does not follow the symlink, so the birthtimes are different.
+ * File.lstat(filepath).birthtime # => 2026-09-01 09:09:28.378987388 -0500
+ * File.lstat(linkpath).birthtime # => 2026-09-04 10:29:45.884317953 -0500
+ * File.unlink(linkpath) # Clean up.
* ```
*
*/
diff --git a/gc/default/default.c b/gc/default/default.c
index 4e36f6d399b433..a6afd9e65c97e9 100644
--- a/gc/default/default.c
+++ b/gc/default/default.c
@@ -852,14 +852,35 @@ static void objspace_absorb(rb_objspace_t *dst, rb_objspace_t *src);
static struct heap_page_body *page_pool_acquire(struct page_arena **arena_out);
static void page_pool_release(struct heap_page_body *body, struct page_arena *arena);
+#ifdef HAVE_MMAP
+static void page_pool_release_locked(struct heap_page_body *body, struct page_arena *arena);
+#endif
static void page_pool_reclaim(rb_global_objspace_t *g);
+#if RGENGC_CHECK_MODE && !defined(_WIN32) && !defined(__wasi__) && defined(HAVE_PTHREAD_H)
+# define PAGE_POOL_LOCK_ERRORCHECK 1
+#endif
+
+static void
+page_pool_lock_initialize(rb_nativethread_lock_t *lock)
+{
+#ifdef PAGE_POOL_LOCK_ERRORCHECK
+ pthread_mutexattr_t attr;
+ pthread_mutexattr_init(&attr);
+ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
+ pthread_mutex_init(lock, &attr);
+ pthread_mutexattr_destroy(&attr);
+#else
+ rb_native_mutex_initialize(lock);
+#endif
+}
+
static void
global_objspace_init(void)
{
if (global_objspace == NULL) {
rb_global_objspace_t *g = &rb_global_objspace_instance;
- rb_native_mutex_initialize(&g->page_pool.lock);
+ page_pool_lock_initialize(&g->page_pool.lock);
g->page_pool.hot_list = NULL;
g->page_pool.hot_count = 0;
g->page_pool.arenas = NULL;
@@ -2224,6 +2245,12 @@ heap_page_body_free(struct heap_page_body *page_body, struct page_arena *arena)
page_pool_release(page_body, arena);
}
+#ifdef PAGE_POOL_LOCK_ERRORCHECK
+# define ASSERT_PAGE_POOL_LOCKED(g) GC_ASSERT(pthread_mutex_lock(&(g)->page_pool.lock) == EDEADLK)
+#else
+# define ASSERT_PAGE_POOL_LOCKED(g) ((void)0)
+#endif
+
/* Insert into page_index. Writers serialize on page_pool.lock; lomem and himem are a
* monotonically growing over-approximation used for a quick reject. */
static void
@@ -2259,12 +2286,13 @@ global_page_index_insert(struct heap_page *page)
}
static void
-global_page_index_remove(const struct heap_page *page)
+global_page_index_remove_locked(const struct heap_page *page)
{
rb_global_objspace_t *g = global_objspace;
uintptr_t body = (uintptr_t)page->body;
- rb_native_mutex_lock(&g->page_pool.lock);
+ ASSERT_PAGE_POOL_LOCKED(g);
+
size_t lo = 0, hi = g->page_index.n_pages;
while (lo < hi) {
size_t mid = (lo + hi) / 2;
@@ -2275,6 +2303,15 @@ global_page_index_remove(const struct heap_page *page)
memmove(&g->page_index.pages[lo], &g->page_index.pages[lo + 1],
(g->page_index.n_pages - lo - 1) * sizeof(struct heap_page *));
g->page_index.n_pages--;
+}
+
+static void
+global_page_index_remove(const struct heap_page *page)
+{
+ rb_global_objspace_t *g = global_objspace;
+
+ rb_native_mutex_lock(&g->page_pool.lock);
+ global_page_index_remove_locked(page);
rb_native_mutex_unlock(&g->page_pool.lock);
}
@@ -2287,6 +2324,37 @@ heap_page_free(rb_objspace_t *objspace, struct heap_page *page)
free(page);
}
+static void
+heap_pages_free_batch(rb_objspace_t *objspace, struct heap_page *pages)
+{
+ rb_global_objspace_t *g = global_objspace;
+
+ rb_native_mutex_lock(&g->page_pool.lock);
+ for (struct heap_page *page = pages; page != NULL; page = page->free_next) {
+ global_page_index_remove_locked(page);
+ if (HEAP_PAGE_ALLOC_USE_MMAP) {
+#ifdef HAVE_MMAP
+ page_pool_release_locked(page->body, page->arena);
+#endif
+ }
+ }
+ rb_native_mutex_unlock(&g->page_pool.lock);
+
+ if (!HEAP_PAGE_ALLOC_USE_MMAP) {
+ /* gc_aligned_free does not need the pool lock. */
+ for (struct heap_page *page = pages; page != NULL; page = page->free_next) {
+ heap_page_body_free(page->body, page->arena);
+ }
+ }
+
+ while (pages != NULL) {
+ struct heap_page *next = pages->free_next;
+ objspace->heap_pages.freed_pages++;
+ free(pages);
+ pages = next;
+ }
+}
+
static void
heap_pages_free_unused_pages(rb_objspace_t *objspace)
{
@@ -2296,11 +2364,13 @@ heap_pages_free_unused_pages(rb_objspace_t *objspace)
objspace->empty_pages_count = 0;
size_t i, j;
+ struct heap_page *to_free = NULL;
for (i = j = 0; i < rb_darray_size(objspace->heap_pages.sorted); i++) {
struct heap_page *page = rb_darray_get(objspace->heap_pages.sorted, i);
if (heap_page_in_global_empty_pages_pool(objspace, page) && heap_pages_freeable_pages > 0) {
- heap_page_free(objspace, page);
+ page->free_next = to_free;
+ to_free = page;
heap_pages_freeable_pages--;
}
else {
@@ -2336,6 +2406,8 @@ heap_pages_free_unused_pages(rb_objspace_t *objspace)
heap_pages_lomem = 0;
heap_pages_himem = 0;
}
+
+ heap_pages_free_batch(objspace, to_free);
}
}
@@ -2529,6 +2601,33 @@ page_pool_acquire(struct page_arena **arena_out)
return body;
}
+#ifdef HAVE_MMAP
+static void
+page_pool_release_locked(struct heap_page_body *body, struct page_arena *arena)
+{
+ rb_global_objspace_t *g = global_objspace;
+
+ ASSERT_PAGE_POOL_LOCKED(g);
+
+ /* A body in the empty-pages pool stays fully poisoned (see gc_sweep_page), so
+ * unpoison the scratch area (link + arena tag) before writing. */
+ asan_unpoison_memory_region(body, PAGE_POOL_SCRATCH_SIZE, false);
+ arena->free_count++;
+ PAGE_POOL_BODY_ARENA(body) = arena;
+ if (g->page_pool.hot_count < PAGE_POOL_HOT_MAX) {
+ *(uintptr_t *)body = (uintptr_t)g->page_pool.hot_list;
+ g->page_pool.hot_list = body;
+ g->page_pool.hot_count++;
+ }
+ else {
+ *(uintptr_t *)body = (uintptr_t)arena->cold_freelist;
+ arena->cold_freelist = body;
+ arena->cold_count++;
+ }
+ asan_poison_memory_region(body, HEAP_PAGE_SIZE);
+}
+#endif
+
static void
page_pool_release(struct heap_page_body *body, struct page_arena *arena)
{
@@ -2537,22 +2636,7 @@ page_pool_release(struct heap_page_body *body, struct page_arena *arena)
rb_global_objspace_t *g = global_objspace;
rb_native_mutex_lock(&g->page_pool.lock);
- /* A body in the empty-pages pool stays fully poisoned (see gc_sweep_page), so
- * unpoison the scratch area (link + arena tag) before writing. */
- asan_unpoison_memory_region(body, PAGE_POOL_SCRATCH_SIZE, false);
- arena->free_count++;
- PAGE_POOL_BODY_ARENA(body) = arena;
- if (g->page_pool.hot_count < PAGE_POOL_HOT_MAX) {
- *(uintptr_t *)body = (uintptr_t)g->page_pool.hot_list;
- g->page_pool.hot_list = body;
- g->page_pool.hot_count++;
- }
- else {
- *(uintptr_t *)body = (uintptr_t)arena->cold_freelist;
- arena->cold_freelist = body;
- arena->cold_count++;
- }
- asan_poison_memory_region(body, HEAP_PAGE_SIZE);
+ page_pool_release_locked(body, arena);
rb_native_mutex_unlock(&g->page_pool.lock);
#endif
}
@@ -12428,7 +12512,7 @@ rb_gc_impl_after_fork(void *objspace_ptr, rb_pid_t pid)
heap_alloc_state_clear(objspace);
/* The forking Ractor becomes the child process's main Ractor. */
global_objspace->main_objspace = objspace;
- rb_native_mutex_initialize(&rb_global_objspace_instance.page_pool.lock);
+ page_pool_lock_initialize(&rb_global_objspace_instance.page_pool.lock);
}
}
diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb
index 59e54c819de0bd..8050c0a3f2421b 100644
--- a/lib/bundler/fetcher/downloader.rb
+++ b/lib/bundler/fetcher/downloader.rb
@@ -51,6 +51,11 @@ def fetch(uri, headers = {}, counter = 0)
response
when Gem::Net::HTTPRedirection
new_uri = Gem::URI.parse(response["location"])
+ # Following a downgrade would put the credentials this request carries
+ # on a plaintext connection, so refuse it like Gem::RemoteFetcher does.
+ if https?(uri) && !https?(new_uri)
+ raise HTTPError, "Redirecting to a non-https URI is not allowed: #{URICredentialsFilter.credential_filtered_uri(new_uri)}"
+ end
if new_uri.host == uri.host
new_uri.user = uri.user
new_uri.password = uri.password
@@ -120,6 +125,10 @@ def network_down_error(uri, filtered_uri)
"connection and try again.")
end
+ def https?(uri)
+ uri.scheme&.casecmp("https")&.zero?
+ end
+
def validate_uri_scheme!(uri)
return if /\Ahttps?\z/.match?(uri.scheme)
raise InvalidOption,
diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb
index 291cc333819df7..a468827bd5d206 100644
--- a/lib/net/http/header.rb
+++ b/lib/net/http/header.rb
@@ -685,7 +685,7 @@ def content_length=(len)
end
# Returns +true+ if field 'Transfer-Encoding'
- # exists and has value 'chunked',
+ # exists and has 'chunked' as its final transfer coding,
# +false+ otherwise;
# see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]:
#
@@ -693,10 +693,13 @@ def content_length=(len)
# res['Transfer-Encoding'] # => "chunked"
# res.chunked? # => true
#
+ # 'gzip, chunked' is chunked but 'chunked, gzip' is not,
+ # because only the final transfer coding frames the message.
+ # See {RFC 9112 Section 6.3}[https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3].
def chunked?
return false unless @header['transfer-encoding']
field = self['Transfer-Encoding']
- (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
+ (/(?:\A|[^\-\w])chunked(?![\-\w])\s*(?:,\s*)*\z/i =~ field) ? true : false
end
# Returns a Range object representing the value of field
diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb
index db719161752e91..4395e11188810f 100644
--- a/lib/net/http/response.rb
+++ b/lib/net/http/response.rb
@@ -616,15 +616,20 @@ def read_body_0(dest)
@socket = inflate_body_io
- clen = content_length()
- if clen
- @socket.read clen, dest, @ignore_eof
- return
- end
- clen = range_length()
- if clen
- @socket.read clen, dest
- return
+ # Transfer-Encoding overrides Content-Length, so a body that is not
+ # chunk-framed runs until the server closes the connection.
+ # See RFC 9112 Section 6.3.
+ unless @header['transfer-encoding']
+ clen = content_length()
+ if clen
+ @socket.read clen, dest, @ignore_eof
+ return
+ end
+ clen = range_length()
+ if clen
+ @socket.read clen, dest
+ return
+ end
end
@socket.read_all dest
end
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index 8e4ab4a4b8a9ff..035f63e312cdc9 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -511,7 +511,7 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
if entry.symlink?
link_target = entry.header.linkname
- real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination))
+ real_destination = File.expand_path(link_target, File.dirname(destination))
raise Gem::Package::SymlinkError.new(full_name, real_destination, destination_dir) unless
normalize_path(real_destination).start_with? normalize_path(destination_dir + "/")
diff --git a/marshal.c b/marshal.c
index a9fd5aeb918d47..ec081e78b9e6b5 100644
--- a/marshal.c
+++ b/marshal.c
@@ -1971,6 +1971,11 @@ r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE klass, VALUE ex
if (TYPE(v) != TYPE(tmp)) goto format_error;
}
+ if (RB_TYPE_P(v, T_STRUCT) &&
+ RSTRUCT_LEN_RAW(v) != RARRAY_LEN(rb_struct_s_members(c))) {
+ rb_raise(rb_eTypeError, "struct %"PRIsVALUE" not compatible (struct size differs)",
+ rb_class_name(c));
+ }
RBASIC_SET_CLASS(v, c);
}
break;
diff --git a/parse.y b/parse.y
index a5bbb047798222..fee9970de988bf 100644
--- a/parse.y
+++ b/parse.y
@@ -517,6 +517,14 @@ struct parser_params {
/* track the nest level of only braces "{}" */
int brace_nest;
} lex;
+ /* Cache the tail of the and/or chain most recently built by logop(), so a
+ * left-associative chain does not rescan its right branch on every
+ * operator. head is the chain's top node, tail the node whose nd_2nd is the
+ * next insertion point. */
+ struct {
+ NODE *head;
+ NODE *tail;
+ } logop;
stack_type cond_stack;
stack_type cmdarg_stack;
int tokidx;
@@ -14341,25 +14349,70 @@ new_unless(struct parser_params *p, NODE *cc, NODE *left, NODE *right, const YYL
#define NEW_AND_OR(type, f, s, loc, op_loc) (type == NODE_AND ? NEW_AND(f,s,loc,op_loc) : NEW_OR(f,s,loc,op_loc))
+/* A cached logop tail is usable only if it is still a node of the chain's type
+ * whose nd_2nd is the insertion point, i.e. not itself another node of that
+ * type. This rejects a stale cache left over from an earlier parse. */
+static int
+logop_valid_tail(NODE *tail, enum node_type type)
+{
+ NODE *second;
+ return tail && nd_type_p(tail, type) &&
+ ((second = RNODE_AND(tail)->nd_2nd) == 0 || !nd_type_p(second, type));
+}
+
static NODE*
logop(struct parser_params *p, ID id, NODE *left, NODE *right,
const YYLTYPE *op_loc, const YYLTYPE *loc)
{
enum node_type type = id == idAND || id == idANDOP ? NODE_AND : NODE_OR;
NODE *op;
+ /* Snapshot the cache from the previous logop() call before overwriting it. */
+ NODE *prev_head = p->logop.head;
+ NODE *prev_tail = p->logop.tail;
+ NODE *node, *second;
value_expr(p, left);
if (left && nd_type_p(left, type)) {
- NODE *node = left, *second;
- while ((second = RNODE_AND(node)->nd_2nd) != 0 && nd_type_p(second, type)) {
- node = second;
+ /* The insertion point is the far end of left's right branch.
+ * Reusing the cached tail keeps a chain such as `a && a && ... && a`
+ * linear instead of rescanning the whole branch on every operator. The
+ * cache is validated (its nd_2nd is not another node of the same type)
+ * so a stale entry falls back to the scan. */
+ if (left == prev_head && logop_valid_tail(prev_tail, type)) {
+ node = prev_tail;
+ }
+ else {
+ node = left;
+ while ((second = RNODE_AND(node)->nd_2nd) != 0 && nd_type_p(second, type)) {
+ node = second;
+ }
}
+ second = RNODE_AND(node)->nd_2nd;
RNODE_AND(node)->nd_2nd = NEW_AND_OR(type, second, right, loc, op_loc);
nd_set_line(RNODE_AND(node)->nd_2nd, op_loc->beg_pos.lineno);
left->nd_loc.end_pos = loc->end_pos;
+ p->logop.head = left;
+ p->logop.tail = RNODE_AND(node)->nd_2nd;
return left;
}
op = NEW_AND_OR(type, left, right, loc, op_loc);
nd_set_line(op, op_loc->beg_pos.lineno);
+ /* Record where the next operator will extend op, mirroring the scan the
+ * chained branch above would perform: if right is itself a chain of the
+ * same type (from parentheses), its far end; otherwise op itself. */
+ p->logop.head = op;
+ if (right == prev_head && logop_valid_tail(prev_tail, type)) {
+ p->logop.tail = prev_tail;
+ }
+ else if (right && nd_type_p(right, type)) {
+ node = right;
+ while ((second = RNODE_AND(node)->nd_2nd) != 0 && nd_type_p(second, type)) {
+ node = second;
+ }
+ p->logop.tail = node;
+ }
+ else {
+ p->logop.tail = op;
+ }
return op;
}
diff --git a/ractor.c b/ractor.c
index 5311c4b96ba861..6d60d6e6af6869 100644
--- a/ractor.c
+++ b/ractor.c
@@ -1181,7 +1181,7 @@ rb_ractor_terminate_all(void)
// wait for 1sec
rb_vm_ractor_blocking_cnt_inc(vm, cr, __FILE__, __LINE__);
rb_del_running_thread(rb_ec_thread_ptr(cr->threads.running_ec));
- rb_vm_cond_timedwait(vm, &vm->ractor.sync.terminate_cond, 1000 /* ms */);
+ rb_ractor_sched_wait_terminate(vm, &vm->ractor.sync.terminate_cond, 1000 /* ms */);
while (vm->ractor.sched.barrier_is_waiting) {
// A barrier is waiting. Threads relinquish the VM lock before joining the barrier and
// since we just acquired the VM lock back, we're blocking other threads from joining it.
diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb
index a3390ac5276fc0..a7f642f456e67e 100644
--- a/spec/bundler/bundler/fetcher/downloader_spec.rb
+++ b/spec/bundler/bundler/fetcher/downloader_spec.rb
@@ -59,6 +59,19 @@
subject.fetch(uri, options, counter)
end
end
+
+ context "when the redirect uri downgrades https to http" do
+ let(:uri) { Gem::URI("https://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ before { http_response["location"] = "http://www.uri-to-fetch.com/api/v2/endpoint" }
+
+ it "should raise a Bundler::HTTPError instead of following the redirect" do
+ expect(subject).to receive(:fetch).with(uri, options, 0).and_call_original
+ expect(subject).not_to receive(:fetch).with(Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 1)
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError,
+ "Redirecting to a non-https URI is not allowed: http://www.uri-to-fetch.com/api/v2/endpoint")
+ end
+ end
end
context "when the request response is a Gem::Net::HTTPSuccess" do
diff --git a/string.c b/string.c
index 9b5667f3a26813..4327492619f7d7 100644
--- a/string.c
+++ b/string.c
@@ -4846,7 +4846,7 @@ str_rindex(VALUE str, VALUE sub, const char *s, rb_encoding *enc)
c = *t & 0xff;
searchlen = s - sbeg + 1;
- if (memcmp(s, t, slen) == 0) {
+ if (s + slen <= e && memcmp(s, t, slen) == 0) {
return s - sbeg;
}
@@ -4858,7 +4858,7 @@ str_rindex(VALUE str, VALUE sub, const char *s, rb_encoding *enc)
searchlen = adjusted - sbeg;
continue;
}
- if (memcmp(hit, t, slen) == 0)
+ if (hit + slen <= e && memcmp(hit, t, slen) == 0)
return hit - sbeg;
searchlen = adjusted - sbeg;
} while (searchlen > 0);
@@ -4883,16 +4883,20 @@ rb_str_rindex(VALUE str, VALUE sub, long pos)
/* substring longer than string */
if (len < slen) return -1;
+ /* character counts, so the byte tail can still be shorter than sub */
if (len - pos < slen) pos = len - slen;
if (len == 0) return pos;
sbeg = RSTRING_PTR(str);
if (pos == 0) {
- if (memcmp(sbeg, RSTRING_PTR(sub), RSTRING_LEN(sub)) == 0)
+ if (RSTRING_LEN(sub) <= RSTRING_LEN(str) &&
+ memcmp(sbeg, RSTRING_PTR(sub), RSTRING_LEN(sub)) == 0) {
return 0;
- else
+ }
+ else {
return -1;
+ }
}
s = str_nth(sbeg, RSTRING_END(str), pos, enc, singlebyte);
@@ -10604,7 +10608,7 @@ rb_str_enumerate_lines(int argc, VALUE *argv, VALUE str, VALUE ary)
subptr = hit;
}
- if (subptr != pend) {
+ if (subptr < pend) {
if (chomp) {
if (rsnewline) {
pend = chomp_newline(subptr, pend, enc);
@@ -11124,6 +11128,8 @@ smart_chomp(VALUE str, const char *e, const char *p)
{
rb_encoding *enc = rb_enc_get(str);
if (rb_enc_mbminlen(enc) > 1) {
+ /* a receiver shorter than one character has nothing to chomp */
+ if (e - p < rb_enc_mbminlen(enc)) return e - p;
const char *pp = rb_enc_left_char_head(p, e-rb_enc_mbminlen(enc), e, enc);
if (rb_enc_is_newline(pp, e, enc)) {
e = pp;
@@ -11171,7 +11177,7 @@ chompped_length(VALUE str, VALUE rs)
RSTRING_GETMEM(rs, rsptr, rslen);
if (rslen == 0) {
if (rb_enc_mbminlen(enc) > 1) {
- while (e > p) {
+ while (e - p >= rb_enc_mbminlen(enc)) {
pp = rb_enc_left_char_head(p, e-rb_enc_mbminlen(enc), e, enc);
if (!rb_enc_is_newline(pp, e, enc)) break;
e = pp;
@@ -12309,10 +12315,12 @@ rb_str_partition(VALUE str, VALUE sep)
pos = rb_str_index(str, sep, 0);
if (pos < 0) goto failed;
}
+
+ long rpos = pos + RSTRING_LEN(sep);
+ if (rpos > RSTRING_LEN(str)) goto failed;
return rb_ary_new3(3, rb_str_subseq(str, 0, pos),
sep,
- rb_str_subseq(str, pos+RSTRING_LEN(sep),
- RSTRING_LEN(str)-pos-RSTRING_LEN(sep)));
+ rb_str_subseq(str, rpos, RSTRING_LEN(str)-rpos));
failed:
return rb_ary_new3(3, str_duplicate(rb_cString, str), str_new_empty_String(str), str_new_empty_String(str));
@@ -12349,10 +12357,11 @@ rb_str_rpartition(VALUE str, VALUE sep)
}
}
+ long rpos = pos + RSTRING_LEN(sep);
+ if (rpos > RSTRING_LEN(str)) goto failed;
return rb_ary_new3(3, rb_str_subseq(str, 0, pos),
sep,
- rb_str_subseq(str, pos+RSTRING_LEN(sep),
- RSTRING_LEN(str)-pos-RSTRING_LEN(sep)));
+ rb_str_subseq(str, rpos, RSTRING_LEN(str)-rpos));
failed:
return rb_ary_new3(3, str_new_empty_String(str), str_new_empty_String(str), str_duplicate(rb_cString, str));
}
diff --git a/test/net/http/test_httpheader.rb b/test/net/http/test_httpheader.rb
index 3c09f4c194a41e..b86d48681e1cb5 100644
--- a/test/net/http/test_httpheader.rb
+++ b/test/net/http/test_httpheader.rb
@@ -384,6 +384,16 @@ def test_chunked?
try_chunked false, 'chunked-but-not-chunked'
end
+ def test_chunked_final_transfer_coding
+ try_chunked true, 'gzip, chunked'
+ try_chunked true, 'chunked,'
+ try_chunked true, 'gzip , chunked , '
+
+ try_chunked false, 'chunked, gzip'
+ try_chunked false, 'chunked, identity'
+ try_chunked false, 'gzip'
+ end
+
def try_chunked(bool, str)
@c['transfer-encoding'] = str
assert_equal bool, @c.chunked?
diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb
index e9ed1213516210..e845f80fb5af25 100644
--- a/test/net/http/test_httpresponse.rb
+++ b/test/net/http/test_httpresponse.rb
@@ -113,6 +113,73 @@ def test_read_body
assert_equal 'hello', body
end
+ def test_read_body_chunked_is_final_transfer_coding
+ io = dummy_io(< 90
+
+ tgz_io = util_tar_gz do |tar|
+ tar.mkdir "lib", 0o755
+ tar.add_symlink "lib/link", File.join(destination_subdir, ".."), 0o644
+ tar.add_file "lib/link/outside.txt", 0o644 do |io|
+ io.write "hi"
+ end
+ end
+
+ e = assert_raise(Gem::Package::SymlinkError) do
+ package.extract_tar_gz tgz_io, destination_subdir
+ end
+
+ assert_equal("installing symlink 'lib/link' pointing to parent path #{@destination} of " \
+ "#{destination_subdir} is not allowed", e.message)
+
+ assert_path_not_exist File.join(@destination, "outside.txt")
+ assert_path_not_exist File.join(destination_subdir, "lib/link")
+ end
+
def test_extract_symlink_parent_doesnt_delete_user_dir
package = Gem::Package.new @gem
diff --git a/thread.c b/thread.c
index 4736a8cd67ae75..8310529a38b941 100644
--- a/thread.c
+++ b/thread.c
@@ -573,7 +573,12 @@ rb_thread_free_native_thread(void *th_ptr)
{
rb_thread_t *th = th_ptr;
- native_thread_destroy_atfork(th->nt);
+ // A thread with a coroutine context does not own its native thread: that
+ // one is in the shared pool, listed there and with its altstack registered
+ // on whichever pthread is running this. See rb_threadptr_sched_free().
+ if (th->sched.context == NULL) {
+ native_thread_destroy_atfork(th->nt);
+ }
th->nt = NULL;
}
diff --git a/thread_none.c b/thread_none.c
index 7de59b6f9af6f2..58f43796eb06d4 100644
--- a/thread_none.c
+++ b/thread_none.c
@@ -291,6 +291,12 @@ rb_del_running_thread(rb_thread_t *th)
// do nothing
}
+void
+rb_ractor_sched_wait_terminate(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
+{
+ // do nothing: with no threads there is no other Ractor to wait for
+}
+
void
rb_threadptr_sched_free(rb_thread_t *th)
{
diff --git a/thread_pthread.c b/thread_pthread.c
index 8f6a7b11667b4b..0aaaf5de6f852f 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -637,6 +637,29 @@ static struct {
VALUE *stack_start;
} native_main_thread;
+// Whether the calling native thread may leave the shared pool. The process's
+// main pthread cannot: under RUBY_MN_THREADS=2 its loop runs on a stack only
+// it could free (thread_sched_main_to_shared). By pthread_self(), not
+// nt->thread_id, which pthread_create may still be writing.
+static bool
+native_thread_self_can_retire_p(void)
+{
+ return !pthread_equal(pthread_self(), native_main_thread.id);
+}
+
+#if defined(HAVE_WORKING_FORK)
+// The forking thread's pthread is the child's only one, hence its main one
+static void
+native_main_thread_atfork(void)
+{
+ native_main_thread.id = pthread_self();
+ // The stack recorded here is the parent's initial one; this thread's is
+ // another. Nothing reads it for a thread that is already running, and
+ // saying "unknown" beats saying the wrong bounds.
+ native_main_thread.stack_maxsize = 0;
+}
+#endif
+
#ifdef STACK_END_ADDRESS
extern void *STACK_END_ADDRESS;
#endif
@@ -739,21 +762,24 @@ native_thread_init_stack(rb_thread_t *th, void *local_in_parent_frame)
native_thread_init_main_thread_stack(local_in_parent_frame);
}
- if (pthread_equal(curr, native_main_thread.id)) {
+ if (th->sched.context != NULL) {
+ // an M:N thread runs on the pool stack native_thread_create_shared
+ // recorded, whichever native thread (the main one included) hosts it.
+ // Not by nt->dedicated: a RESUMED hook may have pinned the nt already.
+ }
+ else if (pthread_equal(curr, native_main_thread.id)) {
th->ec->machine.stack_start = native_main_thread.stack_start;
th->ec->machine.stack_maxsize = native_main_thread.stack_maxsize;
}
else {
#ifdef STACKADDR_AVAILABLE
- if (th_has_dedicated_nt(th)) {
- void *start;
- size_t size;
-
- if (get_stack(&start, &size) == 0) {
- uintptr_t diff = (uintptr_t)start - (uintptr_t)local_in_parent_frame;
- th->ec->machine.stack_start = local_in_parent_frame;
- th->ec->machine.stack_maxsize = size - diff;
- }
+ void *start;
+ size_t size;
+
+ if (get_stack(&start, &size) == 0) {
+ uintptr_t diff = (uintptr_t)start - (uintptr_t)local_in_parent_frame;
+ th->ec->machine.stack_start = local_in_parent_frame;
+ th->ec->machine.stack_maxsize = size - diff;
}
#else
rb_raise(rb_eNotImpError, "ruby engine can initialize only in the main thread");
@@ -1031,6 +1057,13 @@ native_set_thread_name(rb_thread_t *th)
{
#ifdef SET_CURRENT_THREAD_NAME
VALUE loc;
+
+ // An M:N thread does not own the native thread it runs on: naming it here
+ // would name whichever nt started it (under RUBY_MN_THREADS=2 that can be
+ // the process's main one, whose name is the process's). Thread#name= is
+ // skipped for the same reason (rb_thread_setname).
+ if (!th->has_dedicated_nt) return;
+
if (!NIL_P(loc = th->name)) {
SET_CURRENT_THREAD_NAME(RSTRING_PTR(loc));
}
diff --git a/thread_sched.c b/thread_sched.c
index a7d89cc1316eef..7bceb1f66cc934 100644
--- a/thread_sched.c
+++ b/thread_sched.c
@@ -104,9 +104,29 @@ static bool timeslice_scan(rb_vm_t *vm, bool interrupt);
static void timer_thread_wakeup(void);
static void timer_thread_wakeup_locked(rb_vm_t *vm);
static void timer_thread_wakeup_force(void);
+// RUBY_MN_THREADS: -1 = nothing is M:N, not even a Ractor's threads;
+// 0 = the default (a Ractor's threads are, the main Ractor's are not);
+// 1 = the main Ractor's threads too; 2 = the main thread as well.
+static int mn_threads_mode = 0;
+
+// Only consulted from USE_MN_THREADS code; the platform gate is not defined yet here.
+static bool
+mn_threads_enabled_p(void)
+{
+ return mn_threads_mode >= 0;
+}
+
+static void nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt);
+static void nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt);
+static bool nt_shared_loop(struct rb_native_thread *nt);
+static bool native_thread_self_can_retire_p(void);
#include THREAD_IMPL_SRC
+#if USE_MN_THREADS
+static void thread_sched_main_to_shared(rb_thread_t *th);
+#endif
+
// Defaults for what the platform above did not opt out of.
#ifndef RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF
@@ -1151,7 +1171,9 @@ rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
ccan_list_node_init(&sched->timeslice_node);
#if USE_MN_THREADS
- if (!atfork) sched->enable_mn_threads = true; // MN is enabled on Ractors
+ // A Ractor's threads are M:N unless RUBY_MN_THREADS turns it off entirely;
+ // the main Ractor's setting is decided in ruby_mn_threads_params().
+ if (!atfork) sched->enable_mn_threads = mn_threads_enabled_p();
#endif
}
@@ -1206,7 +1228,9 @@ thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th)
struct rb_native_thread *nt = cth->nt;
native_thread_assign(NULL, cth);
RUBY_DEBUG_LOG("th:%u->%u on nt:%d", rb_th_serial(cth), rb_th_serial(next_th), nt->serial);
- thread_sched_switch0(cth->sched.context, next_th, nt, cth->status == THREAD_KILLED);
+ // never final: a thread ends only through co_start's epilogue transfer. The
+ // main thread is THREAD_KILLED (rb_ec_cleanup) while it still parks here.
+ thread_sched_switch0(cth->sched.context, next_th, nt, false);
}
#if VM_CHECK_MODE > 0
@@ -1344,7 +1368,7 @@ ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r)
#define SNT_KEEP_MINIMUM (MINIMUM_SNT > 1 ? MINIMUM_SNT : 1)
static rb_ractor_t *
-ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr)
+ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr, bool can_retire)
{
rb_ractor_t *r;
int idle_streak = 0; // consecutive pops that found the queue empty
@@ -1360,7 +1384,7 @@ ractor_sched_deq(rb_vm_t *vm, rb_ractor_t *cr)
while ((r = ccan_list_pop(&vm->ractor.sched.grq, rb_ractor_t, threads.sched.grq_node)) == NULL) {
RUBY_DEBUG_LOG("wait grq_cnt:%d", (int)vm->ractor.sched.grq_cnt);
- if (SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
+ if (can_retire && SNT_IDLE_RETIRE >= 0 && ++idle_streak > SNT_IDLE_RETIRE &&
(int)RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) > SNT_KEEP_MINIMUM) {
RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
RUBY_DEBUG_LOG("retire, snt_cnt:%d", (int)vm->ractor.sched.snt_cnt);
@@ -1753,6 +1777,9 @@ thread_sched_atfork(struct rb_thread_sched *sched)
rb_native_mutex_initialize(&vm->ractor.sched.timeslice.lock);
ccan_list_head_init(&vm->ractor.sched.timeslice.scheds);
rb_native_mutex_initialize(&th->nt->running_th_lock); // a scan could hold it at fork
+ th->nt->running_th = NULL; // th re-records itself below
+ th->nt->retiring = false; // the pool it had no room in is gone
+ native_main_thread_atfork(); // this pthread is the process's main one now
// Fork can copy nodes linked (or torn mid-link); re-init every sched's
// node so rb_thread_sched_destroy's del_init stays a no-op for them.
rb_ractor_t *r;
@@ -1800,14 +1827,20 @@ ruby_mn_threads_params(void)
rb_vm_t *vm = GET_VM();
rb_ractor_t *main_ractor = GET_RACTOR();
+ // RUBY_MN_THREADS: -1 = nothing is M:N, 0 = the default, 1 = the main
+ // Ractor's threads too, 2 = the main thread as well (see
+ // thread_sched_main_to_shared). The main Ractor's sched already exists
+ // here, so it is set rather than defaulted.
const char *mn_threads_cstr = getenv("RUBY_MN_THREADS");
- bool enable_mn_threads = false;
+ int mn_threads = (USE_MN_THREADS && mn_threads_cstr) ? atoi(mn_threads_cstr) : 0;
- if (USE_MN_THREADS && mn_threads_cstr && (enable_mn_threads = atoi(mn_threads_cstr) > 0)) {
- // enabled
- ruby_mn_threads_enabled = 1;
+ mn_threads_mode = mn_threads;
+ if (mn_threads > 0) {
+ ruby_mn_threads_enabled = mn_threads;
}
- main_ractor->threads.sched.enable_mn_threads = enable_mn_threads;
+ // =2 publishes it from thread_sched_main_to_shared, with the main nt's
+ // pool entry, so that the timer thread cannot mint an snt in between
+ main_ractor->threads.sched.enable_mn_threads = mn_threads == 1;
const char *max_cpu_cstr = getenv("RUBY_MAX_CPU");
int max_cpu = native_thread_default_max_cpu();
@@ -1820,6 +1853,12 @@ ruby_mn_threads_params(void)
}
vm->ractor.sched.max_cpu = max_cpu;
+
+#if USE_MN_THREADS
+ if (mn_threads >= 2) {
+ thread_sched_main_to_shared(GET_THREAD());
+ }
+#endif
}
static void
@@ -1857,10 +1896,13 @@ native_thread_dedicated_dec(rb_vm_t *vm, rb_ractor_t *cr, struct rb_native_threa
if (nt->dedicated == 0) {
// Rejoin under the max_cpu cap; with no room this nt retires and
- // belongs to neither count until it ends.
+ // belongs to neither count until it ends. The process's main nt
+ // cannot end (its loop runs on a stack it would have to free), so
+ // it always rejoins.
+ const bool can_retire = native_thread_self_can_retire_p();
while (1) {
rb_atomic_t snt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
- if (snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
+ if (!can_retire || snt < vm->ractor.sched.max_cpu || (int)snt <= MINIMUM_SNT) {
if (RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt, snt + 1) == snt) break;
}
else {
@@ -1958,123 +2000,240 @@ nt_start(void *ptr)
RUBY_DEBUG_LOG("nt:%u", nt->serial);
- bool in_snts = false;
+ if (nt->dedicated) {
+ // wait running turn
+ rb_thread_t *th = nt->running_thread;
+ struct rb_thread_sched *sched = TH_SCHED(th);
- if (!nt->dedicated) {
- coroutine_initialize_main(nt->nt_context);
+ RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
+ ruby_thread_set_native(th);
- // join the snt list that the barrier/timeslice scans walk
- rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
+ thread_sched_lock(sched, th);
{
- ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
+ if (sched->running == th) {
+ thread_sched_add_running_thread(sched, th);
+ }
+ thread_sched_wait_running_turn(sched, th, false, NULL);
}
- rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
- in_snts = true;
+ thread_sched_unlock(sched, th);
+
+ // start threads
+ call_thread_start_func_2(th);
+ // TODO: allow to change to the SNT
}
+ else {
+ coroutine_initialize_main(nt->nt_context);
+ nt_snts_join(vm, nt);
- bool retired = false;
+ bool retired = nt_shared_loop(nt);
+ nt_snts_leave(vm, nt);
- while (1) {
- if (nt->dedicated) {
- // wait running turn
- rb_thread_t *th = nt->running_thread;
- struct rb_thread_sched *sched = TH_SCHED(th);
+ if (retired) {
+ // The counts dropped this nt already; nothing can reference it now.
+ RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
+ native_thread_destroy_self(nt);
+ }
+ }
- RUBY_DEBUG_LOG("on dedicated th:%u", rb_th_serial(th));
- ruby_thread_set_native(th);
+ return NULL;
+}
- thread_sched_lock(sched, th);
- {
- if (sched->running == th) {
- thread_sched_add_running_thread(sched, th);
- }
- thread_sched_wait_running_turn(sched, th, false, NULL);
- }
- thread_sched_unlock(sched, th);
+// join the snt list that the barrier/timeslice scans walk
+static void
+nt_snts_join(rb_vm_t *vm, struct rb_native_thread *nt)
+{
+ rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
+ {
+ ccan_list_add(&vm->ractor.sched.ntlist.snts, &nt->snts_node);
+ }
+ rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
+}
+
+// Leaving the shared loop: every path back here deregistered first
+// (park and death both precede the transfer), so only the snts entry
+// is left to remove.
+static void
+nt_snts_leave(rb_vm_t *vm, struct rb_native_thread *nt)
+{
+ VM_ASSERT(nt->running_th == NULL);
+ rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
+ {
+ ccan_list_del_init(&nt->snts_node);
+ }
+ rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
+}
+
+// The shared nt's scheduling loop: serve Ractors from the grq until this nt
+// retires (returns true) or goes dedicated while running (returns false).
+static bool
+nt_shared_loop(struct rb_native_thread *nt)
+{
+ rb_vm_t *vm = nt->vm;
- // start threads
- call_thread_start_func_2(th);
- break; // TODO: allow to change to the SNT
+ while (1) {
+ RUBY_DEBUG_LOG("check next");
+ if (nt->retiring) { // came back with no room in the shared pool
+ return true;
}
- else {
- RUBY_DEBUG_LOG("check next");
- if (nt->retiring) { // came back with no room in the shared pool
- retired = true;
- break;
- }
- rb_ractor_t *r = ractor_sched_deq(vm, NULL);
+ // asked every time: a fork leaves this loop's native thread as the
+ // child's main one, which may not retire
+ rb_ractor_t *r = ractor_sched_deq(vm, NULL, native_thread_self_can_retire_p());
- if (r) {
- struct rb_thread_sched *sched = &r->threads.sched;
+ if (r) {
+ struct rb_thread_sched *sched = &r->threads.sched;
- bool locked = true;
+ bool locked = true;
- thread_sched_lock(sched, NULL);
- {
- rb_thread_t *next_th = sched->running;
+ thread_sched_lock(sched, NULL);
+ {
+ rb_thread_t *next_th = sched->running;
- if (next_th && next_th->nt == NULL) {
- RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
+ if (next_th && next_th->nt == NULL) {
+ RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial);
#if USE_MN_THREADS
- thread_sched_switch0(nt->nt_context, next_th, nt, false);
-
- // If a coroutine terminated during the transfer, co_start
- // recorded it in nt->dead_co (switch0's return value is
- // backend-dependent, unusable; see thread_pthread.h).
- struct coroutine_context *dead_co = nt->dead_co;
- nt->dead_co = NULL;
- if (thread_sched_reclaim(dead_co)) {
- // it already released the sched lock before its
- // transfer (its Ractor may be gone): leave sched be.
- locked = false;
- }
+ thread_sched_switch0(nt->nt_context, next_th, nt, false);
+
+ // If a coroutine terminated during the transfer, co_start
+ // recorded it in nt->dead_co (switch0's return value is
+ // backend-dependent, unusable; see thread_pthread.h).
+ struct coroutine_context *dead_co = nt->dead_co;
+ nt->dead_co = NULL;
+ if (thread_sched_reclaim(dead_co)) {
+ // it already released the sched lock before its
+ // transfer (its Ractor may be gone): leave sched be.
+ locked = false;
+ }
#else
- thread_sched_switch0(nt->nt_context, next_th, nt, false);
+ thread_sched_switch0(nt->nt_context, next_th, nt, false);
#endif
- }
- else {
- RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
- }
}
- if (locked) {
- thread_sched_unlock(sched, NULL);
+ else {
+ RUBY_DEBUG_LOG("no schedulable threads -- next_th:%p", next_th);
}
}
- else {
- // ractor_sched_deq retired this nt.
- retired = true;
- break;
+ if (locked) {
+ thread_sched_unlock(sched, NULL);
}
+ }
+ else {
+ // ractor_sched_deq retired this nt.
+ return true;
+ }
- if (nt->dedicated) {
- // SNT becomes DNT while running
- break;
- }
+ if (nt->dedicated) {
+ // SNT becomes DNT while running
+ return false;
}
}
+}
- if (in_snts) {
- // Leaving the shared loop: every path back here deregistered first
- // (park and death both precede the transfer), so only the snts entry
- // is left to remove.
- VM_ASSERT(nt->running_th == NULL);
- rb_native_mutex_lock(&vm->ractor.sched.ntlist.lock);
- {
- ccan_list_del_init(&nt->snts_node);
- }
- rb_native_mutex_unlock(&vm->ractor.sched.ntlist.lock);
+#if USE_MN_THREADS
+// The scheduling loop of the process's main nt, entered when RUBY_MN_THREADS=2
+// turned it shared: the process stack belongs to the main thread's context,
+// so this loop runs on a coroutine of its own (thread_sched_main_to_shared).
+static COROUTINE
+nt_loop_co(struct coroutine_context *from, struct coroutine_context *self)
+{
+#ifdef RUBY_ASAN_ENABLED
+ __sanitizer_finish_switch_fiber(self->fake_stack,
+ (const void**)&from->stack_base, &from->stack_size);
+#endif
+ struct rb_native_thread *nt = (struct rb_native_thread *)self->argument;
+
+ // The first entry is a transfer that in nt_shared_loop would return from
+ // thread_sched_switch0: a parked thread left its sched lock held for the
+ // loop to release, or a terminated one (dead_co) released it itself.
+ struct coroutine_context *dead_co = nt->dead_co;
+ nt->dead_co = NULL;
+ if (!thread_sched_reclaim(dead_co)) {
+ rb_thread_t *parked_th = (rb_thread_t *)from->argument;
+ thread_sched_unlock(TH_SCHED(parked_th), NULL);
}
- if (retired) {
- // The counts dropped this nt already; nothing can reference it now.
- RUBY_DEBUG_LOG("retired nt:%u", nt->serial);
- native_thread_destroy_self(nt);
+ // as after a switch0 return: the thread may have pinned this nt
+ // (rb_thread_lock_native_thread) before it ended
+ if (!nt->dedicated) {
+ if (nt_shared_loop(nt)) rb_bug("main nt retired"); // native_thread_self_can_retire_p() is false here
}
+ nt_snts_leave(nt->vm, nt);
- return NULL;
+ // Went dedicated while running (rb_thread_lock_native_thread) and that
+ // thread ended. Nothing can resume this context; sleep out the process.
+ while (1) {
+ pause();
+ }
}
+// RUBY_MN_THREADS=2: make the running main thread an M:N thread in place.
+// Its context becomes the process stack (as nt_start's own stack is an snt's
+// context) and its nt joins the shared pool with a fresh stack for its loop.
+// Nothing changes for the thread until it first blocks: that transfer
+// starts nt_loop_co on this nt, and any snt may resume the thread later.
+static void
+thread_sched_main_to_shared(rb_thread_t *th)
+{
+ rb_vm_t *vm = th->vm;
+ struct rb_native_thread *nt = th->nt;
+ struct rb_thread_sched *sched = TH_SCHED(th);
+
+ VM_ASSERT(th == vm->ractor.main_thread);
+ VM_ASSERT(nt->dedicated == 1 && th->has_dedicated_nt);
+ VM_ASSERT(sched->running == th);
+
+ // the loop's stack (the vm stack half of the pool slot goes unused)
+ void *vm_stack, *machine_stack;
+ int err = nt_alloc_stack(vm, &vm_stack, &machine_stack);
+ if (err) {
+ rb_warn("RUBY_MN_THREADS=2: cannot allocate the main nt's stack (%s); the main thread stays dedicated", strerror(err));
+ th->ractor->threads.sched.enable_mn_threads = true; // as =1
+ return;
+ }
+ size_t machine_stack_size = vm->default_params.thread_machine_stack_size - sizeof(struct nt_machine_stack_footer);
+ // the main nt is ZALLOC'd by Init_BareVM, not native_thread_alloc: no context yet
+ nt->nt_context = ruby_xmalloc(sizeof(struct coroutine_context));
+ coroutine_initialize(nt->nt_context, nt_loop_co, machine_stack, machine_stack_size);
+ nt->nt_context->argument = nt;
+
+ // the thread's context is the process stack it already runs on
+ struct rb_thread_context *tctx = ruby_xmalloc(sizeof(struct rb_thread_context));
+ tctx->stack = NULL; // not a pool stack: never freed
+ tctx->dead = false;
+ tctx->nt = NULL;
+ coroutine_initialize_main(&tctx->co);
+ tctx->co.argument = th;
+ th->sched.context = &tctx->co;
+
+ thread_sched_lock(sched, th);
+ {
+ // re-record the running thread as an snt's (running_dnts -> nt->running_th)
+ thread_sched_del_running_thread(sched, th);
+ nt->dedicated = 0;
+ th->has_dedicated_nt = 0;
+ nt_snts_join(vm, nt);
+ // the pool's first snt; under the lock native_thread_check_and_create_shared
+ // takes, so that it never mints one for the main Ractor alongside
+ ractor_sched_lock(vm, th->ractor);
+ {
+ // The pool is still empty: the timer thread has been up since
+ // Init_Thread and would mint one here, but its timeout branch is
+ // the only path there and it sleeps untimed with nothing waiting.
+ VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt) == 0);
+ RUBY_ATOMIC_INC(vm->ractor.sched.snt_cnt);
+ th->ractor->threads.sched.enable_mn_threads = true;
+ }
+ ractor_sched_unlock(vm, th->ractor);
+#if USE_RUBY_DEBUG_LOG
+ vm->ractor.sched.dnt_cnt--;
+#endif
+ thread_sched_add_running_thread(sched, th);
+ }
+ thread_sched_unlock(sched, th);
+
+ RUBY_DEBUG_LOG("main th:%u on nt:%d is now shared", rb_th_serial(th), nt->serial);
+}
+#endif
+
static int native_thread_create_shared(rb_thread_t *th);
#if USE_MN_THREADS
@@ -2115,13 +2274,15 @@ rb_threadptr_sched_free(rb_thread_t *th)
{
timer_thread_wake_fence(th);
#if USE_MN_THREADS
- if (th->sched.malloc_stack) {
- // has dedicated
- SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
- native_thread_destroy(th->nt);
- }
- else if (th->sched.context != NULL) {
- // a coroutine thread that never reached its epilogue (never started);
+ // A thread with a coroutine context runs on a native thread it shares and
+ // does not own: one made for the shared pool, or the main thread made
+ // shared. nt->dedicated does not say so, rb_thread_lock_native_thread()
+ // raising it on a shared native thread that stays in the pool.
+ const bool owns_nt = th->sched.context == NULL;
+
+ if (th->sched.context != NULL) {
+ // a coroutine thread that never reached its epilogue (never started),
+ // or the main thread made shared (its stack is the process stack);
// a terminated one is reclaimed by whoever resumed from its final
// transfer (thread_sched_reclaim), and cleared this pointer.
struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
@@ -2130,6 +2291,12 @@ rb_threadptr_sched_free(rb_thread_t *th)
th->sched.context = NULL;
// TODO: how to free nt and nt->altstack?
}
+ if (th->sched.malloc_stack) {
+ SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
+ if (th->nt && owns_nt) {
+ native_thread_destroy(th->nt);
+ }
+ }
#else
SIZED_FREE_N((VALUE *)th->sched.context_stack, th->sched.context_stack_size);
native_thread_destroy(th->nt);
@@ -2469,6 +2636,31 @@ rb_thread_lock_native_thread(void)
return is_snt;
}
+// rb_ractor_terminate_all() waits for the Ractors it interrupted on a condvar
+// of its own, holding the VM lock, which it drops for the wait and takes back
+// after. An M:N thread would hold the shared native thread it runs on for the
+// whole wait as well, leaving those Ractors with nothing to run on, so give
+// that native thread back the way a blocking region does.
+void
+rb_ractor_sched_wait_terminate(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
+{
+ ASSERT_vm_locking();
+
+ rb_thread_t *th = GET_THREAD();
+ unsigned int lock_rec = vm->ractor.sync.lock_rec;
+ rb_ractor_t *lock_owner = vm->ractor.sync.lock_owner;
+
+ vm->ractor.sync.lock_rec = 0;
+ vm->ractor.sync.lock_owner = NULL;
+
+ native_thread_dedicated_inc(vm, th->ractor, th->nt);
+ rb_native_cond_timedwait(cond, &vm->ractor.sync.lock, msec);
+ native_thread_dedicated_dec(vm, th->ractor, th->nt);
+
+ vm->ractor.sync.lock_rec = lock_rec;
+ vm->ractor.sync.lock_owner = lock_owner;
+}
+
void
rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
{
diff --git a/thread_sched.h b/thread_sched.h
index 63ca7d003b6204..e56f5d7dd19c32 100644
--- a/thread_sched.h
+++ b/thread_sched.h
@@ -262,6 +262,7 @@ struct rb_ractor_sched {
void rb_ractor_sched_wait(struct rb_execution_context_struct *ec, struct rb_ractor_struct *cr, rb_unblock_function_t *ptr, void *arg);
void rb_ractor_sched_wakeup(struct rb_ractor_struct *r, struct rb_thread_struct *th);
+void rb_ractor_sched_wait_terminate(struct rb_vm_struct *vm, rb_nativethread_cond_t *cond, unsigned long msec);
void rb_thread_wake_fence(struct rb_thread_struct *th);
#endif /* RUBY_THREAD_SCHED_H */
diff --git a/thread_sched_mn.c b/thread_sched_mn.c
index bac0e9eeaf63c4..73a195c557f3ae 100644
--- a/thread_sched_mn.c
+++ b/thread_sched_mn.c
@@ -925,6 +925,8 @@ native_thread_check_and_create_shared(rb_vm_t *vm)
{
bool need_to_make = false;
+ if (!mn_threads_enabled_p()) return 0; // no thread is M:N: the pool serves nobody
+
ractor_sched_lock(vm, NULL); // NULL: the timer thread also calls this
{
unsigned int schedulable_ractor_cnt = vm->ractor.cnt;
diff --git a/thread_win32.c b/thread_win32.c
index 456ad8114bd86d..943744a9bcf023 100644
--- a/thread_win32.c
+++ b/thread_win32.c
@@ -872,6 +872,12 @@ native_reset_timer_thread(void)
* the ones that rb_bug().
* ------------------------------------------------------------------------- */
+static bool
+native_thread_self_can_retire_p(void)
+{
+ return true;
+}
+
static int
native_thread_create_shared(rb_thread_t *th)
{
diff --git a/time.c b/time.c
index f57b68cb5d3360..32b7048d8f68fc 100644
--- a/time.c
+++ b/time.c
@@ -739,8 +739,9 @@ static struct {
} w32_tz;
static char *
-get_tzname(int dst)
+get_tzname(int dst, rb_encoding **enc)
{
+ *enc = NULL;
if (w32_tz.use_tzkey) {
if (w32_tz.name[0]) {
return w32_tz.name;
@@ -765,6 +766,11 @@ get_tzname(int dst)
}
}
}
+ /* CRT timezone names are encoded in the active code page, which
+ * may differ from the locale (console) code page */
+ char cp[(sizeof(UINT) * 8 / 3) + 4];
+ snprintf(cp, sizeof(cp), "CP%u", GetACP());
+ *enc = rb_enc_find(cp);
return _tzname[_daylight && dst];
}
#endif
@@ -1002,7 +1008,7 @@ timegmw_noleapsecond(struct vtm *vtm)
}
static VALUE
-zone_str(const char *zone)
+zone_str_enc(const char *zone, rb_encoding *enc)
{
const char *p;
int ascii_only = 1;
@@ -1023,6 +1029,9 @@ zone_str(const char *zone)
if (ascii_only) {
return rb_enc_interned_str(zone, len, rb_usascii_encoding());
}
+ else if (enc) {
+ return rb_enc_interned_str(zone, len, enc);
+ }
else {
#ifdef _WIN32
VALUE str = rb_utf8_str_new(zone, len);
@@ -1035,6 +1044,12 @@ zone_str(const char *zone)
}
}
+static inline VALUE
+zone_str(const char *zone)
+{
+ return zone_str_enc(zone, NULL);
+}
+
static void
gmtimew_noleapsecond(wideval_t timew, struct vtm *vtm)
{
@@ -1730,7 +1745,9 @@ localtime_with_gmtoff_zone(const time_t *t, struct tm *result, long *gmtoff, VAL
#if defined(HAVE_TM_ZONE)
*zone = zone_str(tm.tm_zone);
#elif defined(_WIN32)
- *zone = zone_str(get_tzname(tm.tm_isdst));
+ rb_encoding *enc;
+ const char *name = get_tzname(tm.tm_isdst, &enc);
+ *zone = zone_str_enc(name, enc);
#elif defined(HAVE_TZNAME) && defined(HAVE_DAYLIGHT)
/* this needs tzset or localtime, instead of localtime_r */
*zone = zone_str(tzname[daylight && tm.tm_isdst]);
diff --git a/vm_core.h b/vm_core.h
index 8843b7ac124ead..75d6323236169a 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -2354,10 +2354,6 @@ void rb_fiber_close(rb_fiber_t *fib);
void Init_native_thread(rb_thread_t *th);
int rb_vm_check_ints_blocking(rb_execution_context_t *ec);
-// vm_sync.h
-void rb_vm_cond_wait(rb_vm_t *vm, rb_nativethread_cond_t *cond);
-void rb_vm_cond_timedwait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec);
-
#define RUBY_VM_CHECK_INTS(ec) rb_vm_check_ints(ec)
static inline void
rb_vm_check_ints(rb_execution_context_t *ec)
diff --git a/vm_sync.c b/vm_sync.c
index 7b6a587a1442f6..73335699a01626 100644
--- a/vm_sync.c
+++ b/vm_sync.c
@@ -214,37 +214,6 @@ rb_vm_unlock_body(LOCATION_ARGS)
vm_lock_leave(vm, false, &vm->ractor.sync.lock_rec APPEND_LOCATION_PARAMS);
}
-static void
-vm_cond_wait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
-{
- ASSERT_vm_locking();
- unsigned int lock_rec = vm->ractor.sync.lock_rec;
- rb_ractor_t *cr = vm->ractor.sync.lock_owner;
-
- vm->ractor.sync.lock_rec = 0;
- vm->ractor.sync.lock_owner = NULL;
- if (msec > 0) {
- rb_native_cond_timedwait(cond, &vm->ractor.sync.lock, msec);
- }
- else {
- rb_native_cond_wait(cond, &vm->ractor.sync.lock);
- }
- vm->ractor.sync.lock_rec = lock_rec;
- vm->ractor.sync.lock_owner = cr;
-}
-
-void
-rb_vm_cond_wait(rb_vm_t *vm, rb_nativethread_cond_t *cond)
-{
- vm_cond_wait(vm, cond, 0);
-}
-
-void
-rb_vm_cond_timedwait(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
-{
- vm_cond_wait(vm, cond, msec);
-}
-
static bool
vm_barrier_acquired_p(const rb_vm_t *vm, const rb_ractor_t *cr)
{