This is an automated email from the ASF dual-hosted git repository.
marin-ma pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 933315982d [GLUTEN-13225][VL] Fix use-after-free of memory pool when
dumping inputs for micro benchmark (#13227)
933315982d is described below
commit 933315982dc3f883acf890c77414956795c45636
Author: Rong Ma <[email protected]>
AuthorDate: Fri Oct 9 17:22:58 2026 +0100
[GLUTEN-13225][VL] Fix use-after-free of memory pool when dumping inputs
for micro benchmark (#13227)
---
cpp/velox/compute/VeloxRuntime.cc | 5 +-
cpp/velox/tests/CMakeLists.txt | 2 +
cpp/velox/tests/VeloxWholeStageDumperTest.cc | 136 +++++++++++++++++++++++++++
cpp/velox/utils/VeloxWholeStageDumper.cc | 18 ++--
cpp/velox/utils/VeloxWholeStageDumper.h | 7 +-
5 files changed, 155 insertions(+), 13 deletions(-)
diff --git a/cpp/velox/compute/VeloxRuntime.cc
b/cpp/velox/compute/VeloxRuntime.cc
index 45649c05f8..5c4621a209 100644
--- a/cpp/velox/compute/VeloxRuntime.cc
+++ b/cpp/velox/compute/VeloxRuntime.cc
@@ -637,10 +637,7 @@ void VeloxRuntime::enableDumping() {
GLUTEN_CHECK(taskInfo.has_value(), "Task info is not set. Please set task
info before enabling dumping.");
dumper_ = std::make_shared<VeloxWholeStageDumper>(
- taskInfo.value(),
- saveDir.value(),
- veloxCfg_->get<int64_t>(kSparkBatchSize, 4096),
- memoryManager()->getAggregateMemoryPool().get());
+ taskInfo.value(), saveDir.value(),
veloxCfg_->get<int64_t>(kSparkBatchSize, 4096), memoryManager());
dumper_->dumpConf(getConfMap());
}
diff --git a/cpp/velox/tests/CMakeLists.txt b/cpp/velox/tests/CMakeLists.txt
index b3b368703a..6a10641fbf 100644
--- a/cpp/velox/tests/CMakeLists.txt
+++ b/cpp/velox/tests/CMakeLists.txt
@@ -142,6 +142,8 @@ add_velox_test(spark_functions_test SOURCES
SparkFunctionTest.cc
add_velox_test(runtime_test SOURCES RuntimeTest.cc)
add_velox_test(velox_memory_test SOURCES MemoryManagerTest.cc)
add_velox_test(buffer_outputstream_test SOURCES BufferOutputStreamTest.cc)
+add_velox_test(velox_whole_stage_dumper_test SOURCES
+ VeloxWholeStageDumperTest.cc)
if(ENABLE_S3)
add_velox_test(gluten_s3_file_system_test SOURCES GlutenS3FileSystemTest.cc)
endif()
diff --git a/cpp/velox/tests/VeloxWholeStageDumperTest.cc
b/cpp/velox/tests/VeloxWholeStageDumperTest.cc
new file mode 100644
index 0000000000..1ecb5f376a
--- /dev/null
+++ b/cpp/velox/tests/VeloxWholeStageDumperTest.cc
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/VeloxWholeStageDumper.h"
+#include "compute/VeloxBackend.h"
+#include "memory/VeloxColumnarBatch.h"
+#include "memory/VeloxMemoryManager.h"
+#include "velox/vector/tests/utils/VectorTestBase.h"
+
+#include <gtest/gtest.h>
+
+#include <filesystem>
+
+using namespace facebook::velox;
+
+namespace gluten {
+namespace {
+
+// A ColumnarBatchIterator that serves a fixed list of row vectors.
+class RowVectorBatchIterator final : public ColumnarBatchIterator {
+ public:
+ explicit RowVectorBatchIterator(std::vector<RowVectorPtr> vectors) :
vectors_(std::move(vectors)) {}
+
+ std::shared_ptr<ColumnarBatch> next() override {
+ if (idx_ >= vectors_.size()) {
+ return nullptr;
+ }
+ return std::make_shared<VeloxColumnarBatch>(vectors_[idx_++]);
+ }
+
+ private:
+ std::vector<RowVectorPtr> vectors_;
+ size_t idx_{0};
+};
+
+} // namespace
+
+class VeloxWholeStageDumperTest : public ::testing::Test, public
test::VectorTestBase {
+ protected:
+ static void SetUpTestSuite() {
+ VeloxBackend::create(AllocationListener::noop(), {});
+
memory::MemoryManager::testingSetInstance(memory::MemoryManager::Options{});
+ }
+
+ static void TearDownTestSuite() {
+ VeloxBackend::get()->tearDown();
+ }
+
+ void SetUp() override {
+ vmm_ = std::make_unique<VeloxMemoryManager>(
+ kVeloxBackendKind, AllocationListener::noop(),
*VeloxBackend::get()->getBackendConf());
+ saveDir_ = std::filesystem::path(::testing::TempDir()) /
+ fmt::format("gluten-whole-stage-dumper-test-{}",
::testing::UnitTest::GetInstance()->random_seed());
+ std::filesystem::remove_all(saveDir_);
+ }
+
+ void TearDown() override {
+ vmm_.reset();
+ std::filesystem::remove_all(saveDir_);
+ }
+
+ std::unique_ptr<VeloxMemoryManager> vmm_;
+ std::filesystem::path saveDir_;
+};
+
+// Regression test: batches read back from the dumped file must stay valid
after the reader iterator is gone.
+// The reader iterator is dropped by the value stream as soon as the input is
exhausted, while vectors it produced
+// may still be referenced by downstream operators or by batches handed to
Java. If they were allocated from a pool
+// owned by the reader, freeing them later would hit a destroyed pool and
crash in AlignedBuffer::freeToPool.
+TEST_F(VeloxWholeStageDumperTest, dumpedBatchesOutliveReaderIterator) {
+ constexpr int32_t kNumBatches = 3;
+ constexpr vector_size_t kRowsPerBatch = 1000;
+
+ std::vector<RowVectorPtr> inputs;
+ for (int32_t i = 0; i < kNumBatches; ++i) {
+ inputs.push_back(makeRowVector(
+ {"c0", "c1", "c2"},
+ {makeFlatVector<int64_t>(kRowsPerBatch, [i](auto row) { return i *
kRowsPerBatch + row; }),
+ makeFlatVector<double>(kRowsPerBatch, [](auto row) { return row *
0.5; }),
+ makeFlatVector<StringView>(kRowsPerBatch, [](auto row) {
+ return row % 2 == 0 ? StringView("short") : StringView("a string
that is not inlined");
+ })}));
+ }
+ auto input = std::make_shared<RowVectorBatchIterator>(inputs);
+
+ const SparkTaskInfo taskInfo{/*stageId=*/1, /*partitionId=*/2, /*taskId=*/3,
/*vId=*/4};
+ VeloxWholeStageDumper dumper(taskInfo, saveDir_.string(), kRowsPerBatch,
vmm_.get());
+
+ // Capture the baseline before the reader exists: the reader allocates its
own state from the leaf pool as soon
+ // as it is created, and all of it must be gone by the end of the test.
+ const auto leafPool = vmm_->getLeafMemoryPool();
+ const auto baselineBytes = leafPool->usedBytes();
+
+ auto reader = dumper.dumpInputIterator(0, input);
+ ASSERT_NE(reader, nullptr);
+ ASSERT_TRUE(std::filesystem::exists(saveDir_ / "data_1_2_4_0.parquet"));
+
+ std::vector<std::shared_ptr<ColumnarBatch>> batches;
+ int64_t numRows = 0;
+ while (auto cb = reader->next()) {
+ numRows += cb->numRows();
+ // Vectors must be backed by the Runtime-lifetime leaf pool rather than a
pool owned by the reader.
+ ASSERT_EQ(VeloxColumnarBatch::from(leafPool.get(),
cb)->getRowVector()->pool(), leafPool.get());
+ batches.push_back(std::move(cb));
+ }
+ ASSERT_EQ(numRows, kNumBatches * kRowsPerBatch);
+ ASSERT_GT(leafPool->usedBytes(), baselineBytes);
+
+ // Destroying the reader frees the reader's own buffers, but must not
destroy the pool backing the batches,
+ // so the memory held by the batches is still accounted for.
+ reader.reset();
+ const auto bytesHeldByBatches = leafPool->usedBytes();
+ ASSERT_GT(bytesHeldByBatches, baselineBytes);
+
+ // Releasing the batches after the reader is gone must be safe,
+ // and must give the memory back to the pool, which is still alive.
+ batches.clear();
+ ASSERT_LT(leafPool->usedBytes(), bytesHeldByBatches);
+ ASSERT_EQ(leafPool->usedBytes(), baselineBytes);
+}
+
+} // namespace gluten
diff --git a/cpp/velox/utils/VeloxWholeStageDumper.cc
b/cpp/velox/utils/VeloxWholeStageDumper.cc
index d8a451dac7..3f0bb556f8 100644
--- a/cpp/velox/utils/VeloxWholeStageDumper.cc
+++ b/cpp/velox/utils/VeloxWholeStageDumper.cc
@@ -58,8 +58,8 @@ VeloxWholeStageDumper::VeloxWholeStageDumper(
const SparkTaskInfo& taskInfo,
const std::string& saveDir,
int64_t batchSize,
- facebook::velox::memory::MemoryPool* aggregatePool)
- : taskInfo_(taskInfo), saveDir_(saveDir), batchSize_(batchSize),
pool_(aggregatePool) {}
+ VeloxMemoryManager* memoryManager)
+ : taskInfo_(taskInfo), saveDir_(saveDir), batchSize_(batchSize),
memoryManager_(memoryManager) {}
void VeloxWholeStageDumper::dumpConf(const std::unordered_map<std::string,
std::string>& confMap) {
const auto& backendConfMap =
VeloxBackend::get()->getBackendConf()->rawConfigs();
@@ -113,18 +113,22 @@ std::shared_ptr<ColumnarBatchIterator>
VeloxWholeStageDumper::dumpInputIterator(
fmt::format("data_{}_{}_{}_{}.parquet", taskInfo_.stageId,
taskInfo_.partitionId, taskInfo_.vId, iteratorIndex);
const auto dumpPath = checkAndGetDumpPath(saveDir_, fileName);
- // Velox parquet writer requires aggregate memory pool.
+ // Velox parquet writer requires aggregate memory pool. The writer is
short-lived so a dedicated child pool is fine.
auto writer = std::make_shared<VeloxColumnarBatchWriter>(
- dumpPath, batchSize_,
pool_->addAggregateChild(fmt::format("dump_iterator.{}", iteratorIndex)));
+ dumpPath,
+ batchSize_,
+
memoryManager_->getAggregateMemoryPool()->addAggregateChild(fmt::format("dump_iterator.{}",
iteratorIndex)));
while (auto cb = inputIterator->next()) {
GLUTEN_THROW_NOT_OK(writer->write(cb));
}
GLUTEN_THROW_NOT_OK(writer->close());
- // Velox parquet reader requires leaf memory pool.
- return std::make_shared<ParquetStreamReaderIterator>(
- dumpPath, batchSize_,
pool_->addLeafChild(fmt::format("retrieve_iterator.{}", iteratorIndex)));
+ // Velox parquet reader requires leaf memory pool. Do not create a dedicated
child pool here: the reader
+ // iterator is destroyed as soon as the input stream is exhausted, while
vectors it produced may still be
+ // referenced by downstream operators or by batches handed to Java. Freeing
them against a destroyed pool
+ // crashes in AlignedBuffer::freeToPool, so use the Runtime's leaf pool
which outlives all of them.
+ return std::make_shared<ParquetStreamReaderIterator>(dumpPath, batchSize_,
memoryManager_->getLeafMemoryPool());
}
} // namespace gluten
diff --git a/cpp/velox/utils/VeloxWholeStageDumper.h
b/cpp/velox/utils/VeloxWholeStageDumper.h
index dc6c9da90a..9327f40474 100644
--- a/cpp/velox/utils/VeloxWholeStageDumper.h
+++ b/cpp/velox/utils/VeloxWholeStageDumper.h
@@ -18,6 +18,7 @@
#pragma once
#include "compute/VeloxRuntime.h"
+#include "memory/VeloxMemoryManager.h"
#include "utils/WholeStageDumper.h"
namespace gluten {
@@ -28,7 +29,7 @@ class VeloxWholeStageDumper final : public WholeStageDumper {
const SparkTaskInfo& taskInfo,
const std::string& saveDir,
int64_t batchSize,
- facebook::velox::memory::MemoryPool* aggregatePool);
+ VeloxMemoryManager* memoryManager);
void dumpConf(const std::unordered_map<std::string, std::string>& confMap)
override;
@@ -45,7 +46,9 @@ class VeloxWholeStageDumper final : public WholeStageDumper {
std::string saveDir_;
int64_t batchSize_;
- facebook::velox::memory::MemoryPool* pool_;
+ // Owned by the Runtime / JNI wrapper and released after the Runtime, so it
outlives the dumper and
+ // every iterator / batch created by the Runtime.
+ VeloxMemoryManager* memoryManager_;
};
} // namespace gluten
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]