From caa586bf6eb50bdf5c2e4c5d71168e60492f88c7 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Fri, 23 Oct 2026 21:53:16 -0700 Subject: [PATCH] Raise ArgumentError for a zero limit in GzipReader#each_line and #readlines GzipReader#gets(0) returns "" without consuming anything, as IO#gets(0) does, so each_line(0) and readlines(0) never reached the end of the stream and allocated an empty string per iteration until the process ran out of memory. IO#each_line and IO#readlines reject a zero limit for that reason. ```ruby Zlib::GzipReader.new(StringIO.new(Zlib.gzip("ab\ncd\n"))).readlines(0) # expected: ArgumentError # actual: never returns ``` Co-Authored-By: Claude Mythos 5.1 --- ext/zlib/zlib.c | 21 +++++++++++++++++++++ test/zlib/test_zlib.rb | 14 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c index 982933b..0bd55dc 100644 --- a/ext/zlib/zlib.c +++ b/ext/zlib/zlib.c @@ -4549,6 +4549,25 @@ rb_gzreader_readline(int argc, VALUE *argv, VALUE obj) return dst; } +/* + * IO#each_line and IO#readlines reject a zero limit: gets(..., 0) returns "" + * without consuming anything, so a loop over it would never terminate. + */ +static void +gzreader_check_limit(int argc, VALUE *argv, const char *name) +{ + VALUE rs, lim; + + if (argc == 0) return; + rb_scan_args(argc, argv, "11", &rs, &lim); + if (NIL_P(lim) && !NIL_P(rs) && NIL_P(rb_check_string_type(rs))) { + lim = rs; + } + if (!NIL_P(lim) && NUM2LEN(lim) == 0) { + rb_raise(rb_eArgError, "invalid limit: 0 for %s", name); + } +} + /* * Document-method: Zlib::GzipReader#each * @@ -4561,6 +4580,7 @@ rb_gzreader_each(int argc, VALUE *argv, VALUE obj) RETURN_ENUMERATOR(obj, 0, 0); + gzreader_check_limit(argc, argv, "each_line"); while (!NIL_P(str = gzreader_gets(argc, argv, obj))) { rb_yield(str); } @@ -4576,6 +4596,7 @@ static VALUE rb_gzreader_readlines(int argc, VALUE *argv, VALUE obj) { VALUE str, dst; + gzreader_check_limit(argc, argv, "readlines"); dst = rb_ary_new(); while (!NIL_P(str = gzreader_gets(argc, argv, obj))) { rb_ary_push(dst, str); diff --git a/test/zlib/test_zlib.rb b/test/zlib/test_zlib.rb index 1f9cb81..e80a0c1 100644 --- a/test/zlib/test_zlib.rb +++ b/test/zlib/test_zlib.rb @@ -1367,6 +1367,20 @@ def test_reader_wrap } end + def test_each_line_limit_zero + # IO#each_line / IO#readlines raise for a zero limit; gets(0) returns "" + # without consuming anything, so GzipReader used to loop forever here. + s = Zlib.gzip("ab\ncd\n") + [[0], [nil, 0], ["\n", 0]].each do |args| + r = Zlib::GzipReader.new(StringIO.new(s)) + assert_raise(ArgumentError) { r.readlines(*args) } + r = Zlib::GzipReader.new(StringIO.new(s)) + assert_raise(ArgumentError) { r.each_line(*args) { } } + r = Zlib::GzipReader.new(StringIO.new(s)) + assert_equal("", r.gets(*args)) + end + end + def test_corrupted_header gz = Zlib::GzipWriter.new(StringIO.new(s = "".dup)) gz.orig_name = "X"