Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CI][C++] test-ubuntu-20.04-cpp fails sometimes with a segmentation fault on arrow-dataset-file-parquet-encryption-test #43057

Open
raulcd opened this issue Jun 26, 2024 · 20 comments

Comments

@raulcd
Copy link
Member

raulcd commented Jun 26, 2024

Describe the bug, including details regarding any error messages, version, and platform.

test-ubuntu-20.04-cpp seems to fail sometimes with a segmentation fault on arrow-dataset-file-parquet-encryption-test:

66/96 Test #69: arrow-dataset-file-parquet-encryption-test ...***Failed    0.34 sec
Running arrow-dataset-file-parquet-encryption-test, redirecting output into /build/cpp/build/test-logs/arrow-dataset-file-parquet-encryption-test.txt (attempt 1/1)
/arrow/cpp/build-support/run-test.sh: line 88: 41443 Segmentation fault      (core dumped) $TEST_EXECUTABLE "$@" > $LOGFILE.raw 2>&1
Running main() from _deps/googletest-src/googletest/src/gtest_main.cc

It has also failed on PRs: https://github.com/ursacomputing/crossbow/actions/runs/9661748529/job/26650110979
On retry it succeeds sometimes: https://github.com/ursacomputing/crossbow/actions/runs/9671504639

Component(s)

C++, Continuous Integration

@raulcd raulcd changed the title [CI][C++] test-ubuntu-20.04-cpp fails sometimes with a segmentation faul on arrow-dataset-file-parquet-encryption-test [CI][C++] test-ubuntu-20.04-cpp fails sometimes with a segmentation fault on arrow-dataset-file-parquet-encryption-test Jun 26, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
pitrou added a commit to pitrou/arrow that referenced this issue Sep 3, 2024
@pitrou
Copy link
Member

pitrou commented Sep 4, 2024

I finally got a backtrace from https://github.com/ursacomputing/crossbow/actions/runs/10686494834/job/29654051500:

#0  0x00007faf45e66121 in EVP_CipherInit_ex () from /lib/x86_64-linux-gnu/libcrypto.so.1.1
#1  0x00007faf4c4bde73 in parquet::encryption::AesDecryptor::AesDecryptorImpl::GcmDecrypt ()
    at /arrow/cpp/src/parquet/encryption/encryption_internal.cc:616
#2  0x00007faf4c4beaf0 in parquet::encryption::AesDecryptor::AesDecryptorImpl::Decrypt ()
    at /arrow/cpp/src/parquet/encryption/encryption_internal.cc:724
#3  0x00007faf4c4bc97f in parquet::encryption::AesDecryptor::Decrypt () at /arrow/cpp/src/parquet/encryption/encryption_internal.cc:435
#4  0x00007faf4c391672 in parquet::Decryptor::Decrypt () at /arrow/cpp/src/parquet/encryption/internal_file_decryptor.cc:46
#5  0x00007faf4c25f053 in parquet::ThriftDeserializer::DeserializeMessage<parquet::format::PageHeader> ()
    at /arrow/cpp/src/parquet/thrift_internal.h:424
#6  0x00007faf4c20ae29 in NextPage () at /arrow/cpp/src/parquet/column_reader.cc:453
#7  0x00007faf4c2476ee in ReadNewPage () at /arrow/cpp/src/parquet/column_reader.cc:719
#8  0x00007faf4c23addd in HasNextInternal () at /arrow/cpp/src/parquet/column_reader.cc:699
#9  0x00007faf4c22c2dc in ReadRecords () at /arrow/cpp/src/parquet/column_reader.cc:1402
#10 0x00007faf4c112c74 in LoadBatch () at /arrow/cpp/src/parquet/arrow/reader.cc:482
#11 0x00007faf4c12bf07 in parquet::arrow::ColumnReaderImpl::NextBatch () at /arrow/cpp/src/parquet/arrow/reader.cc:109
#12 0x00007faf4c1113b3 in ReadColumn () at /arrow/cpp/src/parquet/arrow/reader.cc:284
#13 0x00007faf4c11885b in operator() () at /arrow/cpp/src/parquet/arrow/reader.cc:1262
#14 0x00007faf4c11c2f7 in OptionalParallelForAsync<parquet::arrow::(anonymous namespace)::FileReaderImpl::DecodeRowGroups(std::shared_ptr<parquet::ar
row::(anonymous namespace)::FileReaderImpl>, const std::vector<int>&, const std::vector<int>&, arrow::internal::Executor*)::<lambda(size_t, std::shar
ed_ptr<parquet::arrow::ColumnReaderImpl>)>&, std::shared_ptr<parquet::arrow::ColumnReaderImpl> >(void) () at /arrow/cpp/src/arrow/util/parallel.h:95
#15 0x00007faf4c118f8f in DecodeRowGroups () at /arrow/cpp/src/parquet/arrow/reader.cc:1280
#16 0x00007faf4c117b92 in ReadOneRowGroup () at /arrow/cpp/src/parquet/arrow/reader.cc:1166
#17 0x00007faf4c12d8cc in parquet::arrow::RowGroupGenerator::FetchNext()::{lambda()#1}::operator()() const ()
    at /arrow/cpp/src/parquet/arrow/reader.cc:1139
