diff --git a/ext/zstdruby/zstdruby.c b/ext/zstdruby/zstdruby.c index 1649fe5..54608ad 100644 --- a/ext/zstdruby/zstdruby.c +++ b/ext/zstdruby/zstdruby.c @@ -51,6 +51,7 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t for (;;) { ZSTD_outBuffer o = (ZSTD_outBuffer){ buf, cap, 0 }; + size_t const in_pos_before = in.pos; size_t ret = ZSTD_decompressStream(dctx, &o, &in); if (ZSTD_isError(ret)) { xfree(buf); @@ -62,6 +63,13 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t if (ret == 0) { break; } + if (o.pos == 0 && in.pos == in_pos_before) { + /* No progress: the frame is truncated/incomplete. Without this guard the + loop would spin forever calling ZSTD_decompressStream on exhausted + input (see decompress of a header-only frame). */ + xfree(buf); + rb_raise(rb_eRuntimeError, "ZSTD_decompressStream failed: truncated or incomplete frame"); + } } xfree(buf); return out; diff --git a/spec/zstd-ruby_spec.rb b/spec/zstd-ruby_spec.rb index ddfae47..c302217 100644 --- a/spec/zstd-ruby_spec.rb +++ b/spec/zstd-ruby_spec.rb @@ -103,6 +103,18 @@ def to_str expect { Zstd.decompress(Object.new) }.to raise_error(TypeError) end + it 'should raise (not hang) on a truncated frame' do + full = Zstd.compress('a' * 2000) + [ + "\x28\xB5\x2F\xFD".b, # bare zstd magic, no body + full.byteslice(0, 5), + full.byteslice(0, 6), + full.byteslice(0, full.bytesize / 2), + ].each do |truncated| + expect { Zstd.decompress(truncated) }.to raise_error(RuntimeError) + end + end + class DummyForDecompress def to_str Zstd.compress('abc')