From 4feb75957aec000797f8130dca30f2b8e2db37ee Mon Sep 17 00:00:00 2001 From: TapTap Date: Sat, 8 Aug 2026 20:08:30 +0200 Subject: [PATCH] fix: validate delta output and integration test skips --- src/shared/delta.c | 14 ++++++++++++-- tests/integration/test_features.py | 2 +- tests/integration/test_ssh.py | 16 +++++++++++++--- tests/test_delta.c | 17 +++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/shared/delta.c b/src/shared/delta.c index f9f7c84..d166f30 100644 --- a/src/shared/delta.c +++ b/src/shared/delta.c @@ -458,7 +458,8 @@ Delta* delta_deserialize(const Data* data) { void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, uint32_t block_size) { - if (!old_data || !delta) + if (!old_data || !delta || (delta->new_file_size > 0 && delta->instructions == NULL) || + (delta->instruction_count > 0 && block_size == 0)) return NULL; void* output = malloc((size_t)delta->new_file_size); @@ -472,10 +473,15 @@ void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, for (uint32_t i = 0; i < delta->instruction_count; i++) { if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH) { uint64_t src_offset = (uint64_t)delta->instructions[i].match.block_index * block_size; + if (src_offset > UINT64_MAX - delta->instructions[i].match.block_offset) { + free(output); + return NULL; + } src_offset += delta->instructions[i].match.block_offset; uint32_t len = delta->instructions[i].match.length; - if (src_offset + len > old_size) { + if (src_offset > old_size || (uint64_t)len > old_size - src_offset || + out_pos > delta->new_file_size || (uint64_t)len > delta->new_file_size - out_pos) { free(output); return NULL; } @@ -483,6 +489,10 @@ void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, out_pos += len; } else { uint32_t len = delta->instructions[i].literal.length; + if (out_pos > delta->new_file_size || (uint64_t)len > delta->new_file_size - out_pos) { + free(output); + return NULL; + } memcpy(out + out_pos, delta->instructions[i].literal.data, len); out_pos += len; } diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index 63a5bf6..65bac5c 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -238,7 +238,7 @@ class TestProgress: ) assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}" output = result.stdout + result.stderr - assert len(output) >= 0 + assert output, "--progress produced no output" class TestBandwidthLimit: diff --git a/tests/integration/test_ssh.py b/tests/integration/test_ssh.py index 82b86ef..bad3790 100644 --- a/tests/integration/test_ssh.py +++ b/tests/integration/test_ssh.py @@ -52,9 +52,11 @@ def _check_ssh(): pass +_check_ssh() + + @pytest.fixture(scope="module", autouse=True) def setup_test_data(): - _check_ssh() if SSH_AVAILABLE: generate_test_files(SOURCE_DIR, full=False) clean_dir(DEST_DIR) @@ -84,8 +86,12 @@ def _run_ssh_test(name, flags, expected_missing=None): return make_result(name, True, duration) -@pytest.mark.skipif(not SSH_AVAILABLE, reason="SSH to localhost not available") class TestSSHStandard: + @pytest.fixture(autouse=True) + def require_ssh(self): + if not SSH_AVAILABLE: + pytest.skip("SSH to localhost not available") + def test_standard(self): r = _run_ssh_test("SSH (localhost)", []) assert r["status"] == "Success", r["error"] @@ -119,8 +125,12 @@ class TestSSHStandard: assert r["status"] == "Success", r["error"] -@pytest.mark.skipif(not SSH_AVAILABLE, reason="SSH to localhost not available") class TestSSHFeatures: + @pytest.fixture(autouse=True) + def require_ssh(self): + if not SSH_AVAILABLE: + pytest.skip("SSH to localhost not available") + def test_archive(self): r = _run_ssh_test("SSH Archive (-a)", ["-a"]) assert r["status"] == "Success", r["error"] diff --git a/tests/test_delta.c b/tests/test_delta.c index 09a6467..a55309d 100644 --- a/tests/test_delta.c +++ b/tests/test_delta.c @@ -323,6 +323,22 @@ static void test_large_file_delta() { free(new_data); } +static void test_delta_apply_rejects_output_overflow() { + uint8_t old_data[8] = {0}; + uint8_t literal_data[2] = {'x', 'y'}; + DeltaInstruction instruction = { + .type = DELTA_INSTR_LITERAL, + .literal = {.data = literal_data, .length = sizeof(literal_data)}, + }; + Delta delta = { + .new_file_size = 1, + .instruction_count = 1, + .instructions = &instruction, + }; + + EXPECT_TRUE(delta_apply(old_data, sizeof(old_data), &delta, 1) == NULL); +} + void test_delta() { test_adler32_basic(); test_adler32_different_data(); @@ -338,4 +354,5 @@ void test_delta() { test_should_attempt(); test_is_worthwhile(); test_large_file_delta(); + test_delta_apply_rejects_output_overflow(); } -- 2.52.0