[...]

@pitrou
Copy link
Member

pitrou commented Sep 4, 2024

Disassembly of crashing code:

Dump of assembler code for function EVP_CipherInit_ex:
   0x00007faf45e660f0 <+0>:     endbr64 
   0x00007faf45e660f4 <+4>:     push   %r15
   0x00007faf45e660f6 <+6>:     mov    %rdx,%r15
   0x00007faf45e660f9 <+9>:     push   %r14
   0x00007faf45e660fb <+11>:    push   %r13
   0x00007faf45e660fd <+13>:    mov    %r8,%r13
   0x00007faf45e66100 <+16>:    push   %r12
   0x00007faf45e66102 <+18>:    mov    %rcx,%r12
   0x00007faf45e66105 <+21>:    push   %rbp
   0x00007faf45e66106 <+22>:    mov    %rsi,%rbp
   0x00007faf45e66109 <+25>:    push   %rbx
   0x00007faf45e6610a <+26>:    mov    %rdi,%rbx
   0x00007faf45e6610d <+29>:    sub    $0x18,%rsp
   0x00007faf45e66111 <+33>:    cmp    $0xffffffff,%r9d
   0x00007faf45e66115 <+37>:    je     0x7faf45e662b0 <EVP_CipherInit_ex+448>
   0x00007faf45e6611b <+43>:    xor    %r14d,%r14d
   0x00007faf45e6611e <+46>:    test   %r9d,%r9d
=> 0x00007faf45e66121 <+49>:    mov    (%rbx),%rax

The %rbx register contains a null pointer:

(gdb) info registers 
rax            0x0                 0
rbx            0x0                 0
rcx            0x7faf18006610      140389998683664
rdx            0x0                 0
rsi            0x0                 0
rdi            0x0                 0
rbp            0x0                 0x0
rsp            0x7faf37ffc310      0x7faf37ffc310
r8             0x7faf37ffc3b4      140390535513012

Its value comes from %rdi, which according to https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI is the first integer/pointer argument to EVP_CipherInit_ex, that is, the EVP_CIPHER_CTX * context pointer.

This probably means that, at this point, the AesDecryptor instance was either destroyed or deinitialized using WipeOut.

@pitrou
Copy link
Member

pitrou commented Sep 4, 2024

Also, the backtrace in the main thread looks like this. Other threads are sleeping:

#0  0x00007faf45ac3170 in __lll_lock_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
#1  0x00007faf45abb0a3 in pthread_mutex_lock () from /lib/x86_64-linux-gnu/libpthread.so.0
#2  0x00007faf48720685 in __gthread_mutex_lock () at /usr/include/x86_64-linux-gnu/c++/9/bits/gthr-default.h:749
#3  0x00007faf48720bb4 in std::mutex::lock () at /usr/include/c++/9/bits/std_mutex.h:100
#4  0x00007faf4a304979 in arrow::util::Mutex::Lock () at /arrow/cpp/src/arrow/util/mutex.cc:56
#5  0x00007faf4cd6ad03 in arrow::MergedGenerator<arrow::dataset::EnumeratedRecordBatch>::OuterCallback::operator()(arrow::Result<std::function<arrow::Future<
arrow::dataset::EnumeratedRecordBatch> ()> > const&) () at /arrow/cpp/src/arrow/util/async_generator.h:1371
#6  0x00007faf4cd6804d in arrow::Future<std::function<arrow::Future<arrow::dataset::EnumeratedRecordBatch> ()> >::WrapResultOnComplete::Callback<arrow::Merge
dGenerator<arrow::dataset::EnumeratedRecordBatch>::OuterCallback>::operator()(arrow::FutureImpl const&) && () at /arrow/cpp/src/arrow/util/future.h:442
#7  0x00007faf4cd66493 in arrow::internal::FnOnce<void (arrow::FutureImpl const&)>::FnImpl<arrow::Future<std::function<arrow::Future<arrow::dataset::Enumerat
edRecordBatch> ()> >::WrapResultOnComplete::Callback<arrow::MergedGenerator<arrow::dataset::EnumeratedRecordBatch>::OuterCallback> >::invoke(arrow::FutureImp
l const&) () at /arrow/cpp/src/arrow/util/functional.h:152
#8  0x00007faf4a2bd3d2 in arrow::internal::FnOnce<void (arrow::FutureImpl const&)>::operator()(arrow::FutureImpl const&) && ()
    at /arrow/cpp/src/arrow/util/functional.h:140
