Declaring the same source URL twice with different cooldown values
silently drops one of them, because duplicate declarations are deduped
into a single source and cooldown applies per server, not per gem.
The reporter of #9723 used a second source block with cooldown: 0 as a
per-gem exemption and got no indication it had no effect. Keep the
dedup behavior but warn when a conflicting cooldown value is discarded.
https://github.com/rubygems/rubygems/issues/9723https://github.com/ruby/rubygems/commit/a2a44cf4e0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The documentation says that Process::Status.wait sets the thread-local
variable $? when there are child processes, but it never does.
rb_process_status_wait does not call rb_last_status_set, and $? is left
untouched:
$? # => nil
Process.spawn('cat /nop') # => 4996
Process::Status.wait # => #<Process::Status: pid 4996 exit 1>
$? # => nil
The existing example showed this as well: the pid displayed for $?
(1155508) did not match the pid of the returned status (1155880),
contradicting the surrounding text.
Since $? is never set, the "does not set thread-local variable $?"
note in the no-child-process case is now redundant and has been folded
into the general description.
The `options/` segment of Bundler's User-Agent is built from
`Bundler.settings.all`, which returns only setting names. Mirror
settings, however, embed a URI in the key itself
(`mirror.http://user:token@host/`), so the URI userinfo becomes part of
the name. That name is then sent to every gem source Bundler contacts,
including public or attacker-controlled sources, leaking the embedded
credentials cross-origin.
Strip the userinfo from any settings key that embeds a URI when building
the User-Agent, keeping the useful telemetry (which settings are in use
and the host) while dropping the secret. The redaction is limited to the
User-Agent; `Bundler.settings.all` still returns the real keys, which
`local_overrides` and `gem_mirrors` rely on.
https://github.com/ruby/rubygems/commit/1468529cbe
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bundler::CompactIndexClient::Cache#info_path and #info_etag_path join
the gem name into the cache directory without validation. The name comes
from the remote index, either versions lines or transitive dependency
names in info files, so a crafted name like "../../../../pwn" escapes
the cache directory because the special-characters branch keeps the raw
name and only appends an MD5 suffix. Reject any name that is not a plain
basename before constructing the path, matching the fetch_spec guard
from https://github.com/ruby/rubygems/commit/56ed326cb2, and apply the same guard to the independent
Gem::CompactIndexClient::Cache port.
https://github.com/ruby/rubygems/commit/4b1c454526
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `error` variable was set on failure but never reached the exit
code, so removal failures such as a locked DLL left `nmake clean`
green while stale artifacts remained. Exit with `error` at the end.
Since `rd /s` does not set ERRORLEVEL on failure, check the entries
remaining after it instead. Also pass `/f` to `del` to remove
read-only files as `rm -f` does.
`BUNDLE_FROZEN=1 bundle check` reported that dependencies are satisfied and exited 0 even when the lockfile was missing CHECKSUMS entries, because `write_lock` only warns in frozen mode. Run the same frozen validation as `bundle install` so `bundle check` fails consistently.
https://github.com/ruby/rubygems/issues/9546https://github.com/ruby/rubygems/commit/07337df092
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements GC fast path for newrange instruction when we do not need to
call <=> method (when either endpoint is nil or both are fixnums).
Benchmark:
def run(max)
i = 0
while i < max
a = i..max
i += 1
end
end
30.times { run(10) }
run(10_000_000)
Result:
Benchmark 1: master
Time (mean ± σ): 221.8 ms ± 5.2 ms [User: 214.4 ms, System: 5.8 ms]
Range (min … max): 216.4 ms … 235.8 ms 13 runs
Benchmark 2: branch
Time (mean ± σ): 62.4 ms ± 1.6 ms [User: 56.0 ms, System: 5.1 ms]
Range (min … max): 60.7 ms … 69.7 ms 46 runs
Summary
branch ran
3.56 ± 0.12 times faster than master
rb_managed_id_table stores its rb_id_table body as embedded data
(RUBY_TYPED_EMBEDDABLE), so managed_id_table_ptr returns a pointer
interior to the wrapper object. rb_managed_id_table_foreach,
_foreach_values, and _dup hand that interior pointer to a callback that
may allocate and trigger a GC. If the compiler stops keeping the
wrapper VALUE live once the interior pointer is taken, the table can be
reclaimed while it is still being iterated. Add RB_GC_GUARD on the
wrapper across these calls.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The invokeblock fast path (InvokeBlockIseqDirect) passes captured self
plus each block argument in C argument registers, but
block_call_inlinable_iseq never bounded the argument count. Yielding 6+
arguments to a matching-arity block made gen_invoke_block_iseq_direct
emit a CCall with more operands than C_ARG_OPNDS, which aborts the
process in the backend:
assertion failed: data.opnds.len() <= C_ARG_OPNDS.len()
at zjit/src/backend/x86_64/mod.rs:302
Reject such blocks in block_call_inlinable_iseq so both the guarded and
the inlined-caller yield paths fall back to VM dispatch.
Previously map_ptr<T>() took *const T, so a reference like &IseqPtr
silently coerced to *const IseqPtr and mapped the address of the
reference itself instead of the pointer value, which is how the bug
fixed by the previous commit slipped in.
Constrain the parameter with an ActualPtr trait implemented only for
*const T and *mut T, so passing a reference no longer compiles.
Co-authored-by: Max Bernstein <tekknolagi@gmail.com>
Proc#refined memoizes {copied_iseq, cref} per source iseq in a
process-global map so the copy can be reused from any Ractor. The
cref's refinements table is already frozen and marked shareable for
that reason, but the copied iseq and the memo array holding it are
left unshareable, so the shared payload is not actually a shareable
object graph.
Complete that intent: mark the copied iseq subtree shareable (it is
self-contained -- nested block iseqs are copied too) and mark the
frozen memo array shareable. A copy's local_iseq/parent_iseq point at
already-shareable iseqs (other copies in the subtree, or the shareable
enclosing iseq), so the shareable check needs no special casing.
st_table_size() is the number of entries, but ractor_memsize() adds the result to sizeof(rb_ractor_t) as a byte count, so a Ractor was reported as costing one byte per open port. Use st_memsize(), as ractor_selector_memsize() in this file already does for its own ports table.
On x86_64-linux hosts `--cooldown 0` resolves to the platform-specific
build, so `include_gems` failed expecting the ruby platform.
https://github.com/ruby/rubygems/commit/d7be25160e
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exclusion is keyed on [name, version] and deliberately ignores
platform, so pushing a fresh platform-specific build under an
already-ripe version number cannot slip new code past the cooldown.
No spec covered that behavior.
https://github.com/ruby/rubygems/commit/be334bd252
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both commands resolve, but only install, update, add and outdated
accepted the flag, so the resolver's failure hint suggesting
`--cooldown 0` was not actionable on `bundle lock --update`.
https://github.com/ruby/rubygems/commit/2c4f4f390a
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gen_invokeblock_ifunc drops the callinfo and passes argv straight to
rb_vm_yield_with_cfunc, which hardcodes kw_splat = 0, so keyword
arguments lost their keyword-ness. Gate the ifunc specialization on
block_call_inlinable, the same check used for the inline ISEQ path.
The GC sweep fast path had an inverted condition for hashes in
rb_gc_obj_needs_cleanup_p. Hashes with an st_table needs to be freed.
The following script leaks memory:
10.times do
100_000.times do
# Hashes with more than 8 elements use st_table with a malloc'd
# entries array
h = {}
9.times { |i| h[i] = i }
end
GC.start
puts `ps -o rss= -p #{$$}`
end
Before:
58816
97056
135296
173584
211840
250096
288336
326608
364848
403088
After:
27696
27696
27696
27696
27696
27696
27696
27696
27696
27696
OpenSSL::PKCS7#decrypt is unreliable when given only a private key and
the PKCS#7 structure contains multiple recipients. It may occasionally
raise an exception or return garbage data. Update the test to avoid
this.
This became much more visible in CI after upgrading to OpenSSL versions
released on 2026-06-09, which enable the implicit rejection mechanism
for PKCS#1 v1.5 decryption in PKCS#7.
https://github.com/ruby/openssl/commit/f7c51a2c0c
The `once` insn (ex: for interpolated regexp with /o option) should only be
executed once even across Ractors. The `once` value is associated with
the iseq, which is shared across Ractors.
Ideally, waiting threads in other Ractors would be put in a queue and
signalled when the winning thread in another Ractor wins the race and
executes the iseq successfully. For now, there's potential for busy
looping when Ractors wait on each other. However, the bug is fixed and
the iseq will only be executed once.
* ZJIT: Fix trivial inlining for optional argument entries
Use SendDirect's jit_entry_idx when reading the callee bytecode for
trivial inlining, so optional-argument calls inspect the same entry
point that SendDirect selected.
This avoids inlining default-argument initialization code when the
caller actually provided the optional positional argument.
* ZJIT: Require trivial inline target to end at leave
Limit the trivial inliner to whole-ISEQ shapes that start at bytecode
index 0 and end with the second instruction's leave. Optional argument
entries can start in the middle of the ISEQ, so keep those SendDirect
calls for the general inliner, which handles jit_entry_idx.
This avoids folding default-argument initialization code when the caller
actually supplied the optional positional argument.
Co-authored-by: Takashi Kokubun (k0kubun) <takashikkbn@gmail.com>
Co-authored-by: Alan Wu <XrXr@users.noreply.github.com>
Ractor::Port has a public allocator, so Ractor::Port.allocate returns a port whose owner Ractor is still NULL; only ractor_port_init() fills it in. The methods dereferenced it unchecked:
Ractor::Port.allocate.closed? # [BUG] Segmentation fault at 0x60
Route the paths reachable from Ruby through ractor_port_ptr_check(), which raises TypeError as other uninitialized typed data objects do. The self of #initialize_copy keeps the unchecked accessor because it is legitimately uninitialized there; only orig is checked.
Ractor.select did not crash, but such a port has id 0, the same as the current Ractor's default port, so it took messages from it.
Port#closed? also loses Primitive.attr! :leaf. It can raise now, and it was not a leaf before either, since the foreign Ractor path takes RACTOR_LOCK().
[Bug #22214]
I benchmarked various values of STR_DUPLICATE_MAX_EMBED_LEN and the
performance impacts. For String#dup, creating a view is faster until it
is below 256 bytes. Unsurprisingly, if we add modification to the string,
then it is faster to eagerly create a copy than use a view. Surprisingly,
if we append to the string, it's still faster to go from embedded -> extended
than shared -> extended. However, both of these cases will incur a memory
penalty as the remainder of the slot is wasted memory.
Benchmark:
require "benchmark"
TIMES = 10_000_000
Benchmark.bm do |x|
[64, 128, 256, 512, 1024].each do |len|
len -= 25 # Subtract header length
x.report("String#dup for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
str.dup
i += 1
end
end
Process.wait(pid)
end
x.report("String#dup + String#[]= for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
dup = str.dup
dup[0] = "1"
i += 1
end
end
Process.wait(pid)
end
x.report("String#dup + String#<< for #{len.to_s}") do
pid = fork do
str = ("a" * len).freeze
i = 0
while i < TIMES
dup = str.dup
dup << "1"
i += 1
end
end
Process.wait(pid)
end
end
end
Result:
1024
user system total real
String#dup for 39 0.000085 0.000646 0.207714 ( 0.208041)
String#dup + String#[]= for 39 0.000067 0.000379 0.499957 ( 0.500514)
String#dup + String#<< for 39 0.000069 0.000413 0.600205 ( 0.600984)
String#dup for 103 0.000068 0.000374 0.236938 ( 0.237424)
String#dup + String#[]= for 103 0.000063 0.000401 0.501791 ( 0.502506)
String#dup + String#<< for 103 0.000066 0.000395 0.600041 ( 0.600870)
String#dup for 231 0.000066 0.000370 0.281924 ( 0.282409)
String#dup + String#[]= for 231 0.000064 0.000355 0.574779 ( 0.575599)
String#dup + String#<< for 231 0.000070 0.000395 0.691011 ( 0.691972)
String#dup for 487 0.000070 0.000366 0.355631 ( 0.356230)
String#dup + String#[]= for 487 0.000067 0.000391 0.653774 ( 0.654509)
String#dup + String#<< for 487 0.000066 0.000359 0.891413 ( 0.892607)
String#dup for 999 0.000068 0.000394 0.527350 ( 0.528033)
String#dup + String#[]= for 999 0.000067 0.000378 0.786794 ( 0.787682)
String#dup + String#<< for 999 0.000063 0.000363 1.070604 ( 1.071885)
512
user system total real
String#dup for 39 0.000083 0.000639 0.207305 ( 0.207687)
String#dup + String#[]= for 39 0.000067 0.000398 0.501968 ( 0.502645)
String#dup + String#<< for 39 0.000071 0.000414 0.586533 ( 0.587365)
String#dup for 103 0.000064 0.000397 0.222104 ( 0.222600)
String#dup + String#[]= for 103 0.000069 0.000389 0.483612 ( 0.484240)
String#dup + String#<< for 103 0.000069 0.000380 0.606659 ( 0.607510)
String#dup for 231 0.000067 0.000388 0.292563 ( 0.293070)
String#dup + String#[]= for 231 0.000064 0.000389 0.573774 ( 0.574479)
String#dup + String#<< for 231 0.000069 0.000413 0.739569 ( 0.740614)
String#dup for 487 0.000067 0.000419 0.370516 ( 0.371072)
String#dup + String#[]= for 487 0.000060 0.000337 0.676507 ( 0.677282)
String#dup + String#<< for 487 0.000074 0.000410 0.911670 ( 0.916480)
String#dup for 999 0.000069 0.000397 0.239633 ( 0.240049)
String#dup + String#[]= for 999 0.000066 0.000370 1.196397 ( 1.198272)
String#dup + String#<< for 999 0.000072 0.000396 1.683562 ( 1.686244)
256
user system total real
String#dup for 39 0.000073 0.000588 0.198103 ( 0.198485)
String#dup + String#[]= for 39 0.000067 0.000397 0.475345 ( 0.476011)
String#dup + String#<< for 39 0.000074 0.000408 0.589001 ( 0.589778)
String#dup for 103 0.000073 0.000420 0.244381 ( 0.244818)
String#dup + String#[]= for 103 0.000065 0.000421 0.480720 ( 0.481569)
String#dup + String#<< for 103 0.000070 0.000414 0.589164 ( 0.590123)
String#dup for 231 0.000077 0.000447 0.272169 ( 0.272640)
String#dup + String#[]= for 231 0.000067 0.000425 0.517280 ( 0.518037)
String#dup + String#<< for 231 0.000064 0.000409 0.681593 ( 0.682572)
String#dup for 487 0.000065 0.000390 0.229708 ( 0.230153)
String#dup + String#[]= for 487 0.000067 0.000395 0.872966 ( 0.874233)
String#dup + String#<< for 487 0.000071 0.000411 1.317758 ( 1.320085)
String#dup for 999 0.000070 0.000397 0.255197 ( 0.255609)
String#dup + String#[]= for 999 0.000068 0.000380 1.174781 ( 1.176700)
String#dup + String#<< for 999 0.000067 0.000389 1.729608 ( 1.738797)
128
user system total real
String#dup for 39 0.000105 0.000950 0.205384 ( 0.206565)
String#dup + String#[]= for 39 0.000066 0.000406 0.483924 ( 0.485308)
String#dup + String#<< for 39 0.000075 0.000433 0.590150 ( 0.591641)
String#dup for 103 0.000076 0.000414 0.225900 ( 0.227975)
String#dup + String#[]= for 103 0.000098 0.000604 0.499234 ( 0.503926)
String#dup + String#<< for 103 0.000070 0.000400 0.619562 ( 0.627411)
String#dup for 231 0.000073 0.000445 0.241433 ( 0.243606)
String#dup + String#[]= for 231 0.000067 0.000402 0.843665 ( 0.853293)
String#dup + String#<< for 231 0.000071 0.000392 0.990903 ( 0.994555)
String#dup for 487 0.000073 0.000403 0.234920 ( 0.236132)
String#dup + String#[]= for 487 0.000077 0.000445 0.944582 ( 0.956001)
String#dup + String#<< for 487 0.000069 0.000410 1.337099 ( 1.341250)
String#dup for 999 0.000072 0.000420 0.233520 ( 0.234182)
String#dup + String#[]= for 999 0.000072 0.000419 1.186288 ( 1.188972)
String#dup + String#<< for 999 0.000069 0.000436 1.685112 ( 1.689536)
The call-seq of IO::Buffer#<=> says it returns "true or false", but
rb_io_buffer_compare always returns an integer: -1 or 1 when the sizes
differ, and the result of memcmp as is when the sizes are equal. Since
the value comes straight from memcmp it is not normalized to -1, 0 or 1,
so only its sign is meaningful.
p(IO::Buffer.for("abc") <=> IO::Buffer.for("abc")) # => 0
p(IO::Buffer.for("abc") <=> IO::Buffer.for("ab")) # => 1
p(IO::Buffer.for("abc") <=> IO::Buffer.for("abz")) # => -23
IO::Buffer#hexdump returns nil when the buffer does not reference any
memory, that is, when #null? returns true, but this was not documented.
buffer = IO::Buffer.new(4)
buffer.free
p buffer.hexdump # => nil