This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 3f1cbc2b133 [fix](scan) Avoid reading freed operator state in scanner
diagnostics (#69181)
3f1cbc2b133 is described below
commit 3f1cbc2b1331ea7b177391d04617d4f3535d3fdc
Author: Gabriel <[email protected]>
AuthorDate: Fri Oct 16 22:17:21 2026 +0800
[fix](scan) Avoid reading freed operator state in scanner diagnostics
(#69181)
### What problem does this PR solve?
A queued scan task can retain `ScannerContext` after its scan operator
has been destroyed. During adaptive scan cleanup, `debug_string()`
dereferences the operator-owned `_shared_scan_limit`, causing an ASAN
heap-use-after-free. The same diagnostic is also used when the scheduler
cannot lock the task execution context.
Remove the borrowed `remaining_limit` value from diagnostics so both
paths remain safe after the operator is released. Scan limit enforcement
and memory quota cleanup are unchanged. Add unit tests for diagnostics
and scheduler submission after owner destruction, and for adaptive
cleanup with verbose logging enabled.
### Release note
Fix a use-after-free in scan diagnostics during task cleanup.
### Check List (For Author)
- Test
- [x] Unit Test: add two lifecycle regression cases and update the
existing diagnostic-format assertion.
- [x] Manual test: a focused ASAN harness extracting the actual
`debug_string()` method reproduces the invalid read before the fix and
passes all four combinations of adaptive scanning enabled/disabled and
limited/unlimited scans after the fix. The surrounding fields are test
stand-ins; this is not a full BE UT run.
- Validation
- Clang syntax checks passed for the modified source and test
translation units using regenerated protocol headers and BE test flags.
- clang-format 16 and build hygiene checks passed.
- The full BE UT invocation was blocked before execution by the local
third-party installation missing `libsimdutf.a`; the new BE unit tests
still require CI execution.
- Behavior changed:
- [x] Yes: diagnostic output no longer includes `remaining_limit`.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/exec/scan/scanner_context.cpp | 7 ++--
be/test/exec/scan/scanner_context_test.cpp | 53 ++++++++++++++++++++++++++++--
2 files changed, 55 insertions(+), 5 deletions(-)
diff --git a/be/src/exec/scan/scanner_context.cpp
b/be/src/exec/scan/scanner_context.cpp
index 944cef09d8d..c8727893f49 100644
--- a/be/src/exec/scan/scanner_context.cpp
+++ b/be/src/exec/scan/scanner_context.cpp
@@ -542,17 +542,18 @@ void ScannerContext::stop_scanners(RuntimeState* state) {
}
std::string ScannerContext::debug_string() {
+ // Queued scan tasks can outlive the scan operator, including during this
context's
+ // destructor. Do not dereference the operator-owned _shared_scan_limit
for diagnostics.
return fmt::format(
"_query_id: {}, id: {}, total scanners: {}, pending tasks: {},
completed tasks: {},"
" _should_stop: {}, _is_finished: {}, free blocks: {},"
- " limit: {}, remaining_limit: {}, _in_flight_tasks_num: {},
_is_context_queued: {}, "
+ " limit: {}, _in_flight_tasks_num: {}, _is_context_queued: {}, "
"_num_finished_scanners: {}, _max_scan_concurrency: {},
expected_scanners: {},"
" _max_bytes_in_queue: {}, _ins_idx: {},
_enable_adaptive_scanners: {}, "
"_mem_share_arb: {}, _scanner_mem_limiter: {}",
print_id(_query_id), ctx_id, _all_scanners.size(),
_pending_tasks.size(),
_completed_tasks.size(), _should_stop, _is_finished,
_free_blocks.size_approx(), limit,
- _shared_scan_limit->load(std::memory_order_relaxed),
_in_flight_tasks_num,
- _is_context_queued, _num_finished_scanners, _max_scan_concurrency,
+ _in_flight_tasks_num, _is_context_queued, _num_finished_scanners,
_max_scan_concurrency,
_enable_adaptive_scanners ? _adaptive_processor->expected_scanners
: -1,
_max_bytes_in_queue, _ins_idx, _enable_adaptive_scanners,
_enable_adaptive_scanners ? _mem_share_arb->debug_string() :
"NULL",
diff --git a/be/test/exec/scan/scanner_context_test.cpp
b/be/test/exec/scan/scanner_context_test.cpp
index 9e40d15fd52..d60f4e77d62 100644
--- a/be/test/exec/scan/scanner_context_test.cpp
+++ b/be/test/exec/scan/scanner_context_test.cpp
@@ -33,6 +33,7 @@
#include <tuple>
#include "common/config.h"
+#include "common/logging.h"
#include "common/object_pool.h"
#include "core/block/block.h"
#include "exec/operator/olap_scan_operator.h"
@@ -1122,19 +1123,67 @@ TEST_F(ScannerContextTest,
debug_string_reports_distinguishable_fields) {
scan_dependency, &shared_limit, nullptr, nullptr, 0, false,
parallel_tasks);
// Every value is distinct so a misplaced placeholder is visible in the
output.
- shared_limit.store(100);
scanner_context->_in_flight_tasks_num = 2;
scanner_context->_is_context_queued = true;
scanner_context->_num_finished_scanners = 5;
const std::string debug = scanner_context->debug_string();
- EXPECT_NE(debug.find("limit: 7, remaining_limit: 100,
_in_flight_tasks_num: 2, "
+ EXPECT_NE(debug.find("limit: 7, _in_flight_tasks_num: 2, "
"_is_context_queued: true, _num_finished_scanners: 5,
"
"_max_scan_concurrency: 3, expected_scanners: -1,"),
std::string::npos)
<< debug;
}
+TEST_F(ScannerContextTest, debug_string_after_scan_operator_destroyed) {
+ auto task_exec_ctx = std::make_shared<TaskExecutionContext>();
+ state->set_task_execution_context(task_exec_ctx);
+ auto scan_operator = std::make_unique<OlapScanOperatorX>(obj_pool.get(),
tnode, 0, *descs, 1,
+ TQueryCacheParam
{});
+ auto local_state = OlapScanLocalState::create_unique(state.get(),
scan_operator.get());
+ auto scanner_context = ScannerContext::create_shared(
+ state.get(), local_state.get(), output_tuple_desc, false,
+ std::list<std::shared_ptr<ScannerDelegate>> {}, -1,
scan_dependency,
+ &scan_operator->_shared_scan_limit, nullptr, nullptr, 0, false, 1);
+
+ // A queued task can retain the context after the fragment releases the
scan operator.
+ local_state.reset();
+ scan_operator.reset();
+ task_exec_ctx.reset();
+ EXPECT_FALSE(scanner_context->debug_string().empty());
+ ASSERT_EQ(scanner_context->task_exec_ctx(), nullptr);
+ // A failed context lock logs diagnostics before returning without
touching the scan task.
+ EXPECT_TRUE(scan_scheduler->submit(scanner_context, nullptr).ok());
+}
+
+TEST_F(ScannerContextTest, adaptive_cleanup_after_scan_operator_destroyed) {
+ const auto query_id = state->get_query_ctx()->query_id();
+ const int64_t query_mem_limit = 1024 * 1024 * 1024;
+ auto arbitrator = MemShareArbitrator::create_shared(query_id,
query_mem_limit, 0.3);
+ auto limiter = MemLimiter::create_shared(query_id, 1, false,
query_mem_limit);
+ auto scan_operator = std::make_unique<OlapScanOperatorX>(obj_pool.get(),
tnode, 0, *descs, 1,
+ TQueryCacheParam
{});
+ auto local_state = OlapScanLocalState::create_unique(state.get(),
scan_operator.get());
+ auto scanner_context = ScannerContext::create_shared(
+ state.get(), local_state.get(), output_tuple_desc, false,
+ std::list<std::shared_ptr<ScannerDelegate>> {}, -1,
scan_dependency,
+ &scan_operator->_shared_scan_limit, arbitrator, limiter, 0, true,
1);
+ limiter->update_open_tasks_count(1);
+ arbitrator->register_scan_node();
+ limiter->update_arb_mem_bytes(DEFAULT_SCANNER_MEM_BYTES);
+
+ // Enable the destructor's diagnostic path, and restore the module's
previous log level.
+ const int old_vlog_level = google::SetVLOGLevel("scanner_context", 7);
+ Defer restore_vlog([&] { google::SetVLOGLevel("scanner_context",
old_vlog_level); });
+ local_state.reset();
+ scan_operator.reset();
+ scanner_context.reset();
+
+ EXPECT_EQ(limiter->update_open_tasks_count(0), 0);
+ EXPECT_EQ(limiter->get_arb_scanner_mem_bytes(), 0);
+ EXPECT_EQ(arbitrator->total_mem_bytes.load(), 0);
+}
+
TEST_F(ScannerContextTest, thread_pool_submit_failure_policy) {
const int parallel_tasks = 2;
auto scan_operator = std::make_unique<OlapScanOperatorX>(obj_pool.get(),
tnode, 0, *descs,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]