From 0df1c01759663873e0d24df03f512ff31d7315ba Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Fri, 6 Sep 2024 18:55:00 +0300 Subject: [PATCH 01/10] sstables: Add digest check in checksummed data source The checksummed data source verifies the checksum of each chunk in the data files of uncompressed SSTables. This is being leveraged by scrub in validation mode. Extend the data source to check the digest (full checksum) as well. Unlike checksums, this is added as an optional feature so that SSTables without a digest can still be validated in a per-chunk basis. To enable this, the caller needs to set the template parameter `check_digest` to true, and provide the expected digest. The data source calculates the digest incrementally through multiple get() calls and compares against the expected digest after reading the whole file range. If there is a mismatch, it throws an exception. Checking the digest requires reading the whole data file. If this cannot be satisfied (e.g., due to partial read or skip()), the data source fails immediately. If the user has successfully read the whole file range, it can be safely assumed that the digest is valid. Signed-off-by: Nikos Dragazis --- sstables/checksummed_data_source.cc | 71 ++++++++++++++++++++++------- sstables/checksummed_data_source.hh | 8 +++- sstables/data_source_types.hh | 24 ++++++++++ sstables/sstables.cc | 4 +- 4 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 sstables/data_source_types.hh diff --git a/sstables/checksummed_data_source.cc b/sstables/checksummed_data_source.cc index 6e2102add5..d44a0b6fc1 100644 --- a/sstables/checksummed_data_source.cc +++ b/sstables/checksummed_data_source.cc @@ -16,6 +16,7 @@ #include "types.hh" #include "exceptions.hh" #include "checksum_utils.hh" +#include "data_source_types.hh" namespace sstables { @@ -23,11 +24,13 @@ extern logging::logger sstlog; // File data source implementation for SSTables with attached checksum // data and no compression -template +template class checksummed_file_data_source_impl : public data_source_impl { std::optional> _input_stream; const checksum& _checksum; + [[no_unique_address]] digest_members _digests; uint64_t _chunk_size_trailing_zeros; + uint64_t _file_len; uint64_t _underlying_pos; uint64_t _pos; uint64_t _beg_pos; @@ -35,8 +38,10 @@ class checksummed_file_data_source_impl : public data_source_impl { public: checksummed_file_data_source_impl(file f, uint64_t file_len, const checksum& checksum, uint64_t pos, size_t len, - file_input_stream_options options) + file_input_stream_options options, + std::optional digest) : _checksum(checksum) + , _file_len(file_len) , _pos(pos) , _beg_pos(pos) , _end_pos(pos + len) @@ -53,16 +58,27 @@ public: on_internal_error(sstlog, format("Invalid chunk size: {}", chunk_size)); } _chunk_size_trailing_zeros = count_trailing_zeros(chunk_size); - if (_pos > file_len) { + if (_pos > _file_len) { on_internal_error(sstlog, "attempt to read beyond end"); } - if (len == 0 || _pos == file_len) { + if (len == 0 || _pos == _file_len) { // Nothing to read _end_pos = _pos; return; } - if (len > file_len - _pos) { - _end_pos = file_len; + if (len > _file_len - _pos) { + _end_pos = _file_len; + } + if constexpr (check_digest) { + if (!digest) { + on_internal_error(sstlog, "Requested digest check but no digest was provided."); + } + if (_end_pos - _pos < _file_len) { + on_internal_error(sstlog, seastar::format( + "Cannot check digest with a partial read: current pos={}, end pos={}, file len={}", + _pos, _end_pos, _file_len)); + } + _digests = {*digest, ChecksumType::init_checksum()}; } auto start = _beg_pos & ~(chunk_size - 1); auto end = (_end_pos & ~(chunk_size - 1)) + chunk_size; @@ -88,9 +104,20 @@ public: if (expected_checksum != actual_checksum) { throw sstables::malformed_sstable_exception(format("Checksummed chunk of size {} at file offset {} failed checksum: expected={}, actual={}", buf.size(), _underlying_pos, expected_checksum, actual_checksum)); } + + if constexpr (check_digest) { + _digests.actual_digest = checksum_combine_or_feed(_digests.actual_digest, actual_checksum, buf.begin(), buf.size()); + } + buf.trim_front(_pos & (chunk_size - 1)); _pos += buf.size(); _underlying_pos += chunk_size; + + if constexpr (check_digest) { + if (_pos == _file_len && _digests.expected_digest != _digests.actual_digest) { + throw malformed_sstable_exception(seastar::format("Digest mismatch: expected={}, actual={}", _digests.expected_digest, _digests.actual_digest)); + } + } return buf; }); } @@ -103,6 +130,9 @@ public: } virtual future> skip(uint64_t n) override { + if constexpr (check_digest) { + on_internal_error(sstlog, "Tried to skip on a data source for which digest check has been requested."); + } auto chunk_size = _checksum.chunk_size; if (_pos + n > _end_pos) { on_internal_error(sstlog, format("Skipping over the end position is disallowed: current pos={}, end pos={}, skip len={}", _pos, _end_pos, n)); @@ -120,37 +150,44 @@ public: } }; -template +template class checksummed_file_data_source : public data_source { public: checksummed_file_data_source(file f, uint64_t file_len, const checksum& checksum, - uint64_t offset, size_t len, file_input_stream_options options) - : data_source(std::make_unique>( - std::move(f), file_len, checksum, offset, len, std::move(options))) + uint64_t offset, size_t len, file_input_stream_options options, + std::optional digest) + : data_source(std::make_unique>( + std::move(f), file_len, checksum, offset, len, std::move(options), digest)) {} }; template inline input_stream make_checksummed_file_input_stream( file f, uint64_t file_len, const checksum& checksum, uint64_t offset, - size_t len, file_input_stream_options options) + size_t len, file_input_stream_options options, std::optional digest) { - return input_stream(checksummed_file_data_source( - std::move(f), file_len, checksum, offset, len, std::move(options))); + if (digest) { + return input_stream(checksummed_file_data_source( + std::move(f), file_len, checksum, offset, len, std::move(options), digest)); + } + return input_stream(checksummed_file_data_source( + std::move(f), file_len, checksum, offset, len, std::move(options), digest)); } input_stream make_checksummed_file_k_l_format_input_stream( file f, uint64_t file_len, const checksum& checksum, uint64_t offset, - size_t len, file_input_stream_options options) + size_t len, file_input_stream_options options, std::optional digest) { - return make_checksummed_file_input_stream(std::move(f), file_len, checksum, offset, len, std::move(options)); + return make_checksummed_file_input_stream(std::move(f), file_len, + checksum, offset, len, std::move(options), digest); } input_stream make_checksummed_file_m_format_input_stream( file f, uint64_t file_len, const checksum& checksum, uint64_t offset, - size_t len, file_input_stream_options options) + size_t len, file_input_stream_options options, std::optional digest) { - return make_checksummed_file_input_stream(std::move(f), file_len, checksum, offset, len, std::move(options)); + return make_checksummed_file_input_stream(std::move(f), file_len, + checksum, offset, len, std::move(options), digest); } } diff --git a/sstables/checksummed_data_source.hh b/sstables/checksummed_data_source.hh index 793095b585..df8ee42449 100644 --- a/sstables/checksummed_data_source.hh +++ b/sstables/checksummed_data_source.hh @@ -17,9 +17,13 @@ namespace sstables { input_stream make_checksummed_file_k_l_format_input_stream(file f, uint64_t file_len, const sstables::checksum& checksum, - uint64_t offset, size_t len, class file_input_stream_options options); + uint64_t offset, size_t len, + class file_input_stream_options options, + std::optional digest); input_stream make_checksummed_file_m_format_input_stream(file f, uint64_t file_len, const sstables::checksum& checksum, - uint64_t offset, size_t len, class file_input_stream_options options); + uint64_t offset, size_t len, + class file_input_stream_options options, + std::optional digest); } \ No newline at end of file diff --git a/sstables/data_source_types.hh b/sstables/data_source_types.hh new file mode 100644 index 0000000000..9bf252323b --- /dev/null +++ b/sstables/data_source_types.hh @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024-present ScyllaDB + */ + +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#pragma once + +#include + +namespace sstables { + +template +struct digest_members { + uint32_t expected_digest; + uint32_t actual_digest; +}; + +template<> +struct digest_members {}; + +} \ No newline at end of file diff --git a/sstables/sstables.cc b/sstables/sstables.cc index 2fbe819468..1ee8a42760 100644 --- a/sstables/sstables.cc +++ b/sstables/sstables.cc @@ -2487,10 +2487,10 @@ input_stream sstable::data_stream(uint64_t pos, size_t len, auto file_len = data_size(); if (_version >= sstable_version_types::mc) { return make_checksummed_file_m_format_input_stream(f, file_len, - *checksum, pos, len, std::move(options)); + *checksum, pos, len, std::move(options), std::nullopt); } else { return make_checksummed_file_k_l_format_input_stream(f, file_len, - *checksum, pos, len, std::move(options)); + *checksum, pos, len, std::move(options), std::nullopt); } } return make_file_input_stream(f, pos, len, std::move(options)); From c893f06409f64f05690e58ed22dd355ab9729350 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Mon, 9 Sep 2024 15:08:37 +0300 Subject: [PATCH 02/10] sstables: Add digest check in compressed data source Following the addition of digest check in the checksummed data source, add the same feature to the compressed data source as well. This ensures consistent behavior across any type of SSTable. This is added as an optional feature so that we can preserve the current behavior, that is verify only the per-chunk checksums during normal user reads. To ensure zero cost at runtime when disabled, we introduce the on/off switch as a template parameter. The digest calculation for compressed SSTables depends on the SSTable format, hence the new template argument for the checksum mode. This is consistent with the compressed data sink. Signed-off-by: Nikos Dragazis --- sstables/compress.cc | 91 +++++++++++++++++++++++++++----------- sstables/compress.hh | 6 ++- sstables/sstables.cc | 6 +-- test/boost/sstable_test.cc | 2 +- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/sstables/compress.cc b/sstables/compress.cc index d261833564..47368f36e5 100644 --- a/sstables/compress.cc +++ b/sstables/compress.cc @@ -24,6 +24,7 @@ #include "utils/assert.hh" #include "utils/class_registrator.hh" #include "reader_permit.hh" +#include "data_source_types.hh" namespace sstables { @@ -335,12 +336,21 @@ compression::locate(uint64_t position, const compression::segmented_offsets::acc } -template +// For SSTables 2.x (formats 'ka' and 'la'), the full checksum is a combination of checksums of compressed chunks. +// For SSTables 3.x (format 'mc'), however, it is supposed to contain the full checksum of the file written so +// the per-chunk checksums also count. +enum class compressed_checksum_mode { + checksum_chunks_only, + checksum_all, +}; + +template class compressed_file_data_source_impl : public data_source_impl { std::optional> _input_stream; sstables::compression* _compression_metadata; sstables::compression::segmented_offsets::accessor _offsets; sstables::local_compression _compression; + [[no_unique_address]] sstables::digest_members _digests; reader_permit _permit; uint64_t _underlying_pos; uint64_t _pos; @@ -348,19 +358,20 @@ class compressed_file_data_source_impl : public data_source_impl { uint64_t _end_pos; public: compressed_file_data_source_impl(file f, sstables::compression* cm, - uint64_t pos, size_t len, file_input_stream_options options, reader_permit permit) + uint64_t pos, size_t len, file_input_stream_options options, + reader_permit permit, std::optional digest) : _compression_metadata(cm) , _offsets(_compression_metadata->offsets.get_accessor()) , _compression(*cm) , _permit(std::move(permit)) { - _beg_pos = pos; + _pos = _beg_pos = pos; if (pos > _compression_metadata->uncompressed_file_length()) { throw std::runtime_error("attempt to uncompress beyond end"); } if (len == 0 || pos == _compression_metadata->uncompressed_file_length()) { // Nothing to read - _end_pos = _pos = _beg_pos; + _end_pos = _pos; return; } if (len <= _compression_metadata->uncompressed_file_length() - pos) { @@ -368,6 +379,17 @@ public: } else { _end_pos = _compression_metadata->uncompressed_file_length(); } + if constexpr (check_digest) { + if (!digest) { + on_internal_error(sstables::sstlog, "Requested digest check but no digest was provided."); + } + if (_end_pos - _pos < _compression_metadata->uncompressed_file_length()) { + on_internal_error(sstables::sstlog, seastar::format( + "Cannot check digest with a partial read: current pos={}, end pos={}, uncompressed file len={}", + _pos, _end_pos, _compression_metadata->uncompressed_file_length())); + } + _digests = {*digest, ChecksumType::init_checksum()}; + } // _beg_pos and _end_pos specify positions in the compressed stream. // We need to translate them into a range of uncompressed chunks, // and open a file_input_stream to read that range. @@ -378,7 +400,6 @@ public: end.chunk_start + end.chunk_len - start.chunk_start, std::move(options)); _underlying_pos = start.chunk_start; - _pos = _beg_pos; } virtual future> get() override { if (_pos >= _end_pos) { @@ -410,6 +431,15 @@ public: throw sstables::malformed_sstable_exception(format("compressed chunk of size {} at file offset {} failed checksum, expected={}, actual={}", addr.chunk_len, _underlying_pos, expected_checksum, actual_checksum)); } + if constexpr (check_digest) { + _digests.actual_digest = checksum_combine_or_feed(_digests.actual_digest, actual_checksum, buf.get(), compressed_len); + if constexpr (mode == compressed_checksum_mode::checksum_all) { + uint32_t be_actual_checksum = cpu_to_be(actual_checksum); + _digests.actual_digest = ChecksumType::checksum(_digests.actual_digest, + reinterpret_cast(&be_actual_checksum), sizeof(be_actual_checksum)); + } + } + // We know that the uncompressed data will take exactly // chunk_length bytes (or less, if reading the last chunk). temporary_buffer out( @@ -424,6 +454,12 @@ public: _pos += out.size(); _underlying_pos += addr.chunk_len; + if constexpr (check_digest) { + if (_pos == _compression_metadata->uncompressed_file_length() + && _digests.expected_digest != _digests.actual_digest) { + throw sstables::malformed_sstable_exception(seastar::format("Digest mismatch: expected={}, actual={}", _digests.expected_digest, _digests.actual_digest)); + } + } return make_tracked_temporary_buffer(std::move(out), std::move(res_units)); }); }); @@ -437,6 +473,9 @@ public: } virtual future> skip(uint64_t n) override { + if constexpr (check_digest) { + on_internal_error(sstables::sstlog, "Tried to skip on a data source for which digest check has been requested."); + } if (_pos + n > _end_pos) { on_internal_error(sstables::sstlog, format("Skipping over the end position is disallowed: current pos={}, end pos={}, skip len={}", _pos, _end_pos, n)); } @@ -454,33 +493,31 @@ public: } }; -template +template class compressed_file_data_source : public data_source { public: compressed_file_data_source(file f, sstables::compression* cm, - uint64_t offset, size_t len, file_input_stream_options options, reader_permit permit) - : data_source(std::make_unique>( - std::move(f), cm, offset, len, std::move(options), std::move(permit))) + uint64_t offset, size_t len, file_input_stream_options options, reader_permit permit, + std::optional digest) + : data_source(std::make_unique>( + std::move(f), cm, offset, len, std::move(options), std::move(permit), digest)) {} }; -template +template inline input_stream make_compressed_file_input_stream( file f, sstables::compression *cm, uint64_t offset, size_t len, - file_input_stream_options options, reader_permit permit) + file_input_stream_options options, reader_permit permit, + std::optional digest) { - return input_stream(compressed_file_data_source( - std::move(f), cm, offset, len, std::move(options), std::move(permit))); + if (digest) [[unlikely]] { + return input_stream(compressed_file_data_source( + std::move(f), cm, offset, len, std::move(options), std::move(permit), digest)); + } + return input_stream(compressed_file_data_source( + std::move(f), cm, offset, len, std::move(options), std::move(permit), digest)); } -// For SSTables 2.x (formats 'ka' and 'la'), the full checksum is a combination of checksums of compressed chunks. -// For SSTables 3.x (format 'mc'), however, it is supposed to contain the full checksum of the file written so -// the per-chunk checksums also count. -enum class compressed_checksum_mode { - checksum_chunks_only, - checksum_all, -}; - // compressed_file_data_sink_impl works as a filter for a file output stream, // where the buffer flushed will be compressed and its checksum computed, then // the result passed to a regular output stream. @@ -582,15 +619,19 @@ inline output_stream make_compressed_file_output_stream(output_stream sstables::make_compressed_file_k_l_format_input_stream(file f, sstables::compression* cm, uint64_t offset, size_t len, - class file_input_stream_options options, reader_permit permit) + class file_input_stream_options options, reader_permit permit, + std::optional digest) { - return make_compressed_file_input_stream(std::move(f), cm, offset, len, std::move(options), std::move(permit)); + return make_compressed_file_input_stream( + std::move(f), cm, offset, len, std::move(options), std::move(permit), digest); } input_stream sstables::make_compressed_file_m_format_input_stream(file f, sstables::compression *cm, uint64_t offset, size_t len, - class file_input_stream_options options, reader_permit permit) { - return make_compressed_file_input_stream(std::move(f), cm, offset, len, std::move(options), std::move(permit)); + class file_input_stream_options options, reader_permit permit, + std::optional digest) { + return make_compressed_file_input_stream( + std::move(f), cm, offset, len, std::move(options), std::move(permit), digest); } output_stream sstables::make_compressed_file_m_format_output_stream(output_stream out, diff --git a/sstables/compress.hh b/sstables/compress.hh index a9ef380534..419465df51 100644 --- a/sstables/compress.hh +++ b/sstables/compress.hh @@ -369,11 +369,13 @@ compressor_ptr get_sstable_compressor(const compression&); // sstable alive, and the compression metadata is only a part of it. input_stream make_compressed_file_k_l_format_input_stream(file f, sstables::compression* cm, uint64_t offset, size_t len, - class file_input_stream_options options, reader_permit permit); + class file_input_stream_options options, reader_permit permit, + std::optional digest); input_stream make_compressed_file_m_format_input_stream(file f, sstables::compression* cm, uint64_t offset, size_t len, - class file_input_stream_options options, reader_permit permit); + class file_input_stream_options options, reader_permit permit, + std::optional digest); output_stream make_compressed_file_m_format_output_stream(output_stream out, sstables::compression* cm, diff --git a/sstables/sstables.cc b/sstables/sstables.cc index 1ee8a42760..63d1ba15ca 100644 --- a/sstables/sstables.cc +++ b/sstables/sstables.cc @@ -2475,11 +2475,11 @@ input_stream sstable::data_stream(uint64_t pos, size_t len, // Disabling integrity checks is not supported by compressed // file input streams. `integrity` is ignored. if (_version >= sstable_version_types::mc) { - return make_compressed_file_m_format_input_stream(f, &_components->compression, - pos, len, std::move(options), permit); + return make_compressed_file_m_format_input_stream(f, &_components->compression, + pos, len, std::move(options), permit, std::nullopt); } else { return make_compressed_file_k_l_format_input_stream(f, &_components->compression, - pos, len, std::move(options), permit); + pos, len, std::move(options), permit, std::nullopt); } } if (_components->checksum && integrity == integrity_check::yes) { diff --git a/test/boost/sstable_test.cc b/test/boost/sstable_test.cc index 3ec79b06b8..ca2b450b53 100644 --- a/test/boost/sstable_test.cc +++ b/test/boost/sstable_test.cc @@ -675,7 +675,7 @@ SEASTAR_TEST_CASE(test_skipping_in_compressed_stream) { auto make_is = [&] { f = open_file_dma(file_path, open_flags::ro).get(); - return make_compressed_file_m_format_input_stream(f, &c, 0, uncompressed_size, opts, semaphore.make_permit()); + return make_compressed_file_m_format_input_stream(f, &c, 0, uncompressed_size, opts, semaphore.make_permit(), std::nullopt); }; auto expect = [] (input_stream& in, const temporary_buffer& buf) { From 7e738bcd2df1bfb6b041597911ab6783ee9eb01b Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Fri, 13 Sep 2024 16:16:14 +0300 Subject: [PATCH 03/10] sstables: Add digest in the SSTable components SSTables store their digest in a Digest file. Add this in the list of SSTable components. In a follow-up patch we will use this component to enable digest checking in the validation path. Signed-off-by: Nikos Dragazis --- sstables/shareable_components.hh | 1 + sstables/sstables.cc | 6 +++++- sstables/sstables.hh | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sstables/shareable_components.hh b/sstables/shareable_components.hh index f956db5d34..2916d0fe2d 100644 --- a/sstables/shareable_components.hh +++ b/sstables/shareable_components.hh @@ -25,6 +25,7 @@ struct shareable_components { sstables::statistics statistics; std::optional scylla_metadata; weak_ptr checksum; + std::optional digest; }; } // namespace sstables diff --git a/sstables/sstables.cc b/sstables/sstables.cc index 63d1ba15ca..9eccba909c 100644 --- a/sstables/sstables.cc +++ b/sstables/sstables.cc @@ -2602,6 +2602,9 @@ static future do_validate_uncompressed(input_stream& stream, const c } future sstable::read_digest() { + if (_components->digest) { + co_return *_components->digest; + } sstring digest_str; co_await do_read_simple(component_type::Digest, [&] (version_types v, file digest_file) -> future<> { @@ -2622,7 +2625,8 @@ future sstable::read_digest() { maybe_rethrow_exception(std::move(ex)); }); - co_return boost::lexical_cast(digest_str); + _components->digest = boost::lexical_cast(digest_str); + co_return *_components->digest; } future> sstable::read_checksum() { diff --git a/sstables/sstables.hh b/sstables/sstables.hh index 0b18acd2b6..9257021df3 100644 --- a/sstables/sstables.hh +++ b/sstables/sstables.hh @@ -934,6 +934,10 @@ public: return _components->checksum ? _components->checksum->shared_from_this() : nullptr; } + std::optional get_digest() const { + return _components->digest; + } + // Gets ratio of droppable tombstone. A tombstone is considered droppable here // for cells and tombstones expired before the time point "GC before", which // is the point before which expiring data can be purged. From 347f5ee1663ac634b42db58f1e0af059ac4b75f9 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Sat, 14 Sep 2024 17:08:50 +0300 Subject: [PATCH 04/10] sstables: Check if digest component exists Extend `read_digest()` to first check if the digest component exists before attempting to load it from disk. Make `validate_checksums()` throw an error if the component does not exist to preserve its current behavior. Signed-off-by: Nikos Dragazis --- sstables/sstables.cc | 18 ++++++++++++------ sstables/sstables.hh | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/sstables/sstables.cc b/sstables/sstables.cc index 9eccba909c..a8e6949b16 100644 --- a/sstables/sstables.cc +++ b/sstables/sstables.cc @@ -2601,10 +2601,13 @@ static future do_validate_uncompressed(input_stream& stream, const c co_return valid; } -future sstable::read_digest() { +future> sstable::read_digest() { if (_components->digest) { co_return *_components->digest; } + if (!has_component(component_type::Digest)) { + co_return std::nullopt; + } sstring digest_str; co_await do_read_simple(component_type::Digest, [&] (version_types v, file digest_file) -> future<> { @@ -2626,7 +2629,7 @@ future sstable::read_digest() { }); _components->digest = boost::lexical_cast(digest_str); - co_return *_components->digest; + co_return _components->digest; } future> sstable::read_checksum() { @@ -2672,6 +2675,9 @@ future> sstable::read_checksum() { future validate_checksums(shared_sstable sst, reader_permit permit) { const auto digest = co_await sst->read_digest(); + if (!digest) { + throw std::runtime_error(seastar::format("No digest available for SSTable: {}", sst->get_filename())); + } auto data_stream = sst->data_stream(0, sst->ondisk_data_size(), permit, nullptr, nullptr, sstable::raw_stream::yes); @@ -2682,9 +2688,9 @@ future validate_checksums(shared_sstable sst, reader_ try { if (sst->get_compression()) { if (sst->get_version() >= sstable_version_types::mc) { - valid = co_await do_validate_compressed(data_stream, sst->get_compression(), true, digest); + valid = co_await do_validate_compressed(data_stream, sst->get_compression(), true, *digest); } else { - valid = co_await do_validate_compressed(data_stream, sst->get_compression(), false, digest); + valid = co_await do_validate_compressed(data_stream, sst->get_compression(), false, *digest); } } else { auto checksum = co_await sst->read_checksum(); @@ -2692,9 +2698,9 @@ future validate_checksums(shared_sstable sst, reader_ sstlog.warn("No checksums available for SSTable: {}", sst->get_filename()); ret = validate_checksums_result::no_checksum; } else if (sst->get_version() >= sstable_version_types::mc) { - valid = co_await do_validate_uncompressed(data_stream, *checksum, digest); + valid = co_await do_validate_uncompressed(data_stream, *checksum, *digest); } else { - valid = co_await do_validate_uncompressed(data_stream, *checksum, digest); + valid = co_await do_validate_uncompressed(data_stream, *checksum, *digest); } } } catch (...) { diff --git a/sstables/sstables.hh b/sstables/sstables.hh index 9257021df3..bd779ed94f 100644 --- a/sstables/sstables.hh +++ b/sstables/sstables.hh @@ -1017,7 +1017,7 @@ public: gc_clock::time_point get_gc_before_for_drop_estimation(const gc_clock::time_point& compaction_time, const tombstone_gc_state& gc_state, const schema_ptr& s) const; gc_clock::time_point get_gc_before_for_fully_expire(const gc_clock::time_point& compaction_time, const tombstone_gc_state& gc_state, const schema_ptr& s) const; - future read_digest(); + future> read_digest(); future> read_checksum(); }; From 3a3783ee2328584fef9d669e945e91d24d53c528 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Fri, 13 Sep 2024 16:29:46 +0300 Subject: [PATCH 05/10] sstables: Verify digests on validation path Extend the validation path to perform digest checking on all SSTables. This is achieved by loading the digest component on demand and passing it to the underlying data sources only during validation. The data sources for compressed and uncompressed SSTables were modified in previous patches to support digest checking. Consider digest checking as part of the integrity checking mechanism (i.e., requires `integrity_check::yes`) to ensure it remains disabled for all reads happening outside of the validation path (i.e., `sstable::validate()`). This practically means that digest checking is enabled only for: * scrub in validate mode * sstable validate Signed-off-by: Nikos Dragazis --- sstables/sstables.cc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sstables/sstables.cc b/sstables/sstables.cc index a8e6949b16..0afe8e1e52 100644 --- a/sstables/sstables.cc +++ b/sstables/sstables.cc @@ -1938,6 +1938,7 @@ future sstable::validate(reader_permit permit, abort_source& abort, lw_shared_ptr checksum; try { checksum = co_await read_checksum(); + co_await read_digest(); } catch (const malformed_sstable_exception& e) { ex = handle_sstable_exception(e, errors); } @@ -2471,15 +2472,18 @@ input_stream sstable::data_stream(uint64_t pos, size_t len, f = tracing::make_traced_file(std::move(f), std::move(trace_state), format("{}:", get_filename())); } + std::optional digest; + if (integrity == integrity_check::yes) { + digest = get_digest(); + } + if (_components->compression && raw == raw_stream::no) { - // Disabling integrity checks is not supported by compressed - // file input streams. `integrity` is ignored. if (_version >= sstable_version_types::mc) { return make_compressed_file_m_format_input_stream(f, &_components->compression, - pos, len, std::move(options), permit, std::nullopt); + pos, len, std::move(options), permit, digest); } else { return make_compressed_file_k_l_format_input_stream(f, &_components->compression, - pos, len, std::move(options), permit, std::nullopt); + pos, len, std::move(options), permit, digest); } } if (_components->checksum && integrity == integrity_check::yes) { @@ -2487,10 +2491,10 @@ input_stream sstable::data_stream(uint64_t pos, size_t len, auto file_len = data_size(); if (_version >= sstable_version_types::mc) { return make_checksummed_file_m_format_input_stream(f, file_len, - *checksum, pos, len, std::move(options), std::nullopt); + *checksum, pos, len, std::move(options), digest); } else { return make_checksummed_file_k_l_format_input_stream(f, file_len, - *checksum, pos, len, std::move(options), std::nullopt); + *checksum, pos, len, std::move(options), digest); } } return make_file_input_stream(f, pos, len, std::move(options)); From 39a74fb692810454b3b2630c21a8fd16e569d705 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Fri, 4 Oct 2024 11:30:41 +0300 Subject: [PATCH 06/10] test: Merge scrub/validate tests for compressed and uncompressed cases Currently, every scrub/validate test is duplicated to cover both compressed and uncompressed SSTables. However, except for the compression type, the tests are identical. This leads to some code bloat. Introduce common functions parameterized by the compression type to reduce code duplication. Also, group together the compressed and uncompressed variants into one compression-agnostic test. Signed-off-by: Nikos Dragazis --- test/boost/sstable_compaction_test.cc | 222 +++++++++----------------- 1 file changed, 77 insertions(+), 145 deletions(-) diff --git a/test/boost/sstable_compaction_test.cc b/test/boost/sstable_compaction_test.cc index fbe450ebb0..01f3607ada 100644 --- a/test/boost/sstable_compaction_test.cc +++ b/test/boost/sstable_compaction_test.cc @@ -2275,8 +2275,8 @@ public: } }; -SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_content) { - scrub_test_framework test(compress_sstable::yes); +void scrub_validate_corrupted_content(compress_sstable compress) { + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2303,153 +2303,85 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_content) { }); } +void scrub_validate_corrupted_file(compress_sstable compress) { + scrub_test_framework test(compress); + + auto schema = test.schema(); + + auto muts = tests::generate_random_mutations( + test.random_schema(), + tests::uncompactible_timestamp_generator(test.seed()), + tests::no_expiry_expiry_generator(), + std::uniform_int_distribution(10, 10)).get(); + + test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { + BOOST_REQUIRE(sstables.size() == 1); + auto sst = sstables.front(); + + // Corrupt the data to cause an invalid checksum. + auto f = open_file_dma(sstables::test(sst).filename(component_type::Data).native(), open_flags::wo).get(); + const auto wbuf_align = f.memory_dma_alignment(); + const auto wbuf_len = f.disk_write_dma_alignment(); + auto wbuf = seastar::temporary_buffer::aligned(wbuf_align, wbuf_len); + std::fill(wbuf.get_write(), wbuf.get_write() + wbuf_len, 0xba); + f.dma_write(0, wbuf.get(), wbuf_len).get(); + f.close().get(); + + sstables::compaction_type_options::scrub opts = { + .operation_mode = sstables::compaction_type_options::scrub::mode::validate, + }; + auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + BOOST_REQUIRE_GT(stats->validation_errors, 0); + BOOST_REQUIRE(sst->is_quarantined()); + BOOST_REQUIRE(in_strategy_sstables(ts).empty()); + }); +} + +void scrub_validate_valid(compress_sstable compress) { + scrub_test_framework test(compress); + + auto schema = test.schema(); + + auto muts = tests::generate_random_mutations(test.random_schema()).get(); + + test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { + BOOST_REQUIRE(sstables.size() == 1); + auto sst = sstables.front(); + + sstables::compaction_type_options::scrub opts = { + .operation_mode = sstables::compaction_type_options::scrub::mode::validate, + }; + auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + BOOST_REQUIRE_EQUAL(stats->validation_errors, 0); + BOOST_REQUIRE(!sst->is_quarantined()); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).size(), 1); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).front(), sst); + }); +} + +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_content) { + for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { + testlog.info("Validating {}compressed SSTable with content-level corruption...", compress == compress_sstable::no ? "un" : ""); + scrub_validate_corrupted_content(compress); + } +} + SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file) { - scrub_test_framework test(compress_sstable::yes); - - auto schema = test.schema(); - - auto muts = tests::generate_random_mutations( - test.random_schema(), - tests::uncompactible_timestamp_generator(test.seed()), - tests::no_expiry_expiry_generator(), - std::uniform_int_distribution(10, 10)).get(); - - test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { - BOOST_REQUIRE(sstables.size() == 1); - auto sst = sstables.front(); - - // Corrupt the data to cause an invalid checksum. - auto f = open_file_dma(sstables::test(sst).filename(component_type::Data).native(), open_flags::wo).get(); - const auto wbuf_align = f.memory_dma_alignment(); - const auto wbuf_len = f.disk_write_dma_alignment(); - auto wbuf = seastar::temporary_buffer::aligned(wbuf_align, wbuf_len); - std::fill(wbuf.get_write(), wbuf.get_write() + wbuf_len, 0xba); - f.dma_write(0, wbuf.get(), wbuf_len).get(); - f.close().get(); - - sstables::compaction_type_options::scrub opts = { - .operation_mode = sstables::compaction_type_options::scrub::mode::validate, - }; - auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); - - BOOST_REQUIRE(stats.has_value()); - BOOST_REQUIRE_GT(stats->validation_errors, 0); - BOOST_REQUIRE(sst->is_quarantined()); - BOOST_REQUIRE(in_strategy_sstables(ts).empty()); - }); + for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { + testlog.info("Validating {}compressed SSTable with invalid checksums...", compress == compress_sstable::no ? "un" : ""); + scrub_validate_corrupted_file(compress); + } } SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_valid_sstable) { - scrub_test_framework test(compress_sstable::yes); - - auto schema = test.schema(); - - auto muts = tests::generate_random_mutations(test.random_schema()).get(); - - test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { - BOOST_REQUIRE(sstables.size() == 1); - auto sst = sstables.front(); - - sstables::compaction_type_options::scrub opts = { - .operation_mode = sstables::compaction_type_options::scrub::mode::validate, - }; - auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); - - BOOST_REQUIRE(stats.has_value()); - BOOST_REQUIRE_EQUAL(stats->validation_errors, 0); - BOOST_REQUIRE(!sst->is_quarantined()); - BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).size(), 1); - BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).front(), sst); - }); -} - -SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_content_uncompressed) { - scrub_test_framework test(compress_sstable::no); - - auto schema = test.schema(); - - auto muts = tests::generate_random_mutations( - test.random_schema(), - tests::uncompactible_timestamp_generator(test.seed()), - tests::no_expiry_expiry_generator(), - std::uniform_int_distribution(10, 10)).get(); - std::swap(*muts.begin(), *(muts.begin() + 1)); - - test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { - BOOST_REQUIRE(sstables.size() == 1); - auto sst = sstables.front(); - - sstables::compaction_type_options::scrub opts = { - .operation_mode = sstables::compaction_type_options::scrub::mode::validate, - }; - auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); - - BOOST_REQUIRE(stats.has_value()); - BOOST_REQUIRE_GT(stats->validation_errors, 0); - BOOST_REQUIRE(sst->is_quarantined()); - BOOST_REQUIRE(in_strategy_sstables(ts).empty()); - }); -} - -SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file_uncompressed) { - scrub_test_framework test(compress_sstable::no); - - auto schema = test.schema(); - - auto muts = tests::generate_random_mutations( - test.random_schema(), - tests::uncompactible_timestamp_generator(test.seed()), - tests::no_expiry_expiry_generator(), - std::uniform_int_distribution(10, 10)).get(); - - test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { - BOOST_REQUIRE(sstables.size() == 1); - auto sst = sstables.front(); - - // Corrupt the data to cause an invalid checksum. - auto f = open_file_dma(sstables::test(sst).filename(component_type::Data).native(), open_flags::wo).get(); - const auto wbuf_align = f.memory_dma_alignment(); - const auto wbuf_len = f.disk_write_dma_alignment(); - auto wbuf = seastar::temporary_buffer::aligned(wbuf_align, wbuf_len); - std::fill(wbuf.get_write(), wbuf.get_write() + wbuf_len, 0xba); - f.dma_write(0, wbuf.get(), wbuf_len).get(); - f.close().get(); - - sstables::compaction_type_options::scrub opts = { - .operation_mode = sstables::compaction_type_options::scrub::mode::validate, - }; - auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); - - BOOST_REQUIRE(stats.has_value()); - BOOST_REQUIRE_GT(stats->validation_errors, 0); - BOOST_REQUIRE(sst->is_quarantined()); - BOOST_REQUIRE(in_strategy_sstables(ts).empty()); - }); -} - -SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_valid_sstable_uncompressed) { - scrub_test_framework test(compress_sstable::no); - - auto schema = test.schema(); - - auto muts = tests::generate_random_mutations(test.random_schema()).get(); - - test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { - BOOST_REQUIRE(sstables.size() == 1); - auto sst = sstables.front(); - - sstables::compaction_type_options::scrub opts = { - .operation_mode = sstables::compaction_type_options::scrub::mode::validate, - }; - auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); - - BOOST_REQUIRE(stats.has_value()); - BOOST_REQUIRE_EQUAL(stats->validation_errors, 0); - BOOST_REQUIRE(!sst->is_quarantined()); - BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).size(), 1); - BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).front(), sst); - BOOST_REQUIRE(!sst->get_checksum()); - }); + for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { + testlog.info("Validating {}compressed SSTable...", compress == compress_sstable::no ? "un" : ""); + scrub_validate_valid(compress); + } } SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_multiple_instances_uncompressed) { From 07ed0a48aa38280bf171dfa665d256523a3ed144 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Mon, 9 Sep 2024 17:58:33 +0300 Subject: [PATCH 07/10] test: Add tests for invalid digests In a previous patch we extended the validation path of the SSTable layer to validate the digests along with the checksums. Add two tests for compressed and uncompressed SSTables to test the validation API against SSTables with valid checksums but corrupted digests. Add two more tests to ensure that the absence of digest does not affect checksum validation. Signed-off-by: Nikos Dragazis --- test/boost/sstable_compaction_test.cc | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/test/boost/sstable_compaction_test.cc b/test/boost/sstable_compaction_test.cc index 01f3607ada..c461075a83 100644 --- a/test/boost/sstable_compaction_test.cc +++ b/test/boost/sstable_compaction_test.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "sstables/generation_type.hh" @@ -2339,6 +2340,89 @@ void scrub_validate_corrupted_file(compress_sstable compress) { }); } +void scrub_validate_corrupted_digest(compress_sstable compress) { + scrub_test_framework test(compress); + + auto schema = test.schema(); + + auto muts = tests::generate_random_mutations( + test.random_schema(), + tests::uncompactible_timestamp_generator(test.seed()), + tests::no_expiry_expiry_generator(), + std::uniform_int_distribution(10, 10)).get(); + + test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { + BOOST_REQUIRE(sstables.size() == 1); + auto sst = sstables.front(); + + // This test is about corrupted data with valid per-chunk checksums. + // This kind of corruption should be detected by the digest check. + // Triggering this is not trivial, so we corrupt the Digest file instead. + auto f = open_file_dma(sstables::test(sst).filename(component_type::Digest).native(), open_flags::rw).get(); + auto stream = make_file_input_stream(f); + auto close_stream = deferred_close(stream); + auto digest_str = util::read_entire_stream_contiguous(stream).get(); + auto digest = boost::lexical_cast(digest_str); + auto new_digest = to_sstring(digest + 1); // a random invalid digest + f.dma_write(0, new_digest.c_str(), new_digest.size()).get(); + + sstables::compaction_type_options::scrub opts = { + .operation_mode = sstables::compaction_type_options::scrub::mode::validate, + }; + auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + BOOST_REQUIRE_GT(stats->validation_errors, 0); + BOOST_REQUIRE(sst->is_quarantined()); + BOOST_REQUIRE(in_strategy_sstables(ts).empty()); + }); +} + +void scrub_validate_no_digest(compress_sstable compress) { + scrub_test_framework test(compress); + + auto schema = test.schema(); + + auto muts = tests::generate_random_mutations(test.random_schema()).get(); + + test.run(schema, muts, [] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { + BOOST_REQUIRE(sstables.size() == 1); + auto sst = sstables.front(); + + // Checksum and digest checking should be orthogonal. + // Ensure that per-chunk checksums are properly checked when digest is missing. + sstables::test(sst).rewrite_toc_without_component(component_type::Digest); + + sstables::compaction_type_options::scrub opts = { + .operation_mode = sstables::compaction_type_options::scrub::mode::validate, + }; + auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + BOOST_REQUIRE_EQUAL(stats->validation_errors, 0); + BOOST_REQUIRE(!sst->is_quarantined()); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).size(), 1); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).front(), sst); + BOOST_REQUIRE(!sst->get_checksum()); + + // Corrupt the data to cause an invalid checksum. + auto f = open_file_dma(sstables::test(sst).filename(component_type::Data).native(), open_flags::wo).get(); + auto close_f = deferred_close(f); + const auto wbuf_align = f.memory_dma_alignment(); + const auto wbuf_len = f.disk_write_dma_alignment(); + auto wbuf = seastar::temporary_buffer::aligned(wbuf_align, wbuf_len); + std::fill(wbuf.get_write(), wbuf.get_write() + wbuf_len, 0xba); + f.dma_write(0, wbuf.get(), wbuf_len).get(); + + stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + BOOST_REQUIRE_GT(stats->validation_errors, 0); + BOOST_REQUIRE(sst->is_quarantined()); + BOOST_REQUIRE(in_strategy_sstables(ts).empty()); + }); +} + void scrub_validate_valid(compress_sstable compress) { scrub_test_framework test(compress); @@ -2377,6 +2461,20 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file) { } } +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file_digest) { + for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { + testlog.info("Validating {}compressed SSTable with invalid digest...", compress == compress_sstable::no ? "un" : ""); + scrub_validate_corrupted_digest(compress); + } +} + +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_no_digest) { + for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { + testlog.info("Validating {}compressed SSTable with no digest...", compress == compress_sstable::no ? "un" : ""); + scrub_validate_no_digest(compress); + } +} + SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_valid_sstable) { for (const auto& compress : {compress_sstable::no, compress_sstable::yes}) { testlog.info("Validating {}compressed SSTable...", compress == compress_sstable::no ? "un" : ""); From 5f2be2924e2b4c903c7c91fa048adaf126160fd9 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Thu, 3 Oct 2024 18:16:21 +0300 Subject: [PATCH 08/10] test: Make random schema optional in scrub_test_framework The scrub_test_framework, which is the foundation for all scrub-related tests, always generates a random schema upon initialization and makes it available to the user. This is useful for running tests with ephemeral SSTables, but is redundant when the creation of the SSTable predates the test (e.g., it lives in the source tree). Turn scrub_test_framework into a template with a boolean parameter to optionally switch off the random schema generation. Also, add an overload for run() to support passing a ready-to-use SSTable instead of mutation fragments. Signed-off-by: Nikos Dragazis --- test/boost/sstable_compaction_test.cc | 67 +++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/test/boost/sstable_compaction_test.cc b/test/boost/sstable_compaction_test.cc index c461075a83..c6f8ff1f9a 100644 --- a/test/boost/sstable_compaction_test.cc +++ b/test/boost/sstable_compaction_test.cc @@ -2194,6 +2194,8 @@ using compress_sstable = tests::random_schema_specification::compress_sstable; // A framework for scrub-related tests. // Lives in a seastar thread +enum class random_schema { no, yes }; +template class scrub_test_framework { public: using test_func = std::function)>; @@ -2276,8 +2278,53 @@ public: } }; +template <> +class scrub_test_framework { +public: + using test_func = std::function)>; + +private: + sharded _env; + +public: + scrub_test_framework() + { + _env.start().get(); + } + + ~scrub_test_framework() { + _env.stop().get(); + } + + test_env& env() { return _env.local(); } + + void run(schema_ptr schema, shared_sstable sst, test_func func) { + auto& env = this->env(); + + auto table = env.make_table_for_tests(schema); + auto close_cf = deferred_stop(table); + table->start(); + + table->add_sstable_and_update_cache(sst).get(); + + bool found_sstable = false; + foreach_table_state_with_thread(table, [&] (compaction::table_state& ts) { + auto sstables = in_strategy_sstables(ts); + if (sstables.empty()) { + return; + } + BOOST_REQUIRE(sstables.size() == 1); + BOOST_REQUIRE(sstables.front() == sst); + found_sstable = true; + + func(table, ts, sstables); + }).get(); + BOOST_REQUIRE(found_sstable); + } +}; + void scrub_validate_corrupted_content(compress_sstable compress) { - scrub_test_framework test(compress); + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2305,7 +2352,7 @@ void scrub_validate_corrupted_content(compress_sstable compress) { } void scrub_validate_corrupted_file(compress_sstable compress) { - scrub_test_framework test(compress); + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2341,7 +2388,7 @@ void scrub_validate_corrupted_file(compress_sstable compress) { } void scrub_validate_corrupted_digest(compress_sstable compress) { - scrub_test_framework test(compress); + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2379,7 +2426,7 @@ void scrub_validate_corrupted_digest(compress_sstable compress) { } void scrub_validate_no_digest(compress_sstable compress) { - scrub_test_framework test(compress); + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2424,7 +2471,7 @@ void scrub_validate_no_digest(compress_sstable compress) { } void scrub_validate_valid(compress_sstable compress) { - scrub_test_framework test(compress); + scrub_test_framework test(compress); auto schema = test.schema(); @@ -2487,7 +2534,7 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_multiple_instances_unc fmt::print("Skipping test as it depends on error injection. Please run in mode where it's enabled (debug,dev).\n"); return; #endif - scrub_test_framework test(compress_sstable::no); + scrub_test_framework test(compress_sstable::no); auto schema = test.schema(); @@ -2676,7 +2723,7 @@ SEASTAR_TEST_CASE(sstable_validate_test) { } SEASTAR_THREAD_TEST_CASE(sstable_scrub_abort_mode_test) { - scrub_test_framework test(compress_sstable::yes); + scrub_test_framework test(compress_sstable::yes); auto schema = test.schema(); @@ -2700,7 +2747,7 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_abort_mode_test) { } SEASTAR_THREAD_TEST_CASE(sstable_scrub_skip_mode_test) { - scrub_test_framework test(compress_sstable::yes); + scrub_test_framework test(compress_sstable::yes); auto schema = test.schema(); @@ -2748,7 +2795,7 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_skip_mode_test) { } SEASTAR_THREAD_TEST_CASE(sstable_scrub_segregate_mode_test) { - scrub_test_framework test(compress_sstable::yes); + scrub_test_framework test(compress_sstable::yes); auto schema = test.schema(); @@ -2788,7 +2835,7 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_segregate_mode_test) { } SEASTAR_THREAD_TEST_CASE(sstable_scrub_quarantine_mode_test) { - scrub_test_framework test(compress_sstable::yes); + scrub_test_framework test(compress_sstable::yes); auto schema = test.schema(); From 7090e2597fbd86ddc04ae987f3ea8a7a3e7377e8 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Wed, 2 Oct 2024 19:16:12 +0300 Subject: [PATCH 09/10] compaction: Make quarantine optional for perform_sstable_scrub() Allow `perform_sstable_scrub()` to disable quarantine for invalid SSTables detected by scrub in validate mode. This is already supported by the lower-level function `scrub_sstables_validate_mode()` via the flag `quarantine_sstables` and is being used by sstable-scrub. Propagate the flag up to `perform_sstable_scrub()`. This will allow to test scrub/validate against read-only SSTables from the source tree. Signed-off-by: Nikos Dragazis --- compaction/compaction_manager.cc | 14 +++++++++----- compaction/compaction_manager.hh | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/compaction/compaction_manager.cc b/compaction/compaction_manager.cc index 53fdeeef1c..f47628f76f 100644 --- a/compaction/compaction_manager.cc +++ b/compaction/compaction_manager.cc @@ -1700,9 +1700,13 @@ compaction_manager::rewrite_sstables(table_state& t, sstables::compaction_type_o namespace compaction { class validate_sstables_compaction_task_executor : public sstables_task_executor { + compaction_manager::quarantine_invalid_sstables _quarantine_sstables; public: - validate_sstables_compaction_task_executor(compaction_manager& mgr, throw_if_stopping do_throw_if_stopping, table_state* t, tasks::task_id parent_id, std::vector sstables) + validate_sstables_compaction_task_executor(compaction_manager& mgr, throw_if_stopping do_throw_if_stopping, + table_state* t, tasks::task_id parent_id, std::vector sstables, + compaction_manager::quarantine_invalid_sstables quarantine_sstables) : sstables_task_executor(mgr, do_throw_if_stopping, t, sstables::compaction_type::Scrub, "Scrub compaction in validate mode", std::move(sstables), parent_id) + , _quarantine_sstables(quarantine_sstables) {} protected: @@ -1731,7 +1735,7 @@ private: sst->get_sstable_level(), sstables::compaction_descriptor::default_max_sstable_bytes, sst->run_identifier(), - sstables::compaction_type_options::make_scrub(sstables::compaction_type_options::scrub::mode::validate)); + sstables::compaction_type_options::make_scrub(sstables::compaction_type_options::scrub::mode::validate, _quarantine_sstables)); co_return co_await sstables::compact_sstables(std::move(desc), _compaction_data, *_compacting_table, _progress_monitor); } catch (sstables::compaction_stopped_exception&) { // ignore, will be handled by can_proceed() @@ -1761,14 +1765,14 @@ static std::vector get_all_sstables(table_state& t) { return s; } -future compaction_manager::perform_sstable_scrub_validate_mode(table_state& t, tasks::task_info info) { +future compaction_manager::perform_sstable_scrub_validate_mode(table_state& t, tasks::task_info info, quarantine_invalid_sstables quarantine_sstables) { auto gh = start_compaction(t); if (!gh) { co_return compaction_stats_opt{}; } // All sstables must be included, even the ones being compacted, such that everything in table is validated. auto all_sstables = get_all_sstables(t); - co_return co_await perform_compaction(throw_if_stopping::no, info, &t, info.id, std::move(all_sstables)); + co_return co_await perform_compaction(throw_if_stopping::no, info, &t, info.id, std::move(all_sstables), quarantine_sstables); } namespace compaction { @@ -2098,7 +2102,7 @@ compaction_manager::maybe_split_sstable(sstables::shared_sstable sst, table_stat future compaction_manager::perform_sstable_scrub(table_state& t, sstables::compaction_type_options::scrub opts, tasks::task_info info) { auto scrub_mode = opts.operation_mode; if (scrub_mode == sstables::compaction_type_options::scrub::mode::validate) { - return perform_sstable_scrub_validate_mode(t, info); + return perform_sstable_scrub_validate_mode(t, info, opts.quarantine_sstables); } owned_ranges_ptr owned_ranges_ptr = {}; sstring option_desc = fmt::format("mode: {};\nquarantine_mode: {}\n", opts.operation_mode, opts.quarantine_operation_mode); diff --git a/compaction/compaction_manager.hh b/compaction/compaction_manager.hh index 7037c041e1..c146832d7d 100644 --- a/compaction/compaction_manager.hh +++ b/compaction/compaction_manager.hh @@ -228,7 +228,8 @@ private: // similar-sized compaction. void postpone_compaction_for_table(compaction::table_state* t); - future perform_sstable_scrub_validate_mode(compaction::table_state& t, tasks::task_info info); + using quarantine_invalid_sstables = sstables::compaction_type_options::scrub::quarantine_invalid_sstables; + future perform_sstable_scrub_validate_mode(compaction::table_state& t, tasks::task_info info, quarantine_invalid_sstables quarantine_sstables); future<> update_static_shares(float shares); using get_candidates_func = std::function>()>; From 7a1ec3aa41eb5f3cb8daf23f6b61671e35c29063 Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Thu, 3 Oct 2024 12:50:26 +0300 Subject: [PATCH 10/10] test: Test scrub/validate with SSTables from Cassandra All current unit tests for scrub in validate mode generate random SSTables on the fly. Add some more tests with frozen Cassandra SSTables from the source tree to verify compatibility with Cassandra. Use some of the existing 3.x Cassandra SSTables to test the valid case, and use the same schema to generate some corrupted SSTables for the invalid case. Overall, the new tests cover the following scenarios: * valid compressed/uncompressed * compressed/uncompressed with invalid checksums * compressed/uncompressed with invalid digest For the compressed SSTable with invalid checksums, a small chunk length was used (4KiB) to have more chunks with less disk space. For uncompressed SSTables the chunk length is not configurable. Finally, since the SSTables live in the source tree, the quarantine mechanism was disabled. Signed-off-by: Nikos Dragazis --- test/boost/sstable_compaction_test.cc | 126 ++++++++++++++++++ .../me-1-big-CompressionInfo.db | Bin 0 -> 83 bytes .../invalid_checksums/me-1-big-Data.db | Bin 0 -> 20752 bytes .../invalid_checksums/me-1-big-Digest.crc32 | 1 + .../invalid_checksums/me-1-big-Filter.db | Bin 0 -> 64 bytes .../invalid_checksums/me-1-big-Index.db | Bin 0 -> 370 bytes .../invalid_checksums/me-1-big-Statistics.db | Bin 0 -> 5242 bytes .../invalid_checksums/me-1-big-Summary.db | Bin 0 -> 56 bytes .../invalid_checksums/me-1-big-TOC.txt | 8 ++ .../me-1-big-CompressionInfo.db | Bin 0 -> 43 bytes .../invalid_digest/me-1-big-Data.db | Bin 0 -> 299 bytes .../invalid_digest/me-1-big-Digest.crc32 | 1 + .../invalid_digest/me-1-big-Filter.db | Bin 0 -> 24 bytes .../invalid_digest/me-1-big-Index.db | Bin 0 -> 43 bytes .../invalid_digest/me-1-big-Statistics.db | Bin 0 -> 5134 bytes .../invalid_digest/me-1-big-Summary.db | Bin 0 -> 56 bytes .../invalid_digest/me-1-big-TOC.txt | 8 ++ .../invalid_checksums/me-1-big-CRC.db | Bin 0 -> 12 bytes .../invalid_checksums/me-1-big-Data.db | Bin 0 -> 69881 bytes .../invalid_checksums/me-1-big-Digest.crc32 | 1 + .../invalid_checksums/me-1-big-Filter.db | Bin 0 -> 168 bytes .../invalid_checksums/me-1-big-Index.db | Bin 0 -> 1172 bytes .../invalid_checksums/me-1-big-Statistics.db | Bin 0 -> 5475 bytes .../invalid_checksums/me-1-big-Summary.db | Bin 0 -> 56 bytes .../invalid_checksums/me-1-big-TOC.txt | 8 ++ .../invalid_digest/me-1-big-CRC.db | Bin 0 -> 8 bytes .../invalid_digest/me-1-big-Data.db | Bin 0 -> 604 bytes .../invalid_digest/me-1-big-Digest.crc32 | 1 + .../invalid_digest/me-1-big-Filter.db | Bin 0 -> 24 bytes .../invalid_digest/me-1-big-Index.db | Bin 0 -> 43 bytes .../invalid_digest/me-1-big-Statistics.db | Bin 0 -> 5134 bytes .../invalid_digest/me-1-big-Summary.db | Bin 0 -> 56 bytes .../invalid_digest/me-1-big-TOC.txt | 8 ++ 33 files changed, 162 insertions(+) create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-CompressionInfo.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Data.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Digest.crc32 create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Filter.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Index.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Statistics.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Summary.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-TOC.txt create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-CompressionInfo.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Data.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Digest.crc32 create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Filter.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Index.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Statistics.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Summary.db create mode 100644 test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-TOC.txt create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-CRC.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Data.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Digest.crc32 create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Filter.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Index.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Statistics.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Summary.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-TOC.txt create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-CRC.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Data.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Digest.crc32 create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Filter.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Index.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Statistics.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Summary.db create mode 100644 test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-TOC.txt diff --git a/test/boost/sstable_compaction_test.cc b/test/boost/sstable_compaction_test.cc index c6f8ff1f9a..ff16eafae5 100644 --- a/test/boost/sstable_compaction_test.cc +++ b/test/boost/sstable_compaction_test.cc @@ -2584,6 +2584,132 @@ SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_multiple_instances_unc }); } +// Following tests run scrub in validate mode with SSTables produced by Cassandra. +// The purpose is to verify compatibility. +// +// The SSTables live in the source tree under: +// test/resource/sstables/3.x/{uncompressed,lz4}/partition_key_with_values_of_different_types and +// test/resource/sstables/3.x/{uncompressed,lz4}/integrity_check +// +// The former are pre-existing SSTables that we use to test the valid case. +// +// The latter were tailor-made to cover the invalid case by triggering the checksum and digest checks. +// The SSTables were produced with the following schema: +// +// CREATE KEYSPACE test_ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; +// +// CREATE TABLE test_ks.test_table ( pk INT, +// bool_val BOOLEAN, +// double_val DOUBLE, +// float_val FLOAT, +// int_val INT, +// long_val BIGINT, +// timestamp_val TIMESTAMP, +// timeuuid_val TIMEUUID, +// uuid_val UUID, +// text_val TEXT, +// PRIMARY KEY(pk)) +// WITH compression = {}; +// +// where is one of the following: +// {'enabled': false} for the uncompressed case, +// {'chunk_length_in_kb': '4', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} for the compressed case. + +static schema_builder make_cassandra_schema_builder() { + return schema_builder("test_ks", "test_table") + .with_column("pk", int32_type, column_kind::partition_key) + .with_column("bool_val", boolean_type) + .with_column("double_val", double_type) + .with_column("float_val", float_type) + .with_column("int_val", int32_type) + .with_column("long_val", long_type) + .with_column("timestamp_val", timestamp_type) + .with_column("timeuuid_val", timeuuid_type) + .with_column("uuid_val", uuid_type) + .with_column("text_val", utf8_type); +} + +void scrub_validate_cassandra_compat(const compression_parameters& cp, sstring sstable_dir, + generation_type::int_t gen, sstable::version_types version, bool valid) { + scrub_test_framework test; + + auto schema = make_cassandra_schema_builder() + .set_compressor_params(cp) + .build(); + auto sst = test.env().reusable_sst(schema, sstable_dir, gen, version).get(); + + test.run(schema, sst, [valid] (table_for_tests& table, compaction::table_state& ts, std::vector sstables) { + BOOST_REQUIRE(sstables.size() == 1); + auto sst = sstables.front(); + + using scrub = sstables::compaction_type_options::scrub; + sstables::compaction_type_options::scrub opts = { + .operation_mode = scrub::mode::validate, + .quarantine_sstables = scrub::quarantine_invalid_sstables::no, + }; + auto stats = table->get_compaction_manager().perform_sstable_scrub(ts, opts, tasks::task_info{}).get(); + + BOOST_REQUIRE(stats.has_value()); + if (valid) { + BOOST_REQUIRE_EQUAL(stats->validation_errors, 0); + } else { + BOOST_REQUIRE_GT(stats->validation_errors, 0); + } + BOOST_REQUIRE(!sst->is_quarantined()); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).size(), 1); + BOOST_REQUIRE_EQUAL(in_strategy_sstables(ts).front(), sst); + BOOST_REQUIRE(!sst->get_checksum()); + }); +} + +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_valid_sstable_cassandra_compat) { + for (const auto& [cp, subdir] : { + std::pair{compression_parameters::no_compression(), "uncompressed"}, + {compression_parameters(compressor::lz4), "lz4"} + }) { + testlog.info("Validating {}compressed SSTable from Cassandra...", cp.get_compressor() ? "" : "un"); + scrub_validate_cassandra_compat( + cp, + seastar::format("test/resource/sstables/3.x/{}/partition_key_with_values_of_different_types", subdir), + 1, + sstable::version_types::mc, + true + ); + } +} + +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file_cassandra_compat) { + for (const auto& [cp, subdir] : { + std::pair{compression_parameters::no_compression(), "uncompressed"}, + {compression_parameters(compressor::lz4), "lz4"} + }) { + testlog.info("Validating {}compressed SSTable from Cassandra with invalid checksums...", cp.get_compressor() ? "" : "un"); + scrub_validate_cassandra_compat( + cp, + seastar::format("test/resource/sstables/3.x/{}/integrity_check/invalid_checksums", subdir), + 1, + sstable::version_types::me, + false + ); + } +} + +SEASTAR_THREAD_TEST_CASE(sstable_scrub_validate_mode_test_corrupted_file_digest_cassandra_compat) { + for (const auto& [cp, subdir] : { + std::pair{compression_parameters::no_compression(), "uncompressed"}, + {compression_parameters(compressor::lz4), "lz4"} + }) { + testlog.info("Validating {}compressed SSTable from Cassandra with invalid digest...", cp.get_compressor() ? "" : "un"); + scrub_validate_cassandra_compat( + cp, + seastar::format("test/resource/sstables/3.x/{}/integrity_check/invalid_digest", subdir), + 1, + sstable::version_types::me, + false + ); + } +} + SEASTAR_TEST_CASE(sstable_validate_test) { return test_env::do_with_async([] (test_env& env) { auto schema = schema_builder("ks", get_name()) diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-CompressionInfo.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-CompressionInfo.db new file mode 100644 index 0000000000000000000000000000000000000000..0141e45dac78ee582134051bc227c63802518534 GIT binary patch literal 83 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 literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Digest.crc32 b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Digest.crc32 new file mode 100644 index 0000000000..27b89a39c9 --- /dev/null +++ b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Digest.crc32 @@ -0,0 +1 @@ +4106266112 \ No newline at end of file diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Filter.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Filter.db new file mode 100644 index 0000000000000000000000000000000000000000..b41c72b10e35681dc8ac67193827f77487188ca6 GIT binary patch literal 64 zcmV-G0KfkL000F5000NbuBfuc#RBL6N|MGn3Me9!e*^$P4_YKD7z0B{ Wfv7m<#55%9SB67?qv&D{4kR=>I1{-5 literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Index.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Index.db new file mode 100644 index 0000000000000000000000000000000000000000..ad98aeebb238241895439afd67406d269b163719 GIT binary patch literal 370 zcmZQzVPIeo2VxMd*fv6NJIte-pxB zoD>6L2v5$2FgT{DKp0F@UqKiw(^(-5p;=H}{Il0XM8xJqLm1KvCO{Y>i?ksO_QiJ~ z43*`#Aq$scf^4Pi+f@HB$36b^VogFy0tR~bZ1>45Jl2#e={Uj>B4 Jd?0Wk0|1s}H4y*+ literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Statistics.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Statistics.db new file mode 100644 index 0000000000000000000000000000000000000000..ff4c73421b65faa1ddffca3c02675b132153acf0 GIT binary patch literal 5242 zcmeI0eN0nV6u|plKPVMCsVIVjYyuO}RVHIXrZBMzU04JJ1*gFWt+b4`q!fq9A~I(j zVTev;GQ`a-M2pdEB33Z4$R^V{RARy`TNB73c9nv$OCX^0H28-rgKNrz=VvsDVY9sbG zZX@i??>O!XE1MN`EERNT8nZyy_{=OEHaY>QamnH)#oPruN_gzTV=0eiJa*%;oW~wK z_T=$&9{cjxpU1&Gp3mcjJdWk@N*-tOxR7No&wF@$oMkSr9W1kXCAj@yN8&YB&as83 z-^4PzZ=@I2CyTLM$bCP%Z^9~8&ax<(Wp)sUVwNnz?82>=ot8eva#<^8w|>meeSlf< z8RluLG5fS*eqkQwmkwZ7reI#MA9M6+%<)q(uk*&7n}E55!d$~UzpW}P@ApGY_}#^v zl7x9bKA%MYPe@L#;Lh3TG?OU9mt2UHRLN<8->Jw2hrHD7rJcRg%mJrC~tdr>XmE0@80_8%V zd~`m9r`EbcxhUT?60&$r2U@Shq|b+P7n^+$va4$81IUy78qodZo}#z}<&%Z#vyi7O z2t?=EOaAZ^D4$Wgvk&r%uf~22dDhRX7DAqPNp}=-#JcyYA*(t=RzXfYU}M38&>DaaX1mcaX;u*RQ+_dhYbTMqAk zQft4~2<*x3BasDwg%-lT3$Q5QqCyRMXs8hGkND5>J>2tyQFEC+rDrkFmsWgLPzPAH z{PVMBz;0<}x8eHbzj*Y)^?O{7J+1)yQ>l_1_&o4F6xE;va=&vu3bXfVWBtA%7HwpaMQsr zj=^~CEy3*z#^i+J{R>Z`0jskH9v6?XfQt?P2LH1LBY_Qb`CrvsL98Dc&mYV&BZ4_* z`2SjSF|jjk&*Nb=hMRlJNa$Dr{QzYh2y-6LbM%89@|bg?^21jWWUt4~DC&&)a+79zSKmf0dN3G*IST9hI$A8!e&plQ!nif23Tc=_XTl+6F57DKDVLtfFZ}EoEfv zWExXmdbXA^3m(szYDAT>a$TQoqAZNjxDhU6G#MKgz46&BoNJP1n~a%^lc4b;i=!18 zA5x1pf5geS7RgD=!Wj#9i{7BkwNQo}#?i~;IZEQR8Cy5THZM=FVNB;fi)nJQS~U_U KXElDDti|6^_kAe< literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Summary.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_checksums/me-1-big-Summary.db new file mode 100644 index 0000000000000000000000000000000000000000..6b5f18c34cbff4286f9bcbe9afa62890946ee500 GIT binary patch literal 56 mcmZQzU}#`qU|1LOBcowW$S! literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Data.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Data.db new file mode 100644 index 0000000000000000000000000000000000000000..db12b35f6487e296bc98d2515f87107e24102153 GIT binary patch literal 299 zcma!HVqiGLz{0@5z*_(R|NjOCMurV4DME{Haxik(Kb*0cl?;+VY>+I(P{D9ukH*(S{V|#xwr9?qEoKm41luCdUBSR2 zXjs8u&v&7=f^Wvanr2UY*yYHm( zYHN@^91JOhc(ZVjLK=mIHt7s3LZ?s>2OxeZ6E7lXIc3r|EJ_IBT1`2IvX-)ray;dE zlowD=p`1>61?5$g^C&+|xjbTVUUpLPCd%!U2e4V{ez5mZ@c`v+Y;G>)J=hZJ|4a7a zxYUL%Gh)lLu@%&KmE$hJDvTY(oil=6>wlQ`iNw zv9}~(`!`_M@z^cY`t3F2csL1}P5K$Tq!@cYUY~6EOBCmd@bk=liY+PWL-nOny#A%< z&!D*M<9cNI)mCI>(Im2J!ihX0|hQ z8ynt7^|Ob*MSgHK?q6wQ2*tPls7Kztz72Wr^_9pSAL9AB)Pd%cRmYCP`mD+FJo2vO zBgmhfUJlJIJb}JnxtFzHz_=u_2CWZC=Oz`5OKVg)(6Yw{(0JuuR}G9SIzrc=Rpzz7 zK+j6*LeGzv$?KjOZbp1!I7U-izdy}C*Zw|fyeeK=;wZQBmjlC1P zY|R?@{IjhEC*bqXPQIjt&p+o3T|No&xuIh@wZM`98)^iWrk>SXp~uE{!uuopwV?^V zKNO4OhF637%I3Ff-vL&yf33#{9J9IpCpdoXMcoxRe%<-Jc0H&+o3E>a?}LORrY-}B zCw(%khxaRW#A2-j@g=r!8r%Czz4p%bgBio`|#^+u)m?x>HV40aaRBIncs7Pty@RO_e`4+`-Z=R|Go#~Kn}V5 zxAt7JNIPn(JvhfC1m~Fe|FY*IeI*61IpQYr2b7sT7nlr&nb~up>0oE}TxdRK_FOZ2 zF0df}+j}myv9R>15r&E77FeC2V78L%rySyEH#;wl z1cVo;*nska@R{&phZe76Iri1(?%ANeKY#x2LCYIE9M9(bA<2ugbPAjQ#sB>Cj@?kn z`}`HW+hFwsj4O(_S2-C*rL=p!?#(eqzz@jSu@p0)C^nkx|JkD v6L3{J{QB literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Summary.db b/test/resource/sstables/3.x/lz4/integrity_check/invalid_digest/me-1-big-Summary.db new file mode 100644 index 0000000000000000000000000000000000000000..ab6bd5d065001dab321da058d45d4d71968b5047 GIT binary patch literal 56 mcmZQzU}#`qU|F;5wDq22*O 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 I@y}lT|4x9d3IG5A literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Digest.crc32 b/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Digest.crc32 new file mode 100644 index 0000000000..e628759613 --- /dev/null +++ b/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Digest.crc32 @@ -0,0 +1 @@ +3195195734 \ No newline at end of file diff --git a/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Filter.db b/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_checksums/me-1-big-Filter.db new file mode 100644 index 0000000000000000000000000000000000000000..851d15e20dcd6103aae1fcd004d68bbe16df79f2 GIT binary patch literal 168 zcmV;Z09XG2000F5000!k)5KStEhsJz3QbGaHz=Ik=CIa#n z2uLCkB20vf2@+m_I%*I=G$8}RF2o`@MBQSyh-lI@SDZvtt zfK(j0O9%)-1_i@_0E?@hD3L3t1QA;!DrM6mL(zRyJvTfG1TAO7WVM2=pySlt! z3zdrHhY1oT4#Z~_6hR-llT<>{k|-<62tQ=Sb3VMhKc4gcIq&(O^S%<9L?W5-@A`kY z){YTtY&FEeQ+J7-br#}CeKApW&PpU+2qoqFOm?x(A4iTZu$Y&ov_0Evh#3iC4|-7gd3f+xgIJ zWK>aRWQoNppe$f!fpVDALuI2TA!m^CL0&W!3Uws49-4kC7FwaD(7j?d0VT=QYUqn7 zDGUMBau{PNC5&E5hEgXL0+W$ih4Mlw4(1T50G2SS7}j=bGb+lcWvEn9hf$qC?Z)v4 zDho9qs36q7qo%_q8hZ$*>zM69eJYiJGrrUooPA4G;CwZ;4j03z?XYVpEiNgjY&4pv zax^`lBGGz`I*QBUxnww!m`#P_J2f6{VrSH7?`3AdRnd)waKO~O;r$p}1e<>)lLY@!ZeKE2}U}Yh2xijO2_XuYCcBYlm>sq jx3v*t;yedBtBO6>D1!5G4*IGz5zu9g)(G zWhhwuMijJ_p-81x#E1;EV)3JhNFAV789JuuC>2ydK?>c2+pzDdgMao9cILA0x9`2| z+qZjnFX8ccG;gp?MjeH&Rp{a)u?F=59?w3A=b<;OSE%FEkq&66}My+0_G5Doy{?q@2!xr=fANTk7_t}i263cs)EhlwNWpw>^+q{Cn zw7Zg$yB~e=l@A&@Vd6Jkcl_ZXU7I2@f9KhkBhJo|wio#%5*G%_ZV|;P}f!a!THc<^JU-mZbu}T`$GG2q`Q+m>RMfD zE>G`BS+KuC-qHISQgsvvvNFUsid7F=tOkS5y(6eUG_`pz6E2DyeYVz%ALhQD^qbrH zgTXtR0`Apn(K47^WR6BLB%gLMUA525H<-`-Xvg-Ui)qYGl8wt~he1O-4H`Zbcup74W;gEd~L0(h_IcN;zwc{Yi2SZL(L(b!z--}A9Z*v7qxjup%9t!yo zJfBosJ<#)4!26kh1+pNVd3p~unuh0}zFs|~M3V4QI$%+I0bucpe!!AG4d79I_JCzk z_&g2{+bQn>y~EkDfSsD00lPGW0G@OW?zj6hxZmD8D}epH=Dz^HyAZ~=%GnI`8~&C9 zPFh|B_(1n;z@`f@KetUFpOkcTCuvXl`KJNiHZdRYua#aT^QWEx&sY9l*$q-Ja83m0 zLvZ#(38|+OCG$xZe$W8cE7EHdNxhZH+(oiPxuk>SQLd%nezFOd*OL0t0#y~s_P$fV zd3Ka_?-)$?3lo_>+8m#es<=U)`6IL2}pWx+Mft(`+Knk{r2sF?s(}1wm)X`=9FHEhF!L zzQ){ij_~I<7tY^Iu)s)}GYO_AUz4jye)eoDxj(|EJNMxC2V-V6-M(cZVK3fya&s}k z(&b003CXI+RgZzo}&5@EX7Oz`1bx6ASf zEtL|Myuxdl+^W-7s;n?6a_`fw6pYUNsTr1#V8VhVM(#KScbog+Ig$qa9;1J>CjESH+cA+V$m}&&RoK zUQTbT^y?oBcwGT+RsrYrJNyoh+wL_6YrZodOW4XM-xCgp$JI%!^u3D%;;U^3WnE`a z$VXa&?4e*Nj*mC;2PvdeSZI^Mz`_p-72yJe52ZpSa+XmheZwM$;Ok1tD#{wlTFMEO zXHuR`IhAq-{<@HnOeWSCLAA5L1t5a#V#qv-jCNOJNz|@i;D2`EczTAccbtg~H~p+b-m$h7dGF1o$nBrt`MK1N=95)Uy9V2{M)NDk z+vglc{^HbPXwlr`==)XlhUP067bjPv^&#$fMgilJYJ~w>`gA|KUYW;H4dZd`q3h5J z)9T-#r=)bE=Sf|nI}hX2#FifD_?&dKo)a}U--Pj5M_#)Eecyw5A3)FjX8m&LMdvGz zK`-0*d=s>3VDWnB!UL{s=<@WBAA#QdyLt$^?sSV8`bg2vn>V3;q%XiU)R9rUt|wmJ^}JYgGUWDz+yie+6^p8JEyZikB)AG_eXl8 zz5%{J;^xN>tpx4mjqlfd2&`QDc8?cWwW018c>S7-+AHw-wHNZ*bfEoouC@xk50VZW zJM|!*^4X9M-mkQ)W=k!IFR%`$!~H;Z)=xbe5I6S!(D@v2{=U|ua6C^9FUyAO*J3rq&X#P(chI@s8r3(ZGt z&lTHqfd%p3-gB|L3rkg2Py;q8UJF37V269`_{`3tW!RmKWoQ;`Q5FyU4}o!mPBWFCd17C=bv}V^e)cp ztK^(|i`$>Iq8MR4lY6xhj$y2smscb-~@FTCEn-#5jel=;MrJ`~_DCF{}Up literal 0 HcmV?d00001 diff --git a/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Summary.db b/test/resource/sstables/3.x/uncompressed/integrity_check/invalid_digest/me-1-big-Summary.db new file mode 100644 index 0000000000000000000000000000000000000000..ab6bd5d065001dab321da058d45d4d71968b5047 GIT binary patch literal 56 mcmZQzU}#`qU|