fix(compression): fail truncated zstd frames instead of spinning

data_decompress_limited() looped while ZSTD_decompressStream() returned a
positive hint.  A truncated frame keeps returning that hint with all input
consumed, so a malformed/truncated payload spun forever (CPU DoS).  Detect
input exhaustion with an incomplete frame and fail via the existing cleanup,
skipping the check when the output buffer merely needs to grow first.

Add a fork+alarm regression test that truncates a valid frame and asserts
decompression returns NULL promptly.
This commit is contained in:
2026-09-14 16:39:05 +02:00
parent df887c73b1
commit e48f19ee2b
2 changed files with 53 additions and 0 deletions
+14
View File
@@ -324,6 +324,20 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
uncompressed_data->data = new_data; uncompressed_data->data = new_data;
output.dst = new_data; output.dst = new_data;
output.size = buf_size; 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); } while (ret > 0);
+39
View File
@@ -6,6 +6,7 @@
#include "utils.h" #include "utils.h"
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/wait.h>
#include <threads.h> #include <threads.h>
#include <unistd.h> #include <unistd.h>
@@ -211,9 +212,47 @@ static void test_data_compress_reused_contexts_multithreaded() {
compression_free_thread_contexts(); 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() { void test_compression() {
test_data_compress_decompress_roundtrip(); test_data_compress_decompress_roundtrip();
test_data_compress_decompress_large(); test_data_compress_decompress_large();
test_data_decompress_truncated_frame_fails();
test_skip_compress_suffix_matching(); test_skip_compress_suffix_matching();
test_data_compress_with_threads_roundtrip(); test_data_compress_with_threads_roundtrip();
test_data_compress_reused_contexts_multithreaded(); test_data_compress_reused_contexts_multithreaded();