Skip to content

Fix Zlib::Inflate#inflate(buffer:) when the buffer changes between calls - #153

Open
mame wants to merge 3 commits into
ruby:masterfrom
mame:zlib-inflate-buffer-fix
Open

mame wants to merge 3 commits into
ruby:masterfrom
mame:zlib-inflate-buffer-fix

Conversation

@mame

@mame mame commented Oct 23, 2026

Copy link
Copy Markdown
Member

Zlib::Inflate#inflate(buffer:) did not handle the buffer, or the buffer: argument itself, changing between calls while the stream was in progress. The worst case is the reported one: if the buffer was modified after a call left the stream in progress (it raised Zlib::NeedDict, or the block form has more input to come), the next run reused the stale next_out and zlib wrote into freed or reallocated memory.

This PR has three commits. Each commit message has a minimal reproduction.

  1. Fix stale next_out in Zlib::Inflate#inflate(buffer:): the reported issue, reported by richardjiakangsun (https://hackerone.com/richardjiakangsun). When a user-provided buffer is in use, next_out/avail_out are re-derived from the buffer at the start of every run, so #inflate, #<<, #finish and the block form are all covered. They are also reset when buffer: is dropped. Code paths without buffer: are unchanged.
  2. Clear ZSTREAM_REUSE_BUFFER when the output buffer is detached: found while working on 1. After a call with buffer: returned its buffer, the flag stayed set, so continuing with #<< and #finish returned a hidden String and crashed.
  3. Carry over pending output when the inflate buffer is replaced: in the block form, output not yet yielded stays in the buffer between calls. It was silently dropped when a different buffer (or none) was given for the next call. This commit copies it into the new buffer. Raising ArgumentError in that case would be a reasonable alternative, and I'm happy to switch if you prefer.

One visible behavior change from 1: with buffer:, ZStream#avail_out= no longer changes the chunk size in the block form. Chunks stay at 16 KiB.

mame and others added 3 commits October 24, 2026 04:51
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).

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
The flag stayed set after a call with buffer: had returned its buffer,
so an internal buffer allocated by a later Inflate#<< was treated as
user-provided and never revealed.

  data = Zlib::Deflate.deflate("A" * 50000)
  half = data.bytesize / 2
  inf = Zlib::Inflate.new
  inf.inflate(data[0, half], buffer: String.new)
  inf << data[half..]
  inf.finish.class
  #=> [BUG] Segmentation fault

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
In the block form, output not yet yielded stays in the buffer between
calls. It was silently dropped when another buffer (or none) was given
for the next call.

  data = Zlib::Deflate.deflate("A" * 50000)
  half = data.bytesize / 2
  inf = Zlib::Inflate.new
  out = +""
  inf.inflate(data[0, half], buffer: String.new) { |c| out << c }
  inf.inflate(data[half..], buffer: String.new) { |c| out << c }
  out << inf.finish
  out.bytesize
  # expected: 50000
  # actual:   47806

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
@mame
mame requested review from jeremyevans and nobu October 23, 2026 19:56

@jeremyevans jeremyevans left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good. This doesn't seem like it would affect most applications using zlib inflate, as applications tend to either always use buffer or never use buffer for a single inflate. I would guess that mixed use for a single inflate is uncommon.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants