[ruby/zlib] Fix stale next_out in Zlib::Inflate#inflate(buffer:)

When a call with buffer: left the stream in progress (it raised
Zlib::NeedDict, or the block form has more input to come), the next run
reused the next_out saved by the previous call. If the buffer had been
modified in between, it pointed into freed or reallocated memory.

  d = Zlib::Deflate.new
  d.set_dictionary("dict")
  data = d.deflate("A" * 12000, Zlib::FINISH)
  inf = Zlib::Inflate.new
  buf = String.new(capacity: 1 << 14)
  begin; inf.inflate(data, buffer: buf); rescue Zlib::NeedDict; end
  buf.clear
  inf.set_dictionary("dict")
  inf.inflate("", buffer: buf)
  #=> [BUG] probable buffer overflow: 12000 for 15

Re-derive next_out/avail_out from the buffer at the start of every run,
and reset them when buffer: is dropped.

Reported by richardjiakangsun (https://hackerone.com/richardjiakangsun).

https://github.com/ruby/zlib/commit/e244392b8d

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
This commit is contained in:
Yusuke EndohandClaude Mythos 5.1 authored and git committed 2026-10-24 17:26:12 +00:00
1 parent 8074d3f475
commit 7d104f9de0
2 files changed
+100

No files matched your search

+8
View File
@@ -1154,6 +1154,12 @@ zstream_run_try(VALUE value_arg)
}
z->flags |= ZSTREAM_IN_PROGRESS;
if (ZSTREAM_REUSE_BUFFER_P(z)) {
/* always re-derive next_out; the user may have modified z->buf */
z->stream.next_out = NULL;
z->stream.avail_out = 0;
}
if (NIL_P(z->input) && len == 0) {
z->stream.next_in = (Bytef*)"";
z->stream.avail_in = 0;
@@ -2201,6 +2207,8 @@ rb_inflate_inflate_body(VALUE _arguments)
} else if (ZSTREAM_REUSE_BUFFER_P(z)) {
z->flags &= ~ZSTREAM_REUSE_BUFFER;
z->buf = Qnil;
z->stream.next_out = NULL;
z->stream.avail_out = 0;
}
rb_scan_args(argc, argv, "10", &src);
+92
View File
@@ -427,6 +427,98 @@ if defined? Zlib
}
end
def test_inflate_buffer_modified_between_calls
# In the block form the buffer is kept across calls while the stream
# is in progress; the application may modify it in between.
src = "0" * 100_000
deflated = Zlib::Deflate.deflate(src)
half = deflated.bytesize / 2
# buffer cleared between calls: the output pending in the buffer is
# discarded by the application, the rest must still be inflated
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
inflated = "".dup
z.inflate(deflated.byteslice(0, half), buffer: buf) { |chunk| inflated << chunk }
discarded = buf.bytesize
buf.clear
z.inflate(deflated.byteslice(half..), buffer: buf) { |chunk| inflated << chunk }
z.inflate(nil, buffer: buf) { |chunk| inflated << chunk }
assert_equal src.bytesize - discarded, inflated.bytesize
assert_match(/\A0*\z/, inflated)
# buffer reallocated between calls, content preserved
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
inflated = "".dup
z.inflate(deflated.byteslice(0, half), buffer: buf) { |chunk| inflated << chunk }
pending = buf.dup
buf << ("x" * 50_000)
buf.replace(pending)
z.inflate(deflated.byteslice(half..), buffer: buf) { |chunk| inflated << chunk }
z.inflate(nil, buffer: buf) { |chunk| inflated << chunk }
assert_equal src, inflated
# buffer cleared, then continued with Inflate#<< and ZStream#finish
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
inflated = "".dup
z.inflate(deflated.byteslice(0, half), buffer: buf) { |chunk| inflated << chunk }
discarded = buf.bytesize
buf.clear
z << deflated.byteslice(half..)
inflated << z.finish
assert_equal src.bytesize - discarded, inflated.bytesize
assert_match(/\A0*\z/, inflated)
end
def test_inflate_buffer_dropped_while_handling_need_dict
dict = "dictionary-" * 4
d = Zlib::Deflate.new
d.set_dictionary(dict)
deflated = d.deflate("A" * 12_000, Zlib::FINISH)
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
assert_raise(Zlib::NeedDict) { z.inflate(deflated, buffer: buf) }
buf.clear
z.set_dictionary(dict)
assert_equal "A" * 12_000, z.inflate("")
end
def test_inflate_buffer_modified_while_handling_need_dict
dict = "dictionary-" * 4
d = Zlib::Deflate.new
d.set_dictionary(dict)
deflated = d.deflate("A" * 12_000, Zlib::FINISH)
# buffer cleared (its heap block is freed) before retrying
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
assert_raise(Zlib::NeedDict) { z.inflate(deflated, buffer: buf) }
buf.clear
z.set_dictionary(dict)
assert_equal "A" * 12_000, z.inflate("", buffer: buf)
# buffer grown beyond its capacity (its heap block is reallocated)
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
assert_raise(Zlib::NeedDict) { z.inflate(deflated, buffer: buf) }
buf << ("x" * 20_000)
buf.clear
z.set_dictionary(dict)
assert_equal "A" * 12_000, z.inflate("", buffer: buf)
# buffer replaced by another string
z = Zlib::Inflate.new
buf = String.new(capacity: 1 << 14)
assert_raise(Zlib::NeedDict) { z.inflate(deflated, buffer: buf) }
buf.replace("x" * 20_000)
buf.clear
z.set_dictionary(dict)
assert_equal "A" * 12_000, z.inflate("", buffer: buf)
end
def test_inflate_chunked_break
# zeros = Zlib::Deflate.deflate("0" * 100_000)
zeros = "x\234\355\3011\001\000\000\000\302\240J\353\237\316\032\036@" \