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]

Reply via email to