Skip to content

Commit 744cbeb

Browse files
Watson1978claude
andcommitted
Raise instead of hanging on a truncated frame in Zstd.decompress
decode_one_frame looped until ZSTD_decompressStream returned 0. For a truncated/incomplete frame libzstd keeps returning a non-zero "need more input" hint while consuming and producing nothing, so the loop spun forever. Because ZSTD_decompressStream is called directly (GVL held), this froze the whole VM at 100% CPU and ignored SIGTERM. A header-only frame reproduces it: Zstd.decompress("\x28\xB5\x2F\xFD") # hung forever Detect the no-progress case (no output produced and no input consumed with a non-zero return) and raise, matching the streaming decompressor which already stops when the input is exhausted. Add a regression spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b9e3c8e commit 744cbeb

2 files changed

Lines changed: 20 additions & 0 deletions

File tree

ext/zstdruby/zstdruby.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t
5151

5252
for (;;) {
5353
ZSTD_outBuffer o = (ZSTD_outBuffer){ buf, cap, 0 };
54+
size_t const in_pos_before = in.pos;
5455
size_t ret = ZSTD_decompressStream(dctx, &o, &in);
5556
if (ZSTD_isError(ret)) {
5657
xfree(buf);
@@ -62,6 +63,13 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t
6263
if (ret == 0) {
6364
break;
6465
}
66+
/* A non-zero return is a "need more input" hint, not an error, and libzstd's
67+
own noForwardProgress guard is bypassed by the early return it takes on a
68+
truncated frame header -- so the stall has to be detected here. */
69+
if (o.pos == 0 && in.pos == in_pos_before) {
70+
xfree(buf);
71+
rb_raise(rb_eRuntimeError, "ZSTD_decompressStream failed: truncated or incomplete frame");
72+
}
6573
}
6674
xfree(buf);
6775
return out;

spec/zstd-ruby_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ def to_str
103103
expect { Zstd.decompress(Object.new) }.to raise_error(TypeError)
104104
end
105105

106+
it 'should raise (not hang) on a truncated frame' do
107+
full = Zstd.compress('a' * 2000)
108+
[
109+
"\x28\xB5\x2F\xFD".b, # bare zstd magic, no body
110+
full.byteslice(0, 5),
111+
full.byteslice(0, 6),
112+
full.byteslice(0, full.bytesize / 2),
113+
].each do |truncated|
114+
expect { Zstd.decompress(truncated) }.to raise_error(RuntimeError)
115+
end
116+
end
117+
106118
class DummyForDecompress
107119
def to_str
108120
Zstd.compress('abc')

0 commit comments

Comments
 (0)