Skip to content

Commit dd6d728

Browse files
tolleybotpitrouadamreevewgtmac
authored
GH-39444: [C++][Parquet] Fix crash in Modular Encryption (#39623)
**Rationale for this change:** This pull request addresses a critical issue (GH-39444) in the C++/Python components of Parquet, specifically a segmentation fault occurring when processing encrypted datasets over 2^15 rows. The fix involves modifications in `cpp/src/parquet/encryption/internal_file_decryptor.cc`, particularly in `InternalFileDecryptor::GetColumnDecryptor`. The caching of the `Decryptor` object was removed to resolve the multithreading issue causing the segmentation fault and encryption failures. **What changes are included in this PR?** - Removal of `Decryptor` object caching in `InternalFileDecryptor::GetColumnDecryptor`. - Addition of two unit tests: `large_row_parquet_encrypt_test.cc` for C++ and an update to `test_dataset_encryption.py` with `test_large_row_encryption_decryption` for Python. **Are these changes tested?** Yes, the unit tests (`large_row_parquet_encrypt_test.cc` and `test_large_row_encryption_decryption` in `test_dataset_encryption.py`) have been added to ensure the reliability and effectiveness of these changes. **Are there any user-facing changes?** No significant user-facing changes, but the update significantly improves the backend stability and reliability of Parquet file handling. Calling DecryptionKeyRetriever::GetKey could be an expensive operation potentially involving network calls to key management servers. * Closes: #39444 * GitHub Issue: #39444 Lead-authored-by: Donald Tolley <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]> Co-authored-by: Adam Reeve <[email protected]> Co-authored-by: Gang Wu <[email protected]> Signed-off-by: Antoine Pitrou <[email protected]>
1 parent 788200a commit dd6d728

5 files changed

Lines changed: 196 additions & 119 deletions

File tree

‎cpp/src/arrow/dataset/file_parquet_encryption_test.cc‎

Lines changed: 111 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@
1919

2020
#include "gtest/gtest.h"
2121

22-
#include <arrow/dataset/dataset.h>
23-
#include <arrow/dataset/file_base.h>
24-
#include <arrow/dataset/file_parquet.h>
2522
#include "arrow/array.h"
23+
#include "arrow/dataset/dataset.h"
24+
#include "arrow/dataset/file_base.h"
25+
#include "arrow/dataset/file_parquet.h"
2626
#include "arrow/dataset/parquet_encryption_config.h"
2727
#include "arrow/dataset/partition.h"
2828
#include "arrow/filesystem/mockfs.h"
2929
#include "arrow/io/api.h"
3030
#include "arrow/status.h"
3131
#include "arrow/table.h"
3232
#include "arrow/testing/gtest_util.h"
33+
#include "arrow/testing/random.h"
3334
#include "arrow/type.h"
3435
#include "parquet/arrow/reader.h"
3536
#include "parquet/encryption/crypto_factory.h"
36-
#include "parquet/encryption/encryption.h"
3737
#include "parquet/encryption/encryption_internal.h"
3838
#include "parquet/encryption/kms_client.h"
3939
#include "parquet/encryption/test_in_memory_kms.h"
@@ -51,14 +51,14 @@ using arrow::internal::checked_pointer_cast;
5151
namespace arrow {
5252
namespace dataset {
5353

54-
class DatasetEncryptionTest : public ::testing::Test {
54+
// Base class to test writing and reading encrypted dataset.
55+
class DatasetEncryptionTestBase : public ::testing::Test {
5556
public:
5657
// This function creates a mock file system using the current time point, creates a
5758
// directory with the given base directory path, and writes a dataset to it using
58-
// provided Parquet file write options. The dataset is partitioned using a Hive
59-
// partitioning scheme. The function also checks if the written files exist in the file
60-
// system.
61-
static void SetUpTestSuite() {
59+
// provided Parquet file write options. The function also checks if the written files
60+
// exist in the file system.
61+
void SetUp() override {
6262
#ifdef ARROW_VALGRIND
6363
// Not necessary otherwise, but prevents a Valgrind leak by making sure
6464
// OpenSSL initialization is done from the main thread
@@ -71,24 +71,8 @@ class DatasetEncryptionTest : public ::testing::Test {
7171
std::chrono::system_clock::now(), {}));
7272
ASSERT_OK(file_system_->CreateDir(std::string(kBaseDir)));
7373

74-
// Prepare table data.
75-
auto table_schema = schema({field("a", int64()), field("c", int64()),
76-
field("e", int64()), field("part", utf8())});
77-
table_ = TableFromJSON(table_schema, {R"([
78-
[ 0, 9, 1, "a" ],
79-
[ 1, 8, 2, "a" ],
80-
[ 2, 7, 1, "c" ],
81-
[ 3, 6, 2, "c" ],
82-
[ 4, 5, 1, "e" ],
83-
[ 5, 4, 2, "e" ],
84-
[ 6, 3, 1, "g" ],
85-
[ 7, 2, 2, "g" ],
86-
[ 8, 1, 1, "i" ],
87-
[ 9, 0, 2, "i" ]
88-
])"});
89-
90-
// Use a Hive-style partitioning scheme.
91-
partitioning_ = std::make_shared<HivePartitioning>(schema({field("part", utf8())}));
74+
// Init dataset and partitioning.
75+
ASSERT_NO_FATAL_FAILURE(PrepareTableAndPartitioning());
9276

9377
// Prepare encryption properties.
9478
std::unordered_map<std::string, std::string> key_map;
@@ -133,13 +117,81 @@ class DatasetEncryptionTest : public ::testing::Test {
133117
ASSERT_OK(FileSystemDataset::Write(write_options, std::move(scanner)));
134118
}
135119

120+
virtual void PrepareTableAndPartitioning() = 0;
121+
122+
void TestScanDataset() {
123+
// Create decryption properties.
124+
auto decryption_config =
125+
std::make_shared<parquet::encryption::DecryptionConfiguration>();
126+
auto parquet_decryption_config = std::make_shared<ParquetDecryptionConfig>();
127+
parquet_decryption_config->crypto_factory = crypto_factory_;
128+
parquet_decryption_config->kms_connection_config = kms_connection_config_;
129+
parquet_decryption_config->decryption_config = std::move(decryption_config);
130+
131+
// Set scan options.
132+
auto parquet_scan_options = std::make_shared<ParquetFragmentScanOptions>();
133+
parquet_scan_options->parquet_decryption_config =
134+
std::move(parquet_decryption_config);
135+
136+
auto file_format = std::make_shared<ParquetFileFormat>();
137+
file_format->default_fragment_scan_options = std::move(parquet_scan_options);
138+
139+
// Get FileInfo objects for all files under the base directory
140+
fs::FileSelector selector;
141+
selector.base_dir = kBaseDir;
142+
selector.recursive = true;
143+
144+
FileSystemFactoryOptions factory_options;
145+
factory_options.partitioning = partitioning_;
146+
factory_options.partition_base_dir = kBaseDir;
147+
ASSERT_OK_AND_ASSIGN(auto dataset_factory,
148+
FileSystemDatasetFactory::Make(file_system_, selector,
149+
file_format, factory_options));
150+
151+
// Read dataset into table
152+
ASSERT_OK_AND_ASSIGN(auto dataset, dataset_factory->Finish());
153+
ASSERT_OK_AND_ASSIGN(auto scanner_builder, dataset->NewScan());
154+
ASSERT_OK_AND_ASSIGN(auto scanner, scanner_builder->Finish());
155+
ASSERT_OK_AND_ASSIGN(auto read_table, scanner->ToTable());
156+
157+
// Verify the data was read correctly
158+
ASSERT_OK_AND_ASSIGN(auto combined_table, read_table->CombineChunks());
159+
// Validate the table
160+
ASSERT_OK(combined_table->ValidateFull());
161+
AssertTablesEqual(*combined_table, *table_);
162+
}
163+
136164
protected:
137-
inline static std::shared_ptr<fs::FileSystem> file_system_;
138-
inline static std::shared_ptr<Table> table_;
139-
inline static std::shared_ptr<HivePartitioning> partitioning_;
140-
inline static std::shared_ptr<parquet::encryption::CryptoFactory> crypto_factory_;
141-
inline static std::shared_ptr<parquet::encryption::KmsConnectionConfig>
142-
kms_connection_config_;
165+
std::shared_ptr<fs::FileSystem> file_system_;
166+
std::shared_ptr<Table> table_;
167+
std::shared_ptr<Partitioning> partitioning_;
168+
std::shared_ptr<parquet::encryption::CryptoFactory> crypto_factory_;
169+
std::shared_ptr<parquet::encryption::KmsConnectionConfig> kms_connection_config_;
170+
};
171+
172+
class DatasetEncryptionTest : public DatasetEncryptionTestBase {
173+
public:
174+
// The dataset is partitioned using a Hive partitioning scheme.
175+
void PrepareTableAndPartitioning() override {
176+
// Prepare table data.
177+
auto table_schema = schema({field("a", int64()), field("c", int64()),
178+
field("e", int64()), field("part", utf8())});
179+
table_ = TableFromJSON(table_schema, {R"([
180+
[ 0, 9, 1, "a" ],
181+
[ 1, 8, 2, "a" ],
182+
[ 2, 7, 1, "c" ],
183+
[ 3, 6, 2, "c" ],
184+
[ 4, 5, 1, "e" ],
185+
[ 5, 4, 2, "e" ],
186+
[ 6, 3, 1, "g" ],
187+
[ 7, 2, 2, "g" ],
188+
[ 8, 1, 1, "i" ],
189+
[ 9, 0, 2, "i" ]
190+
])"});
191+
192+
// Use a Hive-style partitioning scheme.
193+
partitioning_ = std::make_shared<HivePartitioning>(schema({field("part", utf8())}));
194+
}
143195
};
144196

145197
// This test demonstrates the process of writing a partitioned Parquet file with the same
@@ -148,44 +200,7 @@ class DatasetEncryptionTest : public ::testing::Test {
148200
// test reads the data back and verifies that it can be successfully decrypted and
149201
// scanned.
150202
TEST_F(DatasetEncryptionTest, WriteReadDatasetWithEncryption) {
151-
// Create decryption properties.
152-
auto decryption_config =
153-
std::make_shared<parquet::encryption::DecryptionConfiguration>();
154-
auto parquet_decryption_config = std::make_shared<ParquetDecryptionConfig>();
155-
parquet_decryption_config->crypto_factory = crypto_factory_;
156-
parquet_decryption_config->kms_connection_config = kms_connection_config_;
157-
parquet_decryption_config->decryption_config = std::move(decryption_config);
158-
159-
// Set scan options.
160-
auto parquet_scan_options = std::make_shared<ParquetFragmentScanOptions>();
161-
parquet_scan_options->parquet_decryption_config = std::move(parquet_decryption_config);
162-
163-
auto file_format = std::make_shared<ParquetFileFormat>();
164-
file_format->default_fragment_scan_options = std::move(parquet_scan_options);
165-
166-
// Get FileInfo objects for all files under the base directory
167-
fs::FileSelector selector;
168-
selector.base_dir = kBaseDir;
169-
selector.recursive = true;
170-
171-
FileSystemFactoryOptions factory_options;
172-
factory_options.partitioning = partitioning_;
173-
factory_options.partition_base_dir = kBaseDir;
174-
ASSERT_OK_AND_ASSIGN(auto dataset_factory,
175-
FileSystemDatasetFactory::Make(file_system_, selector, file_format,
176-
factory_options));
177-
178-
// Read dataset into table
179-
ASSERT_OK_AND_ASSIGN(auto dataset, dataset_factory->Finish());
180-
ASSERT_OK_AND_ASSIGN(auto scanner_builder, dataset->NewScan());
181-
ASSERT_OK_AND_ASSIGN(auto scanner, scanner_builder->Finish());
182-
ASSERT_OK_AND_ASSIGN(auto read_table, scanner->ToTable());
183-
184-
// Verify the data was read correctly
185-
ASSERT_OK_AND_ASSIGN(auto combined_table, read_table->CombineChunks());
186-
// Validate the table
187-
ASSERT_OK(combined_table->ValidateFull());
188-
AssertTablesEqual(*combined_table, *table_);
203+
ASSERT_NO_FATAL_FAILURE(TestScanDataset());
189204
}
190205

191206
// Read a single parquet file with and without decryption properties.
@@ -220,5 +235,30 @@ TEST_F(DatasetEncryptionTest, ReadSingleFile) {
220235
ASSERT_EQ(checked_pointer_cast<Int64Array>(table->column(2)->chunk(0))->GetView(0), 1);
221236
}
222237

238+
// GH-39444: This test covers the case where parquet dataset scanner crashes when
239+
// processing encrypted datasets over 2^15 rows in multi-threaded mode.
240+
class LargeRowEncryptionTest : public DatasetEncryptionTestBase {
241+
public:
242+
// The dataset is partitioned using a Hive partitioning scheme.
243+
void PrepareTableAndPartitioning() override {
244+
// Specifically chosen to be greater than batch size for triggering prefetch.
245+
constexpr int kRowCount = 32769;
246+
247+
// Create a random floating-point array with large number of rows.
248+
arrow::random::RandomArrayGenerator rand_gen(0);
249+
auto array = rand_gen.Float32(kRowCount, 0.0, 1.0, false);
250+
auto table_schema = schema({field("a", float32())});
251+
252+
// Prepare table and partitioning.
253+
table_ = arrow::Table::Make(table_schema, {array});
254+
partitioning_ = std::make_shared<dataset::DirectoryPartitioning>(arrow::schema({}));
255+
}
256+
};
257+
258+
// Test for writing and reading encrypted dataset with large row count.
259+
TEST_F(LargeRowEncryptionTest, ReadEncryptLargeRows) {
260+
ASSERT_NO_FATAL_FAILURE(TestScanDataset());
261+
}
262+
223263
} // namespace dataset
224264
} // namespace arrow

‎cpp/src/parquet/encryption/encryption_internal.cc‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,7 @@ class AesEncryptor::AesEncryptorImpl {
5555
explicit AesEncryptorImpl(ParquetCipher::type alg_id, int key_len, bool metadata,
5656
bool write_length);
5757

58-
~AesEncryptorImpl() {
59-
if (nullptr != ctx_) {
60-
EVP_CIPHER_CTX_free(ctx_);
61-
ctx_ = nullptr;
62-
}
63-
}
58+
~AesEncryptorImpl() { WipeOut(); }
6459

6560
int Encrypt(const uint8_t* plaintext, int plaintext_len, const uint8_t* key,
6661
int key_len, const uint8_t* aad, int aad_len, uint8_t* ciphertext);
@@ -318,12 +313,7 @@ class AesDecryptor::AesDecryptorImpl {
318313
explicit AesDecryptorImpl(ParquetCipher::type alg_id, int key_len, bool metadata,
319314
bool contains_length);
320315

321-
~AesDecryptorImpl() {
322-
if (nullptr != ctx_) {
323-
EVP_CIPHER_CTX_free(ctx_);
324-
ctx_ = nullptr;
325-
}
326-
}
316+
~AesDecryptorImpl() { WipeOut(); }
327317

328318
int Decrypt(const uint8_t* ciphertext, int ciphertext_len, const uint8_t* key,
329319
int key_len, const uint8_t* aad, int aad_len, uint8_t* plaintext);

‎cpp/src/parquet/encryption/internal_file_decryptor.cc‎

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ InternalFileDecryptor::InternalFileDecryptor(FileDecryptionProperties* propertie
6161
}
6262

6363
void InternalFileDecryptor::WipeOutDecryptionKeys() {
64+
std::lock_guard<std::mutex> lock(mutex_);
6465
properties_->WipeOutDecryptionKeys();
6566
for (auto const& i : all_decryptors_) {
6667
if (auto aes_decryptor = i.lock()) {
@@ -139,10 +140,16 @@ std::shared_ptr<Decryptor> InternalFileDecryptor::GetFooterDecryptor(
139140
// Create both data and metadata decryptors to avoid redundant retrieval of key
140141
// from the key_retriever.
141142
int key_len = static_cast<int>(footer_key.size());
142-
auto aes_metadata_decryptor = encryption::AesDecryptor::Make(
143-
algorithm_, key_len, /*metadata=*/true, &all_decryptors_);
144-
auto aes_data_decryptor = encryption::AesDecryptor::Make(
145-
algorithm_, key_len, /*metadata=*/false, &all_decryptors_);
143+
std::shared_ptr<encryption::AesDecryptor> aes_metadata_decryptor;
144+
std::shared_ptr<encryption::AesDecryptor> aes_data_decryptor;
145+
146+
{
147+
std::lock_guard<std::mutex> lock(mutex_);
148+
aes_metadata_decryptor = encryption::AesDecryptor::Make(
149+
algorithm_, key_len, /*metadata=*/true, &all_decryptors_);
150+
aes_data_decryptor = encryption::AesDecryptor::Make(
151+
algorithm_, key_len, /*metadata=*/false, &all_decryptors_);
152+
}
146153

147154
footer_metadata_decryptor_ = std::make_shared<Decryptor>(
148155
aes_metadata_decryptor, footer_key, file_aad_, aad, pool_);
@@ -168,21 +175,7 @@ std::shared_ptr<Decryptor> InternalFileDecryptor::GetColumnDataDecryptor(
168175
std::shared_ptr<Decryptor> InternalFileDecryptor::GetColumnDecryptor(
169176
const std::string& column_path, const std::string& column_key_metadata,
170177
const std::string& aad, bool metadata) {
171-
std::string column_key;
172-
// first look if we already got the decryptor from before
173-
if (metadata) {
174-
if (column_metadata_map_.find(column_path) != column_metadata_map_.end()) {
175-
auto res(column_metadata_map_.at(column_path));
176-
res->UpdateAad(aad);
177-
return res;
178-
}
179-
} else {
180-
if (column_data_map_.find(column_path) != column_data_map_.end()) {
181-
auto res(column_data_map_.at(column_path));
182-
res->UpdateAad(aad);
183-
return res;
184-
}
185-
}
178+
std::string column_key = properties_->column_key(column_path);
186179

187180
column_key = properties_->column_key(column_path);
188181
// No explicit column key given via API. Retrieve via key metadata.
@@ -200,21 +193,12 @@ std::shared_ptr<Decryptor> InternalFileDecryptor::GetColumnDecryptor(
200193
throw HiddenColumnException("HiddenColumnException, path=" + column_path);
201194
}
202195

203-
// Create both data and metadata decryptors to avoid redundant retrieval of key
204-
// using the key_retriever.
205196
int key_len = static_cast<int>(column_key.size());
206-
auto aes_metadata_decryptor = encryption::AesDecryptor::Make(
207-
algorithm_, key_len, /*metadata=*/true, &all_decryptors_);
208-
auto aes_data_decryptor = encryption::AesDecryptor::Make(
209-
algorithm_, key_len, /*metadata=*/false, &all_decryptors_);
210-
211-
column_metadata_map_[column_path] = std::make_shared<Decryptor>(
212-
aes_metadata_decryptor, column_key, file_aad_, aad, pool_);
213-
column_data_map_[column_path] =
214-
std::make_shared<Decryptor>(aes_data_decryptor, column_key, file_aad_, aad, pool_);
215-
216-
if (metadata) return column_metadata_map_[column_path];
217-
return column_data_map_[column_path];
197+
std::lock_guard<std::mutex> lock(mutex_);
198+
auto aes_decryptor =
199+
encryption::AesDecryptor::Make(algorithm_, key_len, metadata, &all_decryptors_);
200+
return std::make_shared<Decryptor>(std::move(aes_decryptor), column_key, file_aad_, aad,
201+
pool_);
218202
}
219203

220204
namespace {

‎cpp/src/parquet/encryption/internal_file_decryptor.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include <map>
2121
#include <memory>
22+
#include <mutex>
2223
#include <string>
2324
#include <vector>
2425

@@ -91,15 +92,15 @@ class InternalFileDecryptor {
9192
FileDecryptionProperties* properties_;
9293
// Concatenation of aad_prefix (if exists) and aad_file_unique
9394
std::string file_aad_;
94-
std::map<std::string, std::shared_ptr<Decryptor>> column_data_map_;
95-
std::map<std::string, std::shared_ptr<Decryptor>> column_metadata_map_;
9695

9796
std::shared_ptr<Decryptor> footer_metadata_decryptor_;
9897
std::shared_ptr<Decryptor> footer_data_decryptor_;
9998
ParquetCipher::type algorithm_;
10099
std::string footer_key_metadata_;
100+
// Mutex to guard access to all_decryptors_
101+
mutable std::mutex mutex_;
101102
// A weak reference to all decryptors that need to be wiped out when decryption is
102-
// finished
103+
// finished, guarded by mutex_ for thread safety
103104
std::vector<std::weak_ptr<encryption::AesDecryptor>> all_decryptors_;
104105

105106
::arrow::MemoryPool* pool_;

0 commit comments

Comments
 (0)