#9  0x00007faf4a2bc35d in arrow::ConcreteFutureImpl::AddCallback(arrow::internal::FnOnce<void (arrow::FutureImpl const&)>, arrow::CallbackOptions)::{lambda(a
rrow::FutureImpl const&)#1}::operator()(arrow::FutureImpl const&) () at /arrow/cpp/src/arrow/util/future.cc:58
#10 0x00007faf4a2c2d0d in arrow::internal::FnOnce<void (arrow::FutureImpl const&)>::FnImpl<arrow::ConcreteFutureImpl::AddCallback(arrow::internal::FnOnce<voi
d (arrow::FutureImpl const&)>, arrow::CallbackOptions)::{lambda(arrow::FutureImpl const&)#1}>::invoke(arrow::FutureImpl const&) ()
    at /arrow/cpp/src/arrow/util/functional.h:152
#11 0x00007faf4a2bd3d2 in arrow::internal::FnOnce<void (arrow::FutureImpl const&)>::operator()(arrow::FutureImpl const&) && ()
    at /arrow/cpp/src/arrow/util/functional.h:140
#12 0x00007faf4a2bcc0c in arrow::ConcreteFutureImpl::RunOrScheduleCallback () at /arrow/cpp/src/arrow/util/future.cc:110
#13 0x00007faf4a2bc60a in arrow::ConcreteFutureImpl::AddCallback(arrow::internal::FnOnce<void (arrow::FutureImpl const&)>, arrow::CallbackOptions) ()
    at /arrow/cpp/src/arrow/util/future.cc:64
#14 0x00007faf4a2ba36a in arrow::FutureImpl::AddCallback(arrow::internal::FnOnce<void (arrow::FutureImpl const&)>, arrow::CallbackOptions) ()
    at /arrow/cpp/src/arrow/util/future.cc:229
#15 0x00007faf4cd35839 in arrow::Future<std::function<arrow::Future<arrow::dataset::EnumeratedRecordBatch> ()> >::AddCallback<arrow::MergedGenerator<arrow::d
ataset::EnumeratedRecordBatch>::OuterCallback, arrow::Future<std::function<arrow::Future<arrow::dataset::EnumeratedRecordBatch> ()> >::WrapResultOnComplete::
Callback<arrow::MergedGenerator<arrow::dataset::EnumeratedRecordBatch>::OuterCallback> >(arrow::MergedGenerator<arrow::dataset::EnumeratedRecordBatch>::Outer
Callback, arrow::CallbackOptions) const () at /arrow/cpp/src/arrow/util/future.h:493
#16 0x00007faf4cd2bee8 in arrow::MergedGenerator<arrow::dataset::EnumeratedRecordBatch>::operator() () at /arrow/cpp/src/arrow/util/async_generator.h:1098
#17 0x00007faf4cd212de in std::_Function_handler<arrow::Future<arrow::dataset::EnumeratedRecordBatch> (), arrow::MergedGenerator<arrow::dataset::EnumeratedRe
cordBatch> >::_M_invoke(std::_Any_data const&) () at /usr/include/c++/9/bits/std_function.h:286
#18 0x00007faf4cd059cc in std::function<arrow::Future<arrow::dataset::EnumeratedRecordBatch> ()>::operator()() const ()
    at /usr/include/c++/9/bits/std_function.h:688
#19 0x00007faf4cd2c4ba in arrow::ReadaheadGenerator<arrow::dataset::EnumeratedRecordBatch>::operator() () at /arrow/cpp/src/arrow/util/async_generator.h:777
#20 0x00007faf4cd21530 in std::_Function_handler<arrow::Future<arrow::dataset::EnumeratedRecordBatch> (), arrow::ReadaheadGenerator<arrow::dataset::Enumerate
dRecordBatch> >::_M_invoke(std::_Any_data const&) () at /usr/include/c++/9/bits/std_function.h:286
#21 0x00007faf4cd059cc in std::function<arrow::Future<arrow::dataset::EnumeratedRecordBatch> ()>::operator()() const ()
    at /usr/include/c++/9/bits/std_function.h:688
#22 0x00007faf4cd2cdae in arrow::MappingGenerator<arrow::dataset::EnumeratedRecordBatch, std::optional<arrow::compute::ExecBatch> >::operator() ()
    at /arrow/cpp/src/arrow/util/async_generator.h:163
#23 0x00007faf4cd21c61 in std::_Function_handler<arrow::Future<std::optional<arrow::compute::ExecBatch> > (), arrow::MappingGenerator<arrow::dataset::Enumera
tedRecordBatch, std::optional<arrow::compute::ExecBatch> > >::_M_invoke(std::_Any_data const&) () at /usr/include/c++/9/bits/std_function.h:286
#24 0x0000563c62c625f6 in std::function<arrow::Future<std::optional<arrow::compute::ExecBatch> > ()>::operator()() const ()
    at /usr/include/c++/9/bits/std_function.h:688
#25 0x00007faf4b4687cb in operator() () at /arrow/cpp/src/arrow/acero/source_node.cc:201
#26 0x00007faf4b46c72f in Loop<arrow::acero::(anonymous namespace)::SourceNode::StartProducing()::<lambda()> >(void) ()
    at /arrow/cpp/src/arrow/util/future.h:852
#27 0x00007faf4b468d73 in StartProducing () at /arrow/cpp/src/arrow/acero/source_node.cc:219
#28 0x00007faf4b380e1f in operator() () at /arrow/cpp/src/arrow/acero/exec_plan.cc:175
#29 0x00007faf4b3951e3 in invoke () at /arrow/cpp/src/arrow/util/functional.h:152
#30 0x00007faf4a24ac09 in arrow::internal::FnOnce<arrow::Status (arrow::util::AsyncTaskScheduler*)>::operator()(arrow::util::AsyncTaskScheduler*) && ()
    at /arrow/cpp/src/arrow/util/functional.h:140
#31 0x00007faf4a2414cb in arrow::util::AsyncTaskScheduler::Make(arrow::internal::FnOnce<arrow::Status (arrow::util::AsyncTaskScheduler*)>, arrow::internal::F
nOnce<void (arrow::Status const&)>, arrow::StopToken) () at /arrow/cpp/src/arrow/util/async_util.cc:471
#32 0x00007faf4b3815a8 in StartProducing () at /arrow/cpp/src/arrow/acero/exec_plan.cc:193
#33 0x00007faf4b38332a in arrow::acero::ExecPlan::StartProducing () at /arrow/cpp/src/arrow/acero/exec_plan.cc:439
#34 0x00007faf4cce1ae6 in ScanBatchesUnorderedAsync () at /arrow/cpp/src/arrow/dataset/scanner.cc:460
#35 0x00007faf4cce4b0e in ToTableAsync () at /arrow/cpp/src/arrow/dataset/scanner.cc:703
#36 0x00007faf4cce0817 in operator() () at /arrow/cpp/src/arrow/dataset/scanner.cc:398
#37 0x00007faf4ccfaf57 in invoke () at /arrow/cpp/src/arrow/util/functional.h:152
#38 0x00007faf4cd108d1 in arrow::internal::FnOnce<arrow::Future<std::shared_ptr<arrow::Table> > (arrow::internal::Executor*)>::operator()(arrow::internal::Ex
ecutor*) && () at /arrow/cpp/src/arrow/util/functional.h:140
#39 0x00007faf4cd1d0b2 in arrow::internal::SerialExecutor::Run<std::shared_ptr<arrow::Table>, arrow::Result<std::shared_ptr<arrow::Table> > >(arrow::internal
::FnOnce<arrow::Future<std::shared_ptr<arrow::Table> > (arrow::internal::Executor*)>) () at /arrow/cpp/src/arrow/util/thread_pool.h:418
#40 0x00007faf4cd10a13 in arrow::internal::SerialExecutor::RunInSerialExecutor<std::shared_ptr<arrow::Table>, arrow::Future<std::shared_ptr<arrow::Table> >, 
arrow::Result<std::shared_ptr<arrow::Table> > >(arrow::internal::FnOnce<arrow::Future<std::shared_ptr<arrow::Table> > (arrow::internal::Executor*)>) ()
    at /arrow/cpp/src/arrow/util/thread_pool.h:300
#41 0x00007faf4cd04eaa in arrow::internal::RunSynchronously<arrow::Future<std::shared_ptr<arrow::Table> >, std::shared_ptr<arrow::Table> >(arrow::internal::F
nOnce<arrow::Future<std::shared_ptr<arrow::Table> > (arrow::internal::Executor*)>, bool) () at /arrow/cpp/src/arrow/util/thread_pool.h:590
#42 0x00007faf4cce0898 in ToTable () at /arrow/cpp/src/arrow/dataset/scanner.cc:399
#43 0x0000563c62cb7fca in arrow::dataset::DatasetEncryptionTestBase::TestScanDataset () at /arrow/cpp/src/arrow/dataset/file_parquet_encryption_test.cc:159
#44 0x0000563c62cb2006 in arrow::dataset::DatasetEncryptionTest_WriteReadDatasetWithEncryption_Test::TestBody ()
    at /arrow/cpp/src/arrow/dataset/file_parquet_encryption_test.cc:208
#45 0x00007faf4b6915e2 in testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void> ()
    at _deps/googletest-src/googletest/src/gtest.cc:2607
#46 0x00007faf4b689377 in testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void> ()
    at _deps/googletest-src/googletest/src/gtest.cc:2643
#47 0x00007faf4b65c7ae in testing::Test::Run () at _deps/googletest-src/googletest/src/gtest.cc:2682
#48 0x00007faf4b65d1f0 in testing::TestInfo::Run () at _deps/googletest-src/googletest/src/gtest.cc:2861
#49 0x00007faf4b65db0a in testing::TestSuite::Run () at _deps/googletest-src/googletest/src/gtest.cc:3015
#50 0x00007faf4b66d5c2 in testing::internal::UnitTestImpl::RunAllTests () at _deps/googletest-src/googletest/src/gtest.cc:5855
#51 0x00007faf4b6926a3 in testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool> ()
    at _deps/googletest-src/googletest/src/gtest.cc:2607
#52 0x00007faf4b68a63d in testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool> ()
    at _deps/googletest-src/googletest/src/gtest.cc:2643
#53 0x00007faf4b66bcd9 in testing::UnitTest::Run () at _deps/googletest-src/googletest/src/gtest.cc:5438
#54 0x00007faf4b6c5b2a in RUN_ALL_TESTS () at _deps/googletest-src/googletest/include/gtest/gtest.h:2490
#55 0x00007faf4b6c5aac in main () at _deps/googletest-src/googletest/src/gtest_main.cc:52
#56 0x00007faf45ff4083 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#57 0x0000563c62c4907e in _start ()

pitrou added a commit that referenced this issue Sep 5, 2024
…ptor (#43947)

This is to get a clearer error rather than an obscure crash, see #43057 for an example.

* GitHub Issue: #43946

Authored-by: Antoine Pitrou <antoine@python.org>
Signed-off-by: Antoine Pitrou <antoine@python.org>
zanmato1984 pushed a commit to zanmato1984/arrow that referenced this issue Sep 6, 2024
…/encryptor (apache#43947)

This is to get a clearer error rather than an obscure crash, see apache#43057 for an example.

* GitHub Issue: apache#43946

Authored-by: Antoine Pitrou <antoine@python.org>
Signed-off-by: Antoine Pitrou <antoine@python.org>
khwilson pushed a commit to khwilson/arrow that referenced this issue Sep 14, 2024
…/encryptor (apache#43947)

This is to get a clearer error rather than an obscure crash, see apache#43057 for an example.

* GitHub Issue: apache#43946

Authored-by: Antoine Pitrou <antoine@python.org>
Signed-off-by: Antoine Pitrou <antoine@python.org>
@EnricoMi
Copy link
Contributor

I managed to make DatasetEncryptionTest.WriteReadDatasetWithEncryption test case consistently fail by scanning the dataset concurrently with many threads: https://github.com/EnricoMi/arrow/pull/7/files#diff-807e200a6a36f05141d665f692de94e440d62f850a67ca186a2bf06ef0c09177R231-R233

This reveals two issues:

  1. The cipher context is wiped out (probably through AesDecryptor::WipeOut and not the deconstructor) while later being used. Reconstructing the context for every use seems to fix the concurrent wipe outs: https://github.com/EnricoMi/arrow/pull/7/files#diff-10f9496a81a85a38fca5553486a667a49836574479f6b956839a9ae80d559167R637
    It still looks like this is not a matter of wipe out and later use of the context, but of concurrent wipe out and usage of the context, so that the recreation has to be guarded: https://github.com/EnricoMi/arrow/pull/7/files#diff-10f9496a81a85a38fca5553486a667a49836574479f6b956839a9ae80d559167R431
  2. Once a cipher context is always available, the EVP methods exhibit their missing thread-safeness. Using one context for the same key concurrently is not supported: https://docs.openssl.org/master/man7/openssl-threads/

Adding the reconstruction fixes 1., making the cipher context a thread local variable fixes 2. However, I think this should be fixed be rethinking the design.

Why are the cipher contexts wiped out in the first place? Are this security risk concerns or is this to reduce memory footprint?

I haven't yet found the cause of multiple threads getting hold of the same AesDecryptorImpl instance. It'd be great to get some pointers to the mechanics of that.

@EnricoMi
Copy link
Contributor

@pitrou in #43947 (comment) you mentioned the test scans the dataset multi-threaded. Can you point me to the bit of the stacktrace that implies that? I think SerialExecutor::RunInSerialExecutor above indicates the opposite. And the default ScanOptions.use_threads is false.

@pitrou
Copy link
Member

pitrou commented Nov 20, 2024

Can you point me to the bit of the stacktrace that implies that? I think SerialExecutor::RunInSerialExecutor above indicates the opposite. And the default ScanOptions.use_threads is false.

That was just an intuition, so I might be wrong indeed.

@pitrou
Copy link
Member

pitrou commented Nov 20, 2024

Why are the cipher contexts wiped out in the first place? Are this security risk concerns or is this to reduce memory footprint?

I am not the original author, but I suppose this is for security concerns.

@pitrou
Copy link
Member

pitrou commented Nov 20, 2024

I haven't yet found the cause of multiple threads getting hold of the same AesDecryptorImpl instance. It'd be great to get some pointers to the mechanics of that.

I think this happens in multiple places.

At the top-most level (the Parquet file reader), a single CryptoContext is created par column:

CryptoContext ctx(col->has_dictionary_page(), row_group_ordinal_,
static_cast<int16_t>(i), meta_decryptor, data_decryptor);

This CryptoContext has a single data decryptor for the entire column, even though different pages or row groups in the column may be read from different threads at once.

Worse, most encrypted Parquet files will use the same (footer) key for decrypting all columns. This means all CryptoContext instances for the columns of a given file will hold the same data decryptor:

if (crypto_metadata->encrypted_with_footer_key()) {
return metadata ? file_decryptor->GetFooterDecryptorForColumnMeta()
: file_decryptor->GetFooterDecryptorForColumnData();
}

As you point out, the decryptors should be created on an adhoc basic for each decryption operation. Hopefully this is a fast operation?

@pitrou
Copy link
Member

pitrou commented Nov 20, 2024

cc @rok

@EnricoMi
Copy link
Contributor

@andersonm-ibm in #12778 you mentioned that multi-threaded read requires an AesDecryptorImpl per column, but a column can be read by multiple threads in parallel, right?

@EnricoMi
Copy link
Contributor

@thamht4190 can you shed some light on this?

@adamreeve
Copy link
Contributor

#39623 might provide some useful context. It looks like we can create multiple AesDecyptorImpl instances that may be used concurrently, and possibly for the same column. So a single multi-threaded scan of a Dataset is thread safe. But this issue seems to be caused by there being an InternalFileDecryptor shared between Dataset scans, which can wipe out all of the decryptors it has created while some if them are still in use.

@pitrou
Copy link
Member

pitrou commented Nov 25, 2024

@adamreeve Do you disagree with the analysis in #43057 (comment) ? There still seems to be some amount of decryptor caching going on, at several levels.

I think one radical solution would be to remove all kinds of caching and create a new decryptor for each encrypted module. We can later run perf tests to see if that really makes things slower.

@adamreeve
Copy link
Contributor

Yes I don't think the analysis in that comment is quite right. The linked code that creates a CryptoContext is in the SerializedRowGroup::GetColumnPageReader method, and this creates a Decryptor by using the shared InternalFileDecryptor:

std::shared_ptr<Decryptor> data_decryptor = GetColumnDataDecryptor(
crypto_metadata.get(), file_metadata_->file_decryptor().get());

It looks like GetColumnPageReader is called per-thread for multi-threaded reads so these decryptors aren't shared, although I haven't debugged or analysed the code closely to confirm that for sure.

#39623 removed the caching of the decryptors within InternalFileDecryptor but they're still added to the all_decryptors_ list, allowing them to be wiped out via InternalFileDecryptor::WipeOutDecryptionKeys.

@pitrou
Copy link
Member

pitrou commented Nov 25, 2024

But GetColumnDataDecryptor will happily reuse the footer_data_decryptor_ for all columns that use footer key encryption...

@pitrou
Copy link
Member

pitrou commented Nov 25, 2024

And besides, even if we were creating a distinct decryptor per column chunk, the caller may still want to parallelize reading at the page level.

@adamreeve
Copy link
Contributor

But GetColumnDataDecryptor will happily reuse the footer_data_decryptor_ for all columns that use footer key encryption...

Hmm yeah that could be a problem.

And besides, even if we were creating a distinct decryptor per column chunk, the caller may still want to parallelize reading at the page level.

It looks like the PageReader API doesn't provide a way to parallelise reading though, you can only iterate over data pages sequentially, so I don't think this is a concern:

// The returned Page may contain references that aren't guaranteed to live
// beyond the next call to NextPage().
virtual std::shared_ptr<Page> NextPage() = 0;

@pitrou
Copy link
Member

pitrou commented Nov 25, 2024

It looks like the PageReader API doesn't provide a way to parallelise reading though, you can only iterate over data pages sequentially, so I don't think this is a concern:

It may not be obvious how to use it with other Parquet C++ APIs, but the OffsetIndex conceptually allows direct access to individual pages. So, ideally at least, and hopefully in the future, it will be possible to access individual data pages from a column in a non-sequential fashion. (cc @wgtmac @mapleFU )

/// \brief OffsetIndex is a proxy around format::OffsetIndex.
class PARQUET_EXPORT OffsetIndex {
public:
/// \brief Create a OffsetIndex from a serialized thrift message.
static std::unique_ptr<OffsetIndex> Make(const void* serialized_index,
uint32_t index_len,
const ReaderProperties& properties,
Decryptor* decryptor = NULLPTR);
virtual ~OffsetIndex() = default;
/// \brief A vector of locations for each data page in this column.
virtual const std::vector<PageLocation>& page_locations() const = 0;
};

@wgtmac
Copy link
Member

wgtmac commented Nov 25, 2024

it will be possible to access individual data pages from a column in a non-sequential fashion.

Yes, I think we can implement a new PageReader which leverages OffsetIndex for page access. The challenge is to optimize the I/O since it is unpredictable.

@adamreeve
Copy link
Contributor

It may not be obvious how to use it with other Parquet C++ APIs, but the OffsetIndex conceptually allows direct access to individual pages.

Ah OK interesting, thanks. If we support this in future by updating PageReader then maybe we'd need to document that PageReader isn't thread-safe, and that users need to create separate PageReader instances per thread? Otherwise I guess we could allow users to create per-thread decryptors and pass them to the PageReader methods. Another option would be using locking within the decryptors, and maybe the overhead of that would be OK if the locks would not usually be contended.

But GetColumnDataDecryptor will happily reuse the footer_data_decryptor_ for all columns that use footer key encryption...

I tested doing a Dataset scan using uniform encryption and this does cause decryptor errors. I thought it's worth creating a separate issue for that as it's not exactly the same problem as this issue, so I've reported this as #44852.

It's probably worth pointing out that specifying the same master key for columns as the footer master key isn't the same as uniform encryption. With uniform encryption the same data encryption key is used, but if you specify the same key name for columns then separate data encryption keys will be generated per column. And the uniform encryption option isn't exposed in PyArrow, so this scenario might not be that common.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants