Skip to content

Raise instead of hanging on a truncated frame in Zstd.decompress#143

Open
Watson1978 wants to merge 1 commit into
SpringMT:mainfrom
Watson1978:fix/decompress-truncated-frame-hang
Open

Raise instead of hanging on a truncated frame in Zstd.decompress#143
Watson1978 wants to merge 1 commit into
SpringMT:mainfrom
Watson1978:fix/decompress-truncated-frame-hang

Conversation

@Watson1978

Copy link
Copy Markdown
Contributor

Summary

Zstd.decompress could hang forever on a truncated / incomplete frame.

decode_one_frame loops until ZSTD_decompressStream returns 0. For a
truncated frame, libzstd keeps returning a non-zero "need more input" hint
while consuming and producing nothing, so the loop never terminates.
Because ZSTD_decompressStream is called directly (with the GVL held),
this freezes the whole Ruby VM at 100% CPU and does not even respond to
SIGTERM.

A header-only frame reproduces it:

Zstd.decompress("\x28\xB5\x2F\xFD")  # hangs forever (must be SIGKILLed)

This is reachable from any input source that feeds attacker-controlled
compressed bytes to Zstd.decompress (e.g. a compressed network payload).

Fix

Detect the no-progress case — no output produced and no input consumed
with a non-zero return — and raise, instead of looping. This mirrors
Zstd::StreamingDecompress#decompress, which already stops once the input
is exhausted (and returns gracefully on the same input).

     size_t const in_pos_before = in.pos;
     size_t ret = ZSTD_decompressStream(dctx, &o, &in);
     ...
     if (ret == 0) { break; }
+    if (o.pos == 0 && in.pos == in_pos_before) {
+      xfree(buf);
+      rb_raise(rb_eRuntimeError, "ZSTD_decompressStream failed: truncated or incomplete frame");
+    }

Tests

Added a regression spec asserting that truncated frames (bare magic and
several truncation lengths) raise instead of hanging. Full suite passes
(67 examples, 0 failures). Valid single/concatenated frames are
unaffected.

🤖 Generated with Claude Code

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>
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.

1 participant