From c2cef6285962296021fbebc5f01c7d94a9d84074 Mon Sep 17 00:00:00 2001 From: Aristide CORDONNIER Date: Wed, 11 Nov 2026 01:58:28 -0600 Subject: [PATCH] Fix out-of-bounds read in `gets` with a limit inside a truncated character When the limit falls inside a truncated last character of a fixed-width encoding (UTF-32, for example), the adjusted end of the line was placed after the end of the string. The separator search then read a few bytes past the buffer, and `pos` was left beyond the end of the string. On JRuby the same case raised `ArrayIndexOutOfBoundsException`. io = StringIO.new("abcdef".b.freeze) io.set_encoding(Encoding::UTF_32LE) io.gets(5) io.pos # => 8, should be 6 Clamp the adjusted end to the end of the string, in both the C and the Java implementations. `readlines` and `each_line` go through the same code. Co-Authored-By: Claude Mythos 5.2 --- ext/java/org/jruby/ext/stringio/StringIO.java | 3 ++- ext/stringio/stringio.c | 3 +++ test/stringio/test_stringio.rb | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ext/java/org/jruby/ext/stringio/StringIO.java b/ext/java/org/jruby/ext/stringio/StringIO.java index 85bb955..18f0f5a 100644 --- a/ext/java/org/jruby/ext/stringio/StringIO.java +++ b/ext/java/org/jruby/ext/stringio/StringIO.java @@ -896,7 +896,8 @@ private IRubyObject getline(ThreadContext context, final IRubyObject rs, int lim int w = 0; if (limit > 0 && s + limit < e) { - e = getEncoding().rightAdjustCharHead(stringBytes, s, s + limit, e); + // a fixed-width encoding may step past a truncated last character + e = Math.min(e, getEncoding().rightAdjustCharHead(stringBytes, s, s + limit, e)); } if (rs == context.nil) { if (chomp) { diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 4f953f6..1255096 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1555,7 +1555,10 @@ strio_getline(struct getline_arg *arg, struct StringIO *ptr) e = s + RSTRING_LEN(ptr->string); s += ptr->pos; if (limit > 0 && (size_t)limit < (size_t)(e - s)) { + const char *const end = e; e = rb_enc_right_char_head(s, s + limit, e, get_enc(ptr)); + /* a fixed-width encoding may step past a truncated last character */ + if (e > end) e = end; } if (NIL_P(str)) { if (arg->chomp) { diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index fd88de5..cfada6f 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -151,6 +151,17 @@ def test_gets_utf_16 assert_nil(stringio.gets) end + def test_gets_limit_in_truncated_fixed_width_char + # 6 bytes: one UTF-32 code unit and a truncated one. The limit + # falls inside the truncated unit, and the line must stop at the + # end of the string, not beyond it. + stringio = StringIO.new("abcdef".b.freeze) + stringio.set_encoding(Encoding::UTF_32LE) + assert_equal("abcdef".b, stringio.gets(5).b) + assert_equal(6, stringio.pos) + assert_nil(stringio.gets(5)) + end + def test_gets_chomp assert_equal(nil, StringIO.new("").gets(chomp: true)) assert_equal("", StringIO.new("\n").gets(chomp: true))