diff --git a/src/shared/compression.c b/src/shared/compression.c index 7609921..c40c534 100644 --- a/src/shared/compression.c +++ b/src/shared/compression.c @@ -324,6 +324,20 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) { uncompressed_data->data = new_data; output.dst = new_data; output.size = buf_size; + /* Re-attempt with the larger output buffer; the truncated-frame check + * below must not reject a complete frame that merely filled the previous + * buffer exactly. */ + continue; + } + /* A positive hint with all input consumed means the frame is incomplete: a + * truncated stream would otherwise spin here forever (ZSTD_decompressStream + * keeps returning the same hint). Fail instead of burning CPU. */ + if (ret != 0 && input.pos == input.size) { + log_message(LOG_LEVEL_ERROR, + "Truncated zstd frame: input exhausted with %zu bytes still expected", ret); + data_destroy(uncompressed_data); + uncompressed_data = NULL; + goto cleanup; } } while (ret > 0); diff --git a/tests/test_compression.c b/tests/test_compression.c index da51531..2ebb8da 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -6,6 +6,7 @@ #include "utils.h" #include #include +#include #include #include @@ -211,9 +212,47 @@ static void test_data_compress_reused_contexts_multithreaded() { compression_free_thread_contexts(); } +/* A truncated zstd frame used to make the decompressor spin forever: the + * stream call keeps returning a positive hint with all input consumed. Run the + * decompression in a child with an alarm so a regression (infinite loop) is + * caught as a timeout failure instead of hanging the whole unit suite. */ +static void test_data_decompress_truncated_frame_fails() { + const char* original = + "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."; + size_t len = strlen(original); + char* buf = malloc(len); + EXPECT_NOT_NULL(buf); + memcpy(buf, original, len); + Data* input = data_create(buf, len); + EXPECT_NOT_NULL(input); + + pid_t pid = fork(); + EXPECT_TRUE(pid >= 0); + if (pid == 0) { + alarm(10); /* kills the child if the decompressor hangs */ + Data* compressed = data_compress(input, 3); + if (compressed && compressed->size > 1) { + compressed->size -= 1; /* drop the final byte: frame is now incomplete */ + Data* out = data_decompress(compressed); + bool failed_cleanly = (out == NULL); + data_destroy(out); + data_destroy(compressed); + _exit(failed_cleanly ? 0 : 1); + } + data_destroy(compressed); + _exit(2); + } + int status; + waitpid(pid, &status, 0); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); + + data_destroy(input); +} + void test_compression() { test_data_compress_decompress_roundtrip(); test_data_compress_decompress_large(); + test_data_decompress_truncated_frame_fails(); test_skip_compress_suffix_matching(); test_data_compress_with_threads_roundtrip(); test_data_compress_reused_contexts_multithreaded();