Merge branch 'fix/audit-compression' into fix/audit-cycle
This commit is contained in:
@@ -16,7 +16,14 @@
|
|||||||
#include <zstd.h>
|
#include <zstd.h>
|
||||||
|
|
||||||
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
|
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
|
||||||
#define MAX_DECOMPRESSED_SIZE (100ULL * 1024 * 1024) /* 100 MB hard ceiling */
|
|
||||||
|
/* Hard ceiling for a single decompression. The sender compresses whole files
|
||||||
|
* up to the protocol's whole-file receive bound, so the decompressor must
|
||||||
|
* accept payloads that large; referencing the protocol constant keeps the two
|
||||||
|
* bounds from drifting apart (they previously did: a 100 MB ceiling rejected
|
||||||
|
* 100-256 MB files). This remains a real bomb guard -- every allocation in the
|
||||||
|
* paths below is clamped to it -- so it must not exceed the protocol bound. */
|
||||||
|
#define MAX_DECOMPRESSED_SIZE MAX_RECEIVE_WHOLE_FILE_SIZE
|
||||||
|
|
||||||
/* rsync 3.4.1's built-in skip-compress suffix list (the `--skip-compress`
|
/* rsync 3.4.1's built-in skip-compress suffix list (the `--skip-compress`
|
||||||
* defaults, in the man page's order). rsync stores it as space-separated
|
* defaults, in the man page's order). rsync stores it as space-separated
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#include "compression.h"
|
#include "compression.h"
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "protocol.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -216,6 +218,67 @@ static void test_data_decompress_unknown_size_frame() {
|
|||||||
data_destroy(frame);
|
data_destroy(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The receiver advertises MAX_RECEIVE_WHOLE_FILE_SIZE (256 MiB) and the sender
|
||||||
|
* compresses whole files, so the decompressor's internal ceiling must match that
|
||||||
|
* protocol bound. A 130 MiB payload -- above the old 100 MiB ceiling but below
|
||||||
|
* the protocol bound -- must round-trip through
|
||||||
|
* data_decompress_limited(..., MAX_RECEIVE_WHOLE_FILE_SIZE). The payload is all
|
||||||
|
* zeros so it compresses to a tiny frame while still declaring its full size. */
|
||||||
|
static void test_data_decompress_limited_whole_file_ceiling() {
|
||||||
|
const size_t size = 130ULL * 1024 * 1024;
|
||||||
|
Data* input = data_create_empty(size);
|
||||||
|
EXPECT_NOT_NULL(input);
|
||||||
|
memset(input->data, 0, size);
|
||||||
|
input->size = size;
|
||||||
|
|
||||||
|
/* Threaded zstd stores the content size in the frame header, as the sender
|
||||||
|
* does for whole files, so the decompressor sees the exact declared size. */
|
||||||
|
Data* compressed = data_compress_codec(input, COMPRESSION_ALGO_ZSTD, 1, 2);
|
||||||
|
EXPECT_NOT_NULL(compressed);
|
||||||
|
/* Premise: the declared size sits between the old 100 MiB cap and the
|
||||||
|
* protocol whole-file bound -- exactly the range that used to be rejected. */
|
||||||
|
unsigned long long declared =
|
||||||
|
ZSTD_getFrameContentSize((uint8_t*)compressed->data + 1, compressed->size - 1);
|
||||||
|
EXPECT_TRUE(declared > (100ULL * 1024 * 1024));
|
||||||
|
EXPECT_TRUE(declared <= MAX_RECEIVE_WHOLE_FILE_SIZE);
|
||||||
|
|
||||||
|
Data* out = data_decompress_limited(compressed, MAX_RECEIVE_WHOLE_FILE_SIZE);
|
||||||
|
EXPECT_NOT_NULL(out);
|
||||||
|
EXPECT_EQ_INT((int)out->size, (int)size);
|
||||||
|
EXPECT_EQ_INT(memcmp(out->data, input->data, size), 0);
|
||||||
|
|
||||||
|
data_destroy(out);
|
||||||
|
data_destroy(compressed);
|
||||||
|
data_destroy(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A frame declaring an uncompressed size above the hard ceiling is still
|
||||||
|
* rejected before any allocation, even when the caller passes a limit higher
|
||||||
|
* than the protocol bound. The 13-byte header is a valid zstd frame header with
|
||||||
|
* an 8-byte content size and no blocks; rejection happens at the size check. */
|
||||||
|
static void test_data_decompress_limited_rejects_over_ceiling() {
|
||||||
|
const uint64_t declared = MAX_RECEIVE_WHOLE_FILE_SIZE + 1;
|
||||||
|
Data* frame = data_create_empty(1 + 13);
|
||||||
|
EXPECT_NOT_NULL(frame);
|
||||||
|
uint8_t* p = (uint8_t*)frame->data;
|
||||||
|
p[0] = (uint8_t)COMPRESSION_ALGO_ZSTD;
|
||||||
|
p[1] = 0x28; /* zstd magic number, little-endian */
|
||||||
|
p[2] = 0xB5;
|
||||||
|
p[3] = 0x2F;
|
||||||
|
p[4] = 0xFD;
|
||||||
|
p[5] = 0xE0; /* Frame_Header_Descriptor: 8-byte content size, single segment */
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
p[6 + i] = (uint8_t)((declared >> (8 * i)) & 0xff);
|
||||||
|
frame->size = 1 + 13;
|
||||||
|
/* Guard the premise: zstd reads back exactly the declared over-ceiling size. */
|
||||||
|
EXPECT_EQ_INT((int)ZSTD_getFrameContentSize(p + 1, frame->size - 1), (int)declared);
|
||||||
|
|
||||||
|
EXPECT_NULL(data_decompress_limited(frame, MAX_RECEIVE_WHOLE_FILE_SIZE * 2));
|
||||||
|
EXPECT_NULL(data_decompress_limited(frame, MAX_RECEIVE_WHOLE_FILE_SIZE));
|
||||||
|
|
||||||
|
data_destroy(frame);
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int id;
|
int id;
|
||||||
int iterations;
|
int iterations;
|
||||||
@@ -467,6 +530,8 @@ 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_unknown_size_frame();
|
test_data_decompress_unknown_size_frame();
|
||||||
|
test_data_decompress_limited_whole_file_ceiling();
|
||||||
|
test_data_decompress_limited_rejects_over_ceiling();
|
||||||
test_data_decompress_truncated_frame_fails();
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user