mirror of
https://github.com/scylladb/scylladb.git
synced 2026-05-22 15:52:13 +00:00
Drop local formatter for seastar::http::reply, which should have been added to Seastar in the first place, and now conflicts. Also drop local formatters for types that are aliases for Seastar types which have gained formatters. Disable recently-gained TLS use of OpenSSL instead of gnutls. We don't need it, and it causes link errors with LTO. Fix incorrect skipping in encrypted_file_test, which computed the remaining stream length but did not account for already consumed size_to_compare. Change utils::gcp::storage::client::object_data_source::skip() to match new Seastar behavior (rejecting skip-past-eof with an exception). This is needed since30f1075544switched the test's data source to a Seastar implementation. It is also more correct - if we're asked to skip n bytes but the stream doesn't have n bytes, this is a protocol violation. Contains test fix from Pavel, exposed by [1]: test: Handle premature EOF in test_gcp_storage_skip_read The test intentionally uses file_size larger than the actual object to exercise EOF behavior. When input_stream::skip() is called after EOF, it throws std::runtime_error("premature end of stream"). Catch this specific exception from both streams, verify they agree, and exit the loop gracefully. Signed-off-by: Pavel Emelyanov <xemul@scylladb.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> [1]cbd1e17d2f, included in this Seastar submodule update * seastar 4d268e0e...485a62b2 (50): > reactor: open_directory(): honor bypass_fsync > http: Add formatters for http::request and http::reply > Merge 'Assorted set of io-tester cleanups' from Pavel Emelyanov io_tester: Remove unused and internal-only accessor io_tester: Move think-time machinery into thinker_state io_tester: Move _file to io_class_data io_tester: Replace class_data::_start member with a local variable io_tester: Move _alignment from class_data to io_class_data io_tester: Remove buffer allocation from top-level request issuing io_tester: Cleanup context::stop() invocation io_tester: Allocate write buffer once to fill a file io_tester: Declare quantiles arrays as static constexpr io_tester: Drop class_data::type_str() io_tester: Replace != "" comparisons with .empty() io_tester: Replace gen_class_data() if/else chain with a switch io_tester: Deduplicate vectorized I/O classes > io_tester: fix crash from missing metric during startup > net: tls: adjust openssl integration to new module support > http/client: Count and export integrated queue length > Merge 'Introduce pipe_data_source_impl and pipe_data_sink_impl' from Pavel Emelyanov fstream: add pipe_data_source_impl and pipe_data_sink_impl pollable_fd: add write_some/write_all backed by writev pollable_fd: rename write_some/write_all(iovec) to send_some/send_all > reactor: Make pollable_fd_state helper methods private > module: extend seastar.cppm with comprehensive public API exports > Merge 'Add exhaustive input_stream invariant test + fixes' from Pavel Emelyanov tests: add exhaustive input_stream read/skip invariant test iostream: make skip() reject premature end of stream with exception > Merge 'Allow runtime selectability of GnuTLS or OpenSSL' from Noah Watkins net/tls: avoid potential read-past-buffer net/tls: move credential methods to generic tls layer net/tls: rename credentials_impl::dh_params to set_dh_params test/tls: enable openssl tls unit test test/tls: fix CA cert generation to use v3_ca extensions github: disable parallel test execution in alpine workflow crypto: support compiling seastar without gnutls net/tcp: use crypto provider for md5 calculation tls: fix test_peer_certificate_chain_handling for OpenSSL net/tls: fix test for self-signed server cert opoenssl compat net/tls: disable priority strings test for openssl provider core/crypto: expose crypto backend name for introspection test/tls: remove gnutls version guard net/tls: add openssl tls backend http: use backend agnostic tls error code net/tls: make error codes configurable by each tls backend net/tls: move reloadable_credentials to generic tls layer net/tls: move build_certificate to generic tls layer net/tls: move apply_to() to generic tls layer net/tls: move credential methods to generic tls layer net/tls: add OpenSSL-specific methods to public API with no-op defaults net/tls: introduce dh_params and credentials abstraction layer net/tls: add credentials_impl abstract base class net/tls: dispatch tls::error_category() through crypto_provider net/tls: dispatch wrap_client/wrap_server through crypto_provider net/tls: add tls_backend interface to crypto_provider net/tls: move public tls API methods to generic tls layer net/tls: move formatting utilities to generic tls layer net/tls: move credentials_builder blob methods to generic tls layer net/tls: move dh_params::from_file to generic tls layer net/tls: move abstract_credentials file methods to generic tls layer net/tls: move tls_socket_impl to generic tls layer net/tls: move server_session to general tls layer net/tls: move tls_connected_socket_impl to generic tls layer net/tls: move net::get_impl to generic tls layer net/tls: move session_ref to generic tls layer net/tls: add session_impl abstract interface for tls pluggability net/tls: rename tls.cc to be gnutls specific crypto: introduce crypto provider abstraction http: remove unused include > tls: test_send_two_large > rpc: include exception type for remote errors > GHA: increase timeout to 60 minutes > apps/httpd: replace deprecated reply::done() with write_body() > missing header(s) > net: Fix missing throw for runtime_error in create_native_net_device > tests/io_queue: account for token bucket refill granularity in bandwidth checks > Merge 'iovec: fix iovec_trim_front infinite loop on zero-length iovecs' from Travis Downs tests: add regression tests for zero-length iovec handling iovec: fix iovec_trim_front infinite loop on zero-length iovecs > util/process: graduate process management API from experimental > cooking: don't register ready.txt as a build output > sstring: make make_sstring not static > Add SparkyLinux to debian list in install-dependencies.sh > http: allow control over default response headers > Merge 'chunked_fifo: make cached chunk retention configurable' from Brandon Allard tests/perf: add chunked_fifo microbenchmarks chunked_fifo: set the default free chunk retention to 0 chunked_fifo: make free chunk retention configurable > Merge 'reactor_backend: fix pollable_fd_state_completion reuse in io_uring' from Kefu Chai tests: add regression test for pollable_fd_state_completion reuse reactor_backend: use reset() in AIO and epoll poll paths reactor_backend: fix pollable_fd_state_completion reuse after co_await in io_uring > Merge 'coroutine: Generator cleanups' from Kefu Chai coroutine/generator: extract schedule_or_resume helper coroutine/generator: remove unused next_awaiter classes coroutine/generator: remove write-only _started field coroutine/generator: assert on unreachable path in buffered await_resume coroutine/generator: add elements_of tag and #include <ranges> coroutine/generator: add empty() to bounded_container concept > cmake: bump minimum Boost version to 1.79.0 > seastar_test: remove unnecessary headers > cmake: bump minimum GnuTLS version to 3.7.4 > Merge 'reactor: add get_all_io_queues() method' from Travis Downs tests: add unit test for reactor::get_all_io_queues() reactor: add get_all_io_queues() method reactor: move get_io_queue and try_get_io_queue to .cc file > http: deprecate reply::done(), remove _response_line dead field > core: Deprecate scattered_message > ci: add workflow dispatch to tests workflow > perf_tests: exit non-zero when -t pattern matches no tests > Replace duplicate SEGV_MAPERR check in sigsegv_action() with SEGV_ACCERR. > perf_tests: add total runtime to json output > Merge 'Relax large allocation error originating from json_list_template' from Robert Bindar implement move assignment operator for json_list_template json_list_template copy assignment operator reserves capacity upfront > perf_tests: add --no-perf-counters option > Merge 'Fix to_human_readable_value() ability to work with large values' from Pavel Emelyanov memory: Add compile-time test for value-to-human-readable conversion memory: Extend list of suffixes to have peta-s memory: Fix off-by-one in suffix calculation memory: Mark to_human_readable_value() and others constexpr > http: Improve writing of response_line() into the output > Merge 'websocket: add template parameter for text/binary frame mode and implement client-side WebSocket' from wangyuwei websocket: add template parameter for text/binary frame mode websocket: impl client side websocket function > file: Fix checks for file being read-only > reactor: Make do_dump_task_queue a task_queue method > Merge 'Implement fully mixed mode for output_stream-s' from Pavel Emelyanov tests/output_stream: sample type patterns in sanitizer builds tests/output_stream: extend invariant test to cover mixed write modes iostream: allow unrestricted mixing of buffered and zero-copy writes tests/output_stream: remove obsolete ad-hoc splitting tests tests/output_stream: add invariant-based splitting tests iostream: rename output_stream::_size to ::_buffer_size > reactor_backend: replace virtual bool methods with const bool_class members > resource: Avoid copying CPU vector to break it into groups > perf_tests: increase overhead column precision to 3 decimal places > Merge 'Move reactor::fdatasync() into posix_file_impl' from Pavel Emelyanov reactor: Deprecate fdatasync() method file: Do fdatasync() right in the posix_file_impl::flush() file: Propagate aio_fdatasync to posix_file_impl reactor: Move reactor::fdatasync() code to file.cc reactor,file: Make full use of file_open_options::durable bit file: Add file_open_options::durable boolean file: Account io_stats::fsyncs in posix_file_impl::flush() reactor: Move _fsyncs counter onto io_stats > http: Remove connection::write_body() Closes scylladb/scylladb#29553
724 lines
24 KiB
C++
724 lines
24 KiB
C++
/*
|
|
* Copyright (C) 2016 ScyllaDB
|
|
*/
|
|
|
|
|
|
|
|
#include <boost/test/unit_test.hpp>
|
|
#include <stdint.h>
|
|
#include <random>
|
|
|
|
#include <seastar/core/future-util.hh>
|
|
#include <seastar/core/seastar.hh>
|
|
#include <seastar/core/shared_ptr.hh>
|
|
#include <seastar/core/thread.hh>
|
|
#include <seastar/core/fstream.hh>
|
|
|
|
#include <seastar/testing/test_case.hh>
|
|
|
|
#include "ent/encryption/encryption.hh"
|
|
#include "ent/encryption/symmetric_key.hh"
|
|
#include "ent/encryption/encrypted_file_impl.hh"
|
|
#include "test/lib/log.hh"
|
|
#include "test/lib/tmpdir.hh"
|
|
#include "test/lib/random_utils.hh"
|
|
#include "test/lib/exception_utils.hh"
|
|
#include "test/lib/limiting_data_source.hh"
|
|
#include "utils/io-wrappers.hh"
|
|
|
|
#include <seastar/util/memory-data-source.hh>
|
|
|
|
using namespace encryption;
|
|
|
|
static tmpdir dir;
|
|
|
|
static std::tuple<std::string, ::shared_ptr<symmetric_key>> make_filename(const std::string& name, ::shared_ptr<symmetric_key> k = nullptr) {
|
|
auto dst = std::string(dir.path() / std::string(name));
|
|
if (k == nullptr) {
|
|
key_info info{"AES/CBC", 256};
|
|
k = ::make_shared<symmetric_key>(info);
|
|
}
|
|
return std::make_tuple(dst, k);
|
|
}
|
|
|
|
static future<std::tuple<file, ::shared_ptr<symmetric_key>>> make_file(const std::string& name, open_flags mode, ::shared_ptr<symmetric_key> k_in = nullptr) {
|
|
auto [dst, k] = make_filename(name, std::move(k_in));
|
|
file f = co_await open_file_dma(dst, mode);
|
|
co_return std::tuple(file(make_encrypted_file(f, k)), k);
|
|
}
|
|
|
|
template<typename T = char>
|
|
static void fill_random(temporary_buffer<T>& buf) {
|
|
auto data = tests::random::get_sstring(buf.size());
|
|
std::copy(data.begin(), data.end(), buf.get_write());
|
|
}
|
|
|
|
template<typename T = char>
|
|
static temporary_buffer<T> generate_random(size_t n) {
|
|
temporary_buffer<T> tmp(n);
|
|
fill_random(tmp);
|
|
return tmp;
|
|
}
|
|
|
|
template<typename T = char>
|
|
static temporary_buffer<T> generate_random(size_t n, size_t align) {
|
|
auto tmp = temporary_buffer<T>::aligned(align, align_up(n, align));
|
|
fill_random(tmp);
|
|
return tmp;
|
|
}
|
|
|
|
static future<> test_random_data_disk(size_t n) {
|
|
auto name = "test_rand_" + std::to_string(n);
|
|
auto t = co_await make_file(name, open_flags::rw|open_flags::create);
|
|
auto f = std::get<0>(t);
|
|
std::exception_ptr ex = nullptr;
|
|
|
|
try {
|
|
auto k = std::get<1>(t);
|
|
auto a = f.memory_dma_alignment();
|
|
auto buf = generate_random(n, a);
|
|
auto w = co_await f.dma_write(0, buf.get(), buf.size());
|
|
|
|
co_await f.flush();
|
|
if (n != buf.size()) {
|
|
co_await f.truncate(n);
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(w, buf.size());
|
|
|
|
auto k2 = ::make_shared<symmetric_key>(k->info(), k->key());
|
|
auto f2 = std::get<0>(co_await make_file(name, open_flags::ro, k2));
|
|
|
|
auto tmp = temporary_buffer<uint8_t>::aligned(a, buf.size());
|
|
auto n2 = co_await f2.dma_read(0, tmp.get_write(), tmp.size());
|
|
|
|
BOOST_REQUIRE_EQUAL(n2, n);
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp.get(), tmp.get() + n2, buf.get(), buf.get() + n2);
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
co_await f.close();
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
}
|
|
|
|
static void test_random_data(size_t n) {
|
|
auto buf = generate_random(n, 8);
|
|
|
|
|
|
// first, verify padded.
|
|
{
|
|
key_info info{"AES/CBC/PKCSPadding", 256};
|
|
auto k = ::make_shared<symmetric_key>(info);
|
|
|
|
bytes b(bytes::initialized_later(), k->iv_len());
|
|
k->generate_iv(b.data(), k->iv_len());
|
|
|
|
temporary_buffer<uint8_t> tmp(n + k->block_size());
|
|
k->encrypt(buf.get(), buf.size(), tmp.get_write(), tmp.size(), b.data());
|
|
|
|
auto bytes = k->key();
|
|
auto k2 = ::make_shared<symmetric_key>(info, bytes);
|
|
|
|
temporary_buffer<uint8_t> tmp2(n + k->block_size());
|
|
k2->decrypt(tmp.get(), tmp.size(), tmp2.get_write(), tmp2.size(), b.data());
|
|
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp2.get(), tmp2.get() + n, buf.get(), buf.get() + n);
|
|
}
|
|
|
|
// unpadded
|
|
{
|
|
key_info info{"AES/CBC", 256};
|
|
auto k = ::make_shared<symmetric_key>(info);
|
|
|
|
bytes b(bytes::initialized_later(), k->iv_len());
|
|
k->generate_iv(b.data(), k->iv_len());
|
|
|
|
temporary_buffer<uint8_t> tmp(n);
|
|
k->encrypt_unpadded(buf.get(), buf.size(), tmp.get_write(), b.data());
|
|
|
|
auto bytes = k->key();
|
|
auto k2 = ::make_shared<symmetric_key>(info, bytes);
|
|
|
|
temporary_buffer<uint8_t> tmp2(buf.size());
|
|
k2->decrypt_unpadded(tmp.get(), tmp.size(), tmp2.get_write(), b.data());
|
|
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp2.get(), tmp2.get() + n, buf.get(), buf.get() + n);
|
|
}
|
|
}
|
|
|
|
|
|
BOOST_AUTO_TEST_CASE(test_encrypting_data_128) {
|
|
test_random_data(128);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(test_encrypting_data_4k) {
|
|
test_random_data(4*1024);
|
|
}
|
|
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_file_data_4k) {
|
|
return test_random_data_disk(4*1024);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_file_data_16k) {
|
|
return test_random_data_disk(16*1024);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_file_data_unaligned) {
|
|
return test_random_data_disk(16*1024 - 3);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_file_data_unaligned2) {
|
|
return test_random_data_disk(16*1024 - 4092);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_short) {
|
|
auto name = "test_short";
|
|
file f = co_await open_file_dma(sstring(dir.path() / name), open_flags::rw|open_flags::create);
|
|
co_await f.truncate(1);
|
|
co_await f.close();
|
|
|
|
auto t = co_await make_file(name, open_flags::ro);
|
|
f = std::get<0>(t);
|
|
std::exception_ptr ex = nullptr;
|
|
|
|
try {
|
|
temporary_buffer<char> buf(f.memory_dma_alignment());
|
|
|
|
BOOST_REQUIRE_EXCEPTION(
|
|
co_await f.dma_read(0, buf.get_write(), buf.size()),
|
|
std::domain_error,
|
|
exception_predicate::message_contains("file size 1, expected 0 or at least 16")
|
|
);
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
co_await f.close();
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_read_across_size_boundary) {
|
|
auto name = "test_read_across_size_boundary";
|
|
|
|
auto [dst, k] = co_await make_file(name, open_flags::rw|open_flags::create);
|
|
auto size = dst.disk_write_dma_alignment() - 1;
|
|
co_await dst.truncate(size);
|
|
co_await dst.close();
|
|
|
|
auto [f, _] = co_await make_file(name, open_flags::ro, k);
|
|
auto a = f.disk_write_dma_alignment();
|
|
auto m = f.memory_dma_alignment();
|
|
|
|
auto buf = temporary_buffer<char>::aligned(m, a);
|
|
auto n = co_await f.dma_read(0, buf.get_write(), buf.size());
|
|
|
|
auto buf2 = temporary_buffer<char>::aligned(m, a);
|
|
auto n2 = co_await f.dma_read(a, buf2.get_write(), buf2.size());
|
|
|
|
auto buf3 = temporary_buffer<char>::aligned(m, a);
|
|
std::vector<iovec> iov({{buf3.get_write(), buf3.size()}});
|
|
auto n3 = co_await f.dma_read(a, std::move(iov));
|
|
|
|
auto buf4 = co_await f.dma_read_bulk<char>(a, size_t(a));
|
|
|
|
co_await f.close();
|
|
|
|
BOOST_REQUIRE_EQUAL(size, n);
|
|
buf.trim(n);
|
|
for (auto c : buf) {
|
|
BOOST_REQUIRE_EQUAL(c, 0);
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(0, n2);
|
|
BOOST_REQUIRE_EQUAL(0, n3);
|
|
BOOST_REQUIRE_EQUAL(0, buf4.size());
|
|
}
|
|
|
|
static future<> test_read_across_size_boundary_unaligned_helper(int64_t size_off, int64_t read_off) {
|
|
auto name = "test_read_across_size_boundary_unaligned";
|
|
auto [dst, k] = co_await make_file(name, open_flags::rw|open_flags::create);
|
|
auto size = dst.disk_write_dma_alignment() + size_off;
|
|
co_await dst.truncate(size);
|
|
co_await dst.close();
|
|
|
|
auto [f, k2] = co_await make_file(name, open_flags::ro, k);
|
|
auto buf = co_await f.dma_read_bulk<char>(f.disk_write_dma_alignment() + read_off, size_t(f.disk_write_dma_alignment()));
|
|
|
|
co_await f.close();
|
|
|
|
BOOST_REQUIRE_EQUAL(0, buf.size());
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_read_across_size_boundary_unaligned) {
|
|
co_await test_read_across_size_boundary_unaligned_helper(-1, 1);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_read_across_size_boundary_unaligned2) {
|
|
co_await test_read_across_size_boundary_unaligned_helper(-2, -1);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_truncating_empty) {
|
|
auto name = "test_truncating_empty";
|
|
auto t = co_await make_file(name, open_flags::rw|open_flags::create);
|
|
auto f = std::get<0>(t);
|
|
auto k = std::get<1>(t);
|
|
auto s = 64 * f.memory_dma_alignment();
|
|
|
|
co_await f.truncate(s);
|
|
|
|
temporary_buffer<char> buf(s);
|
|
auto n = co_await f.dma_read(0, buf.get_write(), buf.size());
|
|
|
|
co_await f.close();
|
|
|
|
BOOST_REQUIRE_EQUAL(s, n);
|
|
|
|
for (auto c : buf) {
|
|
BOOST_REQUIRE_EQUAL(c, 0);
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_truncating_extend) {
|
|
auto name = "test_truncating_extend";
|
|
auto t = co_await make_file(name, open_flags::rw|open_flags::create);
|
|
auto f = std::get<0>(t);
|
|
auto k = std::get<1>(t);
|
|
auto a = f.memory_dma_alignment();
|
|
auto s = 32 * a;
|
|
auto buf = generate_random(s, a);
|
|
auto w = co_await f.dma_write(0, buf.get(), buf.size());
|
|
|
|
co_await f.flush();
|
|
BOOST_REQUIRE_EQUAL(s, w);
|
|
|
|
for (size_t i = 1; i < 64; ++i) {
|
|
// truncate smaller, unaligned
|
|
auto l = w - i;
|
|
auto r = w + 8 * a;
|
|
co_await f.truncate(l);
|
|
BOOST_REQUIRE_EQUAL(l, (co_await f.stat()).st_size);
|
|
|
|
{
|
|
auto tmp = temporary_buffer<uint8_t>::aligned(a, align_up(l, a));
|
|
auto n = co_await f.dma_read(0, tmp.get_write(), tmp.size());
|
|
|
|
BOOST_REQUIRE_EQUAL(l, n);
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp.get(), tmp.get() + l, buf.get(), buf.get() + l);
|
|
|
|
auto k = align_down(l, a);
|
|
|
|
while (k > 0) {
|
|
n = co_await f.dma_read(0, tmp.get_write(), k);
|
|
|
|
BOOST_REQUIRE_EQUAL(k, n);
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp.get(), tmp.get() + k, buf.get(), buf.get() + k);
|
|
|
|
n = co_await f.dma_read(k, tmp.get_write(), tmp.size());
|
|
BOOST_REQUIRE_EQUAL(l - k, n);
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp.get(), tmp.get() + n, buf.get() + k, buf.get() + k + n);
|
|
|
|
k -= a;
|
|
}
|
|
}
|
|
|
|
co_await f.truncate(r);
|
|
BOOST_REQUIRE_EQUAL(r, (co_await f.stat()).st_size);
|
|
|
|
auto tmp = temporary_buffer<uint8_t>::aligned(a, align_up(r, a));
|
|
auto n = co_await f.dma_read(0, tmp.get_write(), tmp.size());
|
|
|
|
BOOST_REQUIRE_EQUAL(r, n);
|
|
BOOST_REQUIRE_EQUAL_COLLECTIONS(tmp.get(), tmp.get() + l, buf.get(), buf.get() + l);
|
|
|
|
while (l < r) {
|
|
BOOST_REQUIRE_EQUAL(tmp[l], 0);
|
|
++l;
|
|
}
|
|
}
|
|
|
|
co_await f.close();
|
|
}
|
|
|
|
// Reproducer for https://github.com/scylladb/scylladb/issues/22236
|
|
SEASTAR_TEST_CASE(test_read_from_padding) {
|
|
key_info kinfo {"AES/CBC/PKCSPadding", 128};
|
|
shared_ptr<symmetric_key> k = make_shared<symmetric_key>(kinfo);
|
|
testlog.info("Created symmetric key: info={} key={} ", k->info(), k->key());
|
|
|
|
size_t block_size;
|
|
size_t buf_size;
|
|
|
|
constexpr auto& filename = "encrypted_file";
|
|
const auto& filepath = dir.path() / filename;
|
|
|
|
testlog.info("Creating encrypted file {}", filepath.string());
|
|
{
|
|
auto [file, _] = co_await make_file(filename, open_flags::create | open_flags::wo, k);
|
|
auto ostream = co_await make_file_output_stream(file);
|
|
|
|
block_size = file.disk_write_dma_alignment();
|
|
buf_size = block_size - 1;
|
|
|
|
auto wbuf = seastar::temporary_buffer<char>::aligned(file.memory_dma_alignment(), buf_size);
|
|
co_await ostream.write(wbuf.get(), wbuf.size());
|
|
testlog.info("Wrote {} bytes to encrypted file {}", wbuf.size(), filepath.string());
|
|
|
|
co_await ostream.close();
|
|
testlog.info("Length of {}: {} bytes", filename, co_await file.size());
|
|
}
|
|
|
|
testlog.info("Testing DMA reads from padding area of file {}", filepath.string());
|
|
{
|
|
auto [file, _] = co_await make_file(filename, open_flags::ro, k);
|
|
|
|
// Triggering the bug requires reading from the padding area:
|
|
// `buf_size < read_pos < file.size()`
|
|
//
|
|
// For `dma_read()`, we have the additional requirement that `read_pos` must be aligned.
|
|
// For `dma_read_bulk()`, it doesn't have to.
|
|
uint64_t read_pos = block_size;
|
|
size_t read_len = block_size;
|
|
auto rbuf = seastar::temporary_buffer<char>::aligned(file.memory_dma_alignment(), read_len);
|
|
std::vector<iovec> iov {{static_cast<void*>(rbuf.get_write()), rbuf.size()}};
|
|
|
|
auto res = co_await file.dma_read_bulk<char>(read_pos, read_len);
|
|
BOOST_CHECK_MESSAGE(res.size() == 0, seastar::format(
|
|
"Bulk DMA read on pos {}, len {}: returned {} bytes instead of zero", read_pos, read_len, res.size()));
|
|
|
|
auto res_len = co_await file.dma_read(read_pos, iov);
|
|
BOOST_CHECK_MESSAGE(res_len == 0, seastar::format(
|
|
"IOV DMA read on pos {}, len {}: returned {} bytes instead of zero", read_pos, read_len, res_len));
|
|
|
|
res_len = co_await file.dma_read<char>(read_pos, rbuf.get_write(), read_len);
|
|
BOOST_CHECK_MESSAGE(res_len == 0, seastar::format(
|
|
"DMA read on pos {}, len {}: returned {} bytes instead of zero", read_pos, read_len, res_len));
|
|
|
|
co_await file.close();
|
|
}
|
|
}
|
|
|
|
namespace seastar {
|
|
std::ostream& operator<<(std::ostream& os, const temporary_buffer<char>& buf) {
|
|
return os << "temporary_buffer[size=" << buf.size() << ", data=" << std::string_view(buf.get(), buf.size()) << "]";
|
|
}
|
|
}
|
|
|
|
static future<> test_random_data_sink(std::vector<size_t> sizes) {
|
|
auto name = "test_rand_sink";
|
|
std::vector<temporary_buffer<char>> bufs, srcs;
|
|
|
|
auto [dst, k] = make_filename(name);
|
|
uint64_t total = 0;
|
|
std::exception_ptr ex = nullptr;
|
|
|
|
data_sink sink(make_encrypted_sink(create_memory_sink(bufs), k));
|
|
|
|
try {
|
|
for (size_t s : sizes) {
|
|
auto buf = generate_random<char>(s);
|
|
co_await sink.put(buf.clone()); // deep copy. encrypted sink uses "owned" data
|
|
total += buf.size();
|
|
srcs.emplace_back(std::move(buf));
|
|
}
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
co_await sink.close();
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
|
|
{
|
|
auto os = co_await make_file_output_stream(co_await open_file_dma(dst, open_flags::wo|open_flags::create));
|
|
for (auto& buf : bufs) {
|
|
co_await os.write(buf.get(), buf.size());
|
|
}
|
|
co_await os.flush();
|
|
co_await os.close();
|
|
}
|
|
|
|
file f = make_encrypted_file(co_await open_file_dma(dst, open_flags::ro), k);
|
|
|
|
try {
|
|
auto file_size = co_await f.size();
|
|
|
|
BOOST_REQUIRE_EQUAL(file_size, total);
|
|
|
|
auto in = make_file_input_stream(std::move(f));
|
|
|
|
for (auto& src : srcs) {
|
|
auto tmp = co_await in.read_exactly(src.size());
|
|
BOOST_REQUIRE_EQUAL(tmp, src);
|
|
}
|
|
co_await in.close();
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
if (f) {
|
|
co_await f.close();
|
|
}
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_sink_data_small) {
|
|
return test_random_data_sink({ 13 });
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_sink_data_smallish) {
|
|
return test_random_data_sink({ 4*1024, 4*1024, 1467 });
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_sink_data_medium) {
|
|
return test_random_data_sink({ 4*1024, 4*1024, 2*1024, 1457, 234, 999 });
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_sink_data_large) {
|
|
return test_random_data_sink({ 4096, 4096, 4096, 4096, 8192, 1232, 32, 4096, 134 });
|
|
}
|
|
|
|
static future<> test_random_data_source(std::vector<size_t> sizes) {
|
|
testlog.info("test_random_data_source with sizes: {} ({})", sizes, std::accumulate(sizes.begin(), sizes.end(), size_t(0), std::plus{}));
|
|
|
|
auto name = "test_rand_source";
|
|
std::vector<temporary_buffer<char>> bufs, srcs;
|
|
|
|
auto [dst, k] = make_filename(name);
|
|
using namespace std::chrono_literals;
|
|
std::exception_ptr ex = nullptr;
|
|
|
|
data_sink sink(make_encrypted_sink(create_memory_sink(bufs), k));
|
|
|
|
try {
|
|
for (size_t s : sizes) {
|
|
auto buf = generate_random<char>(s);
|
|
co_await sink.put(buf.clone()); // deep copy. encrypted sink uses "owned" data
|
|
srcs.emplace_back(std::move(buf));
|
|
}
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
co_await sink.close();
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
|
|
{
|
|
auto os = co_await make_file_output_stream(co_await open_file_dma(dst, open_flags::truncate|open_flags::wo | open_flags::create));
|
|
for (auto& buf : bufs) {
|
|
co_await os.write(buf.get(), buf.size());
|
|
}
|
|
co_await os.flush();
|
|
co_await os.close();
|
|
}
|
|
|
|
auto f = co_await open_file_dma(dst, open_flags::ro);
|
|
testlog.info("file source {}", (co_await f.stat()).st_size);
|
|
|
|
auto source = make_file_data_source(std::move(f), file_input_stream_options{});
|
|
|
|
class random_chunk_source
|
|
: public data_source_impl
|
|
{
|
|
data_source _source;
|
|
temporary_buffer<char> _buf;
|
|
public:
|
|
random_chunk_source(data_source s)
|
|
: _source(std::move(s))
|
|
{}
|
|
future<temporary_buffer<char>> get() override {
|
|
if (!_buf.empty()) {
|
|
co_return std::exchange(_buf, temporary_buffer<char>{});
|
|
}
|
|
_buf = co_await _source.get();
|
|
if (_buf.empty()) {
|
|
co_return temporary_buffer<char>{};
|
|
}
|
|
auto n = tests::random::get_int(size_t(1), _buf.size());
|
|
auto res = _buf.share(0, n);
|
|
_buf.trim_front(n);
|
|
co_return res;
|
|
}
|
|
future<temporary_buffer<char>> skip(uint64_t n) override {
|
|
if (!_buf.empty()) {
|
|
auto m = std::min(n, _buf.size());
|
|
_buf.trim_front(m);
|
|
n -= m;
|
|
}
|
|
if (n) {
|
|
co_await _source.skip(n);
|
|
}
|
|
co_return temporary_buffer<char>{};
|
|
}
|
|
};
|
|
try {
|
|
auto encrypted_source = data_source(make_encrypted_source(data_source(std::make_unique<random_chunk_source>(std::move(source))), k));
|
|
temporary_buffer<char> unified_buff(std::accumulate(srcs.begin(), srcs.end(), 0, [](size_t acc, const auto& buf) { return acc + buf.size(); }));
|
|
size_t pos = 0;
|
|
for (const auto& src : srcs) {
|
|
memcpy(unified_buff.get_write() + pos, src.get(), src.size());
|
|
pos += src.size();
|
|
}
|
|
|
|
pos = 0;
|
|
while (auto read_buff = co_await encrypted_source.get()) {
|
|
auto rem = unified_buff.size() - pos;
|
|
BOOST_REQUIRE_LE(read_buff.size(), rem);
|
|
size_t size_to_compare = std::min(rem, read_buff.size());
|
|
auto v1 = std::string_view(read_buff.get(), size_to_compare);
|
|
auto v2 = std::string_view(unified_buff.get() + pos, size_to_compare);
|
|
BOOST_REQUIRE_EQUAL(v1, v2);
|
|
pos += size_to_compare;
|
|
auto skip = unified_buff.size() - pos > 4113 ? 4097 : (unified_buff.size() - pos)/2;
|
|
co_await encrypted_source.skip(skip);
|
|
pos += skip;
|
|
}
|
|
co_await encrypted_source.close();
|
|
} catch (...) {
|
|
ex = std::current_exception();
|
|
}
|
|
|
|
|
|
if (ex) {
|
|
std::rethrow_exception(ex);
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_data_source_simple) {
|
|
std::vector<size_t> sizes({3200, 13086, 12065, 200, 11959, 12159, 12852});
|
|
co_await test_random_data_source(sizes);
|
|
}
|
|
|
|
// Reproduces the production deadlock where encrypted SSTable component downloads
|
|
// got stuck during restore. The encrypted_data_source::get() caches a block in
|
|
// _next, then on the next call bypasses input_stream::read()'s _eof check and
|
|
// calls input_stream::read_exactly() — which does NOT check _eof when _buf is
|
|
// empty. This causes a second get() on the underlying source after EOS.
|
|
//
|
|
// In production the underlying source was chunked_download_source whose get()
|
|
// hung forever. Here we simulate it with a strict source that fails the test.
|
|
//
|
|
// The fix belongs in seastar's input_stream::read_exactly(): check _eof before
|
|
// calling _fd.get(), consistent with read(), read_up_to(), and consume().
|
|
static future<> test_encrypted_source_copy(size_t plaintext_size) {
|
|
testlog.info("test_encrypted_source_copy: plaintext_size={}", plaintext_size);
|
|
|
|
key_info info{"AES/CBC", 256};
|
|
auto k = ::make_shared<symmetric_key>(info);
|
|
|
|
// Step 1: Encrypt the plaintext into memory buffers
|
|
auto plaintext = generate_random<char>(plaintext_size);
|
|
std::vector<temporary_buffer<char>> encrypted_bufs;
|
|
{
|
|
data_sink sink(make_encrypted_sink(create_memory_sink(encrypted_bufs), k));
|
|
co_await sink.put(plaintext.clone());
|
|
co_await sink.close();
|
|
}
|
|
|
|
// Flatten encrypted buffers into a single contiguous buffer
|
|
size_t encrypted_total = 0;
|
|
for (const auto& b : encrypted_bufs) {
|
|
encrypted_total += b.size();
|
|
}
|
|
temporary_buffer<char> encrypted(encrypted_total);
|
|
size_t pos = 0;
|
|
for (const auto& b : encrypted_bufs) {
|
|
std::copy(b.begin(), b.end(), encrypted.get_write() + pos);
|
|
pos += b.size();
|
|
}
|
|
|
|
// Step 2: Create a data source from the encrypted data that fails on
|
|
// post-EOS get() — simulating a source like chunked_download_source
|
|
// that would hang forever in this situation.
|
|
class strict_memory_source final : public limiting_data_source_impl {
|
|
bool _eof = false;
|
|
public:
|
|
strict_memory_source(temporary_buffer<char> data, size_t chunk_size)
|
|
: limiting_data_source_impl(
|
|
data_source(std::make_unique<util::temporary_buffer_data_source>(std::move(data))),
|
|
chunk_size) {}
|
|
|
|
future<temporary_buffer<char>> get() override {
|
|
BOOST_REQUIRE_MESSAGE(!_eof,
|
|
"get() called on source after it already returned EOS — "
|
|
"this is the production deadlock: read_exactly() does not "
|
|
"check _eof before calling _fd.get()");
|
|
auto buf = co_await limiting_data_source_impl::get();
|
|
_eof = buf.empty();
|
|
co_return buf;
|
|
}
|
|
};
|
|
|
|
// Step 3: Wrap in encrypted_data_source and drain via consume() —
|
|
// the exact code path used by seastar::copy() which is what
|
|
// sstables_loader_helpers::download_sstable() calls.
|
|
// Try multiple chunk sizes to hit different alignment scenarios.
|
|
for (size_t chunk_size : {1ul, 7ul, 4096ul, 8192ul, encrypted_total, encrypted_total + 1}) {
|
|
if (chunk_size == 0) continue;
|
|
auto src = data_source(make_encrypted_source(
|
|
data_source(std::make_unique<strict_memory_source>(encrypted.clone(), chunk_size)), k));
|
|
auto in = input_stream<char>(std::move(src));
|
|
|
|
// consume() is what seastar::copy() uses internally. It calls
|
|
// encrypted_data_source::get() via _fd.get() until EOF.
|
|
size_t total_decrypted = 0;
|
|
co_await in.consume([&total_decrypted](temporary_buffer<char> buf) {
|
|
total_decrypted += buf.size();
|
|
return make_ready_future<consumption_result<char>>(continue_consuming{});
|
|
});
|
|
co_await in.close();
|
|
|
|
BOOST_REQUIRE_EQUAL(total_decrypted, plaintext_size);
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_8k) {
|
|
co_await test_encrypted_source_copy(8192);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_4k) {
|
|
co_await test_encrypted_source_copy(4096);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_small) {
|
|
co_await test_encrypted_source_copy(100);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_12k) {
|
|
co_await test_encrypted_source_copy(12288);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_unaligned) {
|
|
co_await test_encrypted_source_copy(8193);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_source_copy_1byte) {
|
|
co_await test_encrypted_source_copy(1);
|
|
}
|
|
|
|
|
|
SEASTAR_TEST_CASE(test_encrypted_data_source_fuzzy) {
|
|
std::mt19937_64 rand_gen(std::random_device{}());
|
|
for (auto i = 0; i < 1000; ++i) {
|
|
std::uniform_int_distribution<uint16_t> rand_dist(1, 15);
|
|
std::vector<size_t> sizes(rand_dist(rand_gen));
|
|
for (auto& s : sizes) {
|
|
std::uniform_int_distribution<uint16_t> buff_sizes(1, 147*100);
|
|
s = buff_sizes(rand_gen);
|
|
}
|
|
co_await test_random_data_source(sizes);
|
|
}
|
|
|
|
co_return;
|
|
}
|