Skip to content

Commit dfd04b6

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 2f033eb commit dfd04b6

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
if (consumed) {

‎spec/zstd-ruby_spec.rb‎

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

119+
it 'should raise (not hang) on a truncated frame' do
120+
full = Zstd.compress('a' * 2000)
121+
[
122+
"\x28\xB5\x2F\xFD".b, # bare zstd magic, no body
123+
full.byteslice(0, 5),
124+
full.byteslice(0, 6),
125+
full.byteslice(0, full.bytesize / 2),
126+
].each do |truncated|
127+
expect { Zstd.decompress(truncated) }.to raise_error(RuntimeError)
128+
end
129+
end
130+
119131
class DummyForDecompress
120132
def to_str
121133
Zstd.compress('abc')

0 commit comments

Comments
 (0)