From 90ed85f91ed8c9df26efcbdc5e6a1b428be3ef06 Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 18 May 2026 17:24:17 +0900 Subject: [PATCH 1/4] Fix UAF in IO::Buffer#^ when self or mask is an invalidated slice `io_buffer_xor` accessed `buffer->base` and `mask_buffer->base` directly without validating that the buffers were still live. A slice whose parent had been freed retained its stale base pointer, so calling `^` on it caused a UAF. Use `io_buffer_get_bytes_for_reading` for both operands, which raises `IO::Buffer::InvalidatedError` before any memory access if either buffer has been invalidated. --- io_buffer.c | 14 +++++++++++--- test/ruby/test_io_buffer.rb | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 6bd0b3cfd38cd3..95d6b2491397fb 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -3509,13 +3509,21 @@ io_buffer_xor(VALUE self, VALUE mask) struct rb_io_buffer *mask_buffer = NULL; TypedData_Get_Struct(mask, struct rb_io_buffer, &rb_io_buffer_type, mask_buffer); - io_buffer_check_mask_size(mask_buffer->size); + const void *base; + size_t size; + io_buffer_get_bytes_for_reading(buffer, &base, &size); - VALUE output = rb_io_buffer_new(NULL, buffer->size, io_flags_for_size(buffer->size)); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + + io_buffer_check_mask_size(mask_size); + + VALUE output = rb_io_buffer_new(NULL, size, io_flags_for_size(size)); struct rb_io_buffer *output_buffer = NULL; TypedData_Get_Struct(output, struct rb_io_buffer, &rb_io_buffer_type, output_buffer); - memory_xor(output_buffer->base, buffer->base, buffer->size, mask_buffer->base, mask_buffer->size); + memory_xor(output_buffer->base, base, size, mask_base, mask_size); return output; } diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 92a81ddd97cce0..4022bd5d4873cf 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -710,6 +710,7 @@ def test_operators_raise_on_freed_self mask = IO::Buffer.for("ABCDEFGH") assert_raise(IO::Buffer::InvalidatedError) { slice & mask } assert_raise(IO::Buffer::InvalidatedError) { slice | mask } + assert_raise(IO::Buffer::InvalidatedError) { slice ^ mask } end def test_operators_raise_on_freed_mask @@ -720,6 +721,7 @@ def test_operators_raise_on_freed_mask source = IO::Buffer.for("ABCDEFGH") assert_raise(IO::Buffer::InvalidatedError) { source & mask_slice } assert_raise(IO::Buffer::InvalidatedError) { source | mask_slice } + assert_raise(IO::Buffer::InvalidatedError) { source ^ mask_slice } end def test_bit_count From e846e51daac54b78834805b65f03a915c0941a48 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 19 May 2026 10:19:06 +0900 Subject: [PATCH 2/4] Fix out-of-bounds read for empty strings Fix for GH-15898. --- file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index b045e8e899bc9f..f4814ee0f64e82 100644 --- a/file.c +++ b/file.c @@ -5518,7 +5518,7 @@ rb_file_join_fastpath(long argc, VALUE *args) long tmp_len; RSTRING_GETMEM(tmp, tmp_s, tmp_len); - if (isdirsep(tmp_s[0])) { + if (tmp_len > 0 && isdirsep(tmp_s[0])) { // right side has a leading separator, remove left side separators. long trailing_seps = 0; while (isdirsep(name[len - trailing_seps - 1])) { @@ -5526,7 +5526,7 @@ rb_file_join_fastpath(long argc, VALUE *args) } rb_str_set_len(result, len - trailing_seps); } - else if (!isdirsep(name[len - 1])) { + else if (len < 1 || !isdirsep(name[len - 1])) { // neither side have a separator, append one; rb_str_cat(result, "/", 1); } From 171cb0925a039ab4009660a5e824d7a3cc648c1c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 19 May 2026 10:51:09 +0900 Subject: [PATCH 3/4] Remove assumption that strings are NUL-terminated --- file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index f4814ee0f64e82..ae741ce5671399 100644 --- a/file.c +++ b/file.c @@ -5471,10 +5471,10 @@ rb_file_join_ary(VALUE ary) } else { tail = chompdirsep(name, name + len, true, rb_enc_get(result)); - if (RSTRING_PTR(tmp) && isdirsep(RSTRING_PTR(tmp)[0])) { + if (RSTRING_LEN(tmp) > 0 && isdirsep(RSTRING_PTR(tmp)[0])) { rb_str_set_len(result, tail - name); } - else if (!*tail) { + else if (tail == name + len) { rb_str_cat(result, "/", 1); } } From 37fe088e0e24667381b7ca411eef3e95c1476ed3 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 18 May 2026 21:41:51 -0500 Subject: [PATCH 4/4] [ruby/strscan] [DOC] Doc for StringScanner#scan_integer (https://github.com/ruby/strscan/pull/206) Came here to fix a broken link (second pattern generated a bogus link); stayed to add examples and generally brighten. https://github.com/ruby/strscan/commit/32cc23934c --- ext/strscan/lib/strscan/strscan.rb | 38 ++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/ext/strscan/lib/strscan/strscan.rb b/ext/strscan/lib/strscan/strscan.rb index 46acc7ea823f07..07ed102d9a8cfe 100644 --- a/ext/strscan/lib/strscan/strscan.rb +++ b/ext/strscan/lib/strscan/strscan.rb @@ -1,17 +1,41 @@ # frozen_string_literal: true class StringScanner + # :markup: markdown + # # call-seq: - # scan_integer(base: 10) + # scan_integer(base: 10) -> integer or nil + # + # Returns an integer scanned from `self`, + # beginning at the current position; + # returns `nil` if no such integer was available. + # + # When `base` is `10` (the default), + # equivalent to calling #scan with argument +pattern+ + # as `'[+-]?\d+'`: + # + # ```ruby + # scanner = StringScanner.new('Form 27B/6') + # scanner.scan_integer # => nil # No integer at position 0. + # scanner.pos = 5 + # scanner.scan_integer # => 27 + # scanner.matched # => "27" + # scanner.pos # => 7 + # ``` # - # If `base` isn't provided or is `10`, then it is equivalent to calling `#scan` with a `[+-]?\d+` pattern, - # and returns an Integer or nil. + # When `base` is `16` (the only other value allowed), + # equivalent to calling #scan with argument `pattern` + # as `'[+-]?(0x)?[0-9a-fA-F]+'`: # - # If `base` is `16`, then it is equivalent to calling `#scan` with a `[+-]?(0x)?[0-9a-fA-F]+` pattern, - # and returns an Integer or nil. + # ```ruby + # scanner.pos = 5 + # scanner.scan_integer(base: 16) # => 635 + # scanner.matched # => "27B" + # scanner.pos # => 8 + # ``` # - # The scanned string must be encoded with an ASCII compatible encoding, otherwise - # Encoding::CompatibilityError will be raised. + # Raises Encoding::CompatibilityError if `self` does not have + # an ASCII compatible encoding. def scan_integer(base: 10) case base when 10