Skip to content

fix(dfm-search): eliminate cross-thread race on m_strategy causing heap corruption - #374

Merged
Johnson-zs merged 1 commit into
linuxdeepin:develop/meagle-20260526from
pppanghu77:bugfix/372535-mstrategy-race
Aug 4, 2026
Merged

fix(dfm-search): eliminate cross-thread race on m_strategy causing heap corruption#374
Johnson-zs merged 1 commit into
linuxdeepin:develop/meagle-20260526from
pppanghu77:bugfix/372535-mstrategy-race

Conversation

@pppanghu77

@pppanghu77 pppanghu77 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

修复内容

修复 BUG-372535:搜索压测过程中产生文件管理器 coredump。

根因:util-dfm 的可取消搜索设计中,SearchWorker::cancelSearch()Qt::DirectConnection 在调用线程读取 worker 的 std::unique_ptr<BaseSearchStrategy> m_strategy,与 doSearch()(引擎工作线程)对该指针的创建/重置形成跨线程无锁竞争,导致堆损坏 → Lucene++ 分配内存时 SIGBUS。

改动

  • 移除 cancelSearch() 槽与 requestCancel 信号,主线程不再经 DirectConnection 读 worker 的 m_strategy
  • 通过 setCancelledFlag() 向策略注入引擎级 atomic 取消标志,策略改读 m_cancelledRef
  • 删除冗余的 BaseSearchStrategy::m_cancelled,用 Q_ASSERT 强制注入
  • doSyncSearch() 开头重置 m_cancelled,超时置位,使 worker 真正停止
  • 所有 Content/OcrText/FileName 策略切换到 m_cancelledRef

PMS: https://pms.uniontech.com/bug-view-372535.html

Summary by Sourcery

Refactor the search cancellation mechanism to use an engine-level atomic flag shared with strategies, eliminating cross-thread access to the worker’s strategy pointer and preventing heap corruption during search.

Bug Fixes:

  • Fix a cross-thread race between search cancellation and strategy lifecycle that could cause heap corruption and coredumps during heavy search load.

Enhancements:

  • Inject a shared atomic cancellation flag from the search engine into the worker and all search strategies, ensuring consistent and immediate cancellation handling across content, OCR text, and filename searches.
  • Simplify the search worker by removing the direct cancelSearch slot and associated requestCancel signal wiring, relying solely on the shared cancellation flag path.
  • Ensure synchronous searches reset and respect the cancellation flag, including handling timeout-driven cancellations.

@sourcery-ai

sourcery-ai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors dfm-search cancellation to remove a cross-thread race on SearchWorker::m_strategy by eliminating the direct cancel signal/slot and introducing a shared engine-level atomic cancellation flag that is injected into all search strategies.

Sequence diagram for updated dfm-search cancellation flow

sequenceDiagram
    actor MainThread
    participant GenericSearchEngine
    participant SearchWorker
    participant BaseSearchStrategy

    MainThread->>GenericSearchEngine: init()
    GenericSearchEngine->>SearchWorker: setEngineCancelledFlag(&m_cancelled)

    MainThread->>GenericSearchEngine: search(query, options, searchType)
    GenericSearchEngine->>SearchWorker: doSearch(query, options, searchType)
    SearchWorker->>BaseSearchStrategy: setCancelledFlag(m_engineCancelled)
    SearchWorker->>BaseSearchStrategy: search(query)

    MainThread->>GenericSearchEngine: cancel()
    GenericSearchEngine->>GenericSearchEngine: m_cancelled.store(true)
    BaseSearchStrategy-->>BaseSearchStrategy: [m_cancelledRef->load()]

    MainThread->>GenericSearchEngine: searchSync(query)
    GenericSearchEngine->>GenericSearchEngine: m_cancelled.store(false)
    GenericSearchEngine-->>GenericSearchEngine: [timeout]
    GenericSearchEngine->>GenericSearchEngine: m_cancelled.store(true)
Loading

File-Level Changes

Change Details Files
Introduce engine-level atomic cancellation flag and inject it into all search strategies instead of each strategy owning its own atomic.
  • Add BaseSearchStrategy::setCancelledFlag(std::atomic), store pointer in m_cancelledRef, and assert non-null injection.
  • Replace BaseSearchStrategy::m_cancelled with std::atomic m_cancelledRef and update all strategy implementations to use m_cancelledRef for load/store and to pass it to helper types like SearchCancellationGuard and CancellableCollector.
  • Remove per-search reset of cancellation flags inside individual strategies’ search() methods, relying instead on engine-level management.
src/dfm-search/dfm-search-lib/core/searchstrategy/basesearchstrategy.h
src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp
src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp
src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp
src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/realtimestrategy.cpp
Remove cross-thread DirectConnection-based cancellation and wire cancellation purely through the shared atomic flag managed by GenericSearchEngine and SearchWorker.
  • Delete GenericSearchEngine::requestCancel signal and its connection to SearchWorker::cancelSearch (Qt::DirectConnection).
  • Remove SearchWorker::cancelSearch() slot and the corresponding declaration in the header.
  • Inject the engine-level m_cancelled flag into SearchWorker via setEngineCancelledFlag, and from there into each newly created strategy in SearchWorker::doSearch().
src/dfm-search/dfm-search-lib/core/genericsearchengine.cpp
src/dfm-search/dfm-search-lib/core/genericsearchengine.h
src/dfm-search/dfm-search-lib/core/searchstrategy/searchworker.cpp
src/dfm-search/dfm-search-lib/core/searchstrategy/searchworker.h
Align synchronous and asynchronous cancellation semantics by resetting and setting the engine-level cancellation flag in GenericSearchEngine.
  • Reset m_cancelled to false at the start of GenericSearchEngine::doSyncSearch() so previous cancellations don’t leak into new sync searches.
  • On timeout in doSyncSearch(), set m_cancelled to true instead of emitting requestCancel so the worker stops via the shared flag.
  • In GenericSearchEngine::cancel(), set m_cancelled to true and stop the batch timer without routing through a separate cancel signal.
src/dfm-search/dfm-search-lib/core/genericsearchengine.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Dereferencing m_cancelledRef in the various strategies and in SearchCancellationGuard assumes it is always non-null; relying only on Q_ASSERT means release builds will still have UB if the flag is not injected, so consider making this a reference or otherwise guaranteeing a non-null default (e.g., a fallback static flag) to enforce safety in all builds.
  • Given that SearchWorker::doSearch injects the engine cancel flag only when m_engineCancelled is non-null, it may be safer to assert or early-fail in doSearch when m_engineCancelled is missing, to avoid silently running strategies without a valid cancellation flag and then dereferencing m_cancelledRef.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Dereferencing `m_cancelledRef` in the various strategies and in `SearchCancellationGuard` assumes it is always non-null; relying only on `Q_ASSERT` means release builds will still have UB if the flag is not injected, so consider making this a reference or otherwise guaranteeing a non-null default (e.g., a fallback static flag) to enforce safety in all builds.
- Given that `SearchWorker::doSearch` injects the engine cancel flag only when `m_engineCancelled` is non-null, it may be safer to assert or early-fail in `doSearch` when `m_engineCancelled` is missing, to avoid silently running strategies without a valid cancellation flag and then dereferencing `m_cancelledRef`.

## Individual Comments

### Comment 1
<location path="src/dfm-search/dfm-search-lib/core/searchstrategy/basesearchstrategy.h" line_range="64-72" />
<code_context>
+     * 设 true,工作线程即时响应。flag 必须非空,由 SearchWorker::doSearch
+     * 创建策略后立即注入;未注入视为编程错误(Q_ASSERT 暴露)。
+     */
+    void setCancelledFlag(std::atomic<bool> *flag)
+    {
+        Q_ASSERT(flag);
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid relying on a potentially-null cancellation flag pointer at runtime

`BaseSearchStrategy` holds a `std::atomic<bool>*` that is default-initialized to `nullptr`, and the only protection is a `Q_ASSERT` in `setCancelledFlag`. If the strategy is used without `SearchWorker::setEngineCancelledFlag` (or if `setCancelledFlag` is forgotten), any use of `m_cancelledRef` in `cancel()`/search will dereference `nullptr` in release builds. Please either (1) provide an internal owned `atomic<bool>` fallback when no external flag is injected, (2) assert `m_cancelledRef` at every dereference, or (3) require the flag via constructor so `nullptr` is impossible by design.
</issue_to_address>

### Comment 2
<location path="src/dfm-search/dfm-search-lib/core/searchstrategy/searchworker.h" line_range="32-36" />
<code_context>

     void setStrategyFactory(std::unique_ptr<SearchStrategyFactory> factory);

+    void setEngineCancelledFlag(std::atomic<bool> *flag) { m_engineCancelled = flag; }
+
 public Q_SLOTS:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Make engine cancellation flag injection and usage stricter to prevent null propagation

`SearchWorker::doSearch` only calls `m_strategy->setCancelledFlag` when `m_engineCancelled` is non-null, and `m_engineCancelled` defaults to `nullptr`. If `setEngineCancelledFlag` is never called, both `m_engineCancelled` and the strategy’s `m_cancelledRef` remain unset, yet strategies assume a valid pointer and dereference it. To make this contract safer, consider enforcing that the flag is always set (e.g. add a `Q_ASSERT(m_engineCancelled)` before strategy creation, assert in `setEngineCancelledFlag` on null, or pass the flag into `SearchWorker`’s constructor so it can never be `nullptr`). This helps avoid latent null-deref crashes when the injection step is missed.

Suggested implementation:

```c
    void setStrategyFactory(std::unique_ptr<SearchStrategyFactory> factory);

    /**
     * Injects the engine cancellation flag.
     * This pointer must not be null; strategies assume a valid flag and may dereference it.
     */
    void setEngineCancelledFlag(std::atomic<bool> *flag)
    {
        Q_ASSERT(flag);
        m_engineCancelled = flag;
    }

public Q_SLOTS:

```

` section.

Here are the code changes:

<file_operations>
<file_operation operation="edit" file_path="src/dfm-search/dfm-search-lib/core/searchstrategy/searchworker.h">
<<<<<<< SEARCH
    void setStrategyFactory(std::unique_ptr<SearchStrategyFactory> factory);

    void setEngineCancelledFlag(std::atomic<bool> *flag) { m_engineCancelled = flag; }

public Q_SLOTS:
=======
    void setStrategyFactory(std::unique_ptr<SearchStrategyFactory> factory);

    /**
     * Injects the engine cancellation flag.
     * This pointer must not be null; strategies assume a valid flag and may dereference it.
     */
    void setEngineCancelledFlag(std::atomic<bool> *flag)
    {
        Q_ASSERT(flag);
        m_engineCancelled = flag;
    }

public Q_SLOTS:
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
1. In `searchworker.cpp` (or wherever strategies are created/configured), add a `Q_ASSERT(m_engineCancelled);` before constructing/initializing `m_strategy` and before calling `m_strategy->setCancelledFlag(...)`. This ensures that if the injection step is missed, the failure is caught early in debug builds.
2. Verify that any existing call sites of `setEngineCancelledFlag` never pass `nullptr`; if there are conditional calls, they should be refactored so the flag is always created and passed, or the condition is removed.
3. Consider updating any class invariants or documentation (e.g. comments in the class header) to explicitly state that `SearchWorker` requires a non-null cancellation flag before starting a search.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/dfm-search/dfm-search-lib/core/searchstrategy/searchworker.h
@pppanghu77

Copy link
Copy Markdown
Contributor Author

/retest

@pppanghu77

Copy link
Copy Markdown
Contributor Author

/retest-required

@pppanghu77

Copy link
Copy Markdown
Contributor Author

/retest

@pppanghu77
pppanghu77 force-pushed the bugfix/372535-mstrategy-race branch from 2ef681e to 214bb10 Compare August 4, 2026 09:06
…ap corruption

- Remove cancelSearch() slot and requestCancel signal so the main thread no longer reads the worker's m_strategy via Qt::DirectConnection (root cause of the SIGBUS heap corruption)
- Inject the engine-level atomic cancellation flag into strategies via setCancelledFlag(); strategies now read m_cancelledRef instead of a per-strategy flag
- Drop the redundant BaseSearchStrategy::m_cancelled and enforce mandatory injection with runtime check (not Q_ASSERT, which is stripped in Release)
- If m_engineCancelled is null in doSearch, emit error and abort instead of proceeding with null m_cancelledRef
- Add null guards on all m_cancelledRef dereferences in strategies (cancel() and search loops)
- Reset m_cancelled at the start of doSyncSearch() to match async search(), fixing silent empty results after a cancelled sync search
- Set m_cancelled on sync search timeout so the worker actually stops

Log: 消除主线程与 worker 线程对 SearchWorker::m_strategy 的无锁竞争(SIGBUS 堆损坏根因),补齐同步搜索取消标志重置,增加 m_cancelledRef 空指针防御
Bug: https://pms.uniontech.com/bug-view-372535.html
@pppanghu77
pppanghu77 force-pushed the bugfix/372535-mstrategy-race branch from 214bb10 to 290eec5 Compare August 4, 2026 12:25
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:90分

■ 【总体评价】

代码重构了搜索取消机制,彻底解决了跨线程信号导致的竞态条件问题,但存在传参不一致的轻微逻辑瑕疵
逻辑正确且性能提升明显,因防御性编程不一致及局部变量传参疏漏扣10分

■ 【详细分析】

  • 1.语法逻辑(基本正确)✓

代码将独立的原子变量替换为指针注入,移除了跨线程信号,整体逻辑正确。但在 ContentIndexedStrategy::performContentSearchOcrTextIndexedStrategy::performOcrTextSearch 中,引入了 dummyCancelledcancelledFlag 用于保护 SearchCancellationGuard,却将原始的 m_cancelledRef 传给了 CancellableCollector,导致防御保护不一致。虽然 SearchWorker::doSearch 的前置检查保证了 m_cancelledRef 非空,不会引发实际崩溃,但这种不一致容易在后续维护中引入空指针解引用风险。
潜在问题:CancellableCollector 接收的指针未经过 dummyCancelled 兜底保护,与 SearchCancellationGuard 的处理方式不一致;SearchWorker::setEngineCancelledFlag 缺少非空校验
建议:将传给 CancellableCollector 的参数由 m_cancelledRef 统一改为 cancelledFlag;在 SearchWorker::setEngineCancelledFlag 中增加与 BaseSearchStrategy::setCancelledFlag 一致的空指针检查和警告

  • 2.代码质量(良好)✓

重构思路清晰,通过共享指针替代信号传递,大幅降低了模块间的耦合度。注释详尽,解释了不使用 Q_ASSERT 的原因以及 RAII 守护类的用途。但各处对 m_cancelledRef 的空指针判断显得冗余,因为调用链源头已经保证了非空,过多的 if (m_cancelledRef) 增加了代码噪音和认知负担。
潜在问题:冗余的空指针检查散布于各策略的循环和取消函数中;setEngineCancelledFlag 的实现过于简单,缺少防御性代码
建议:如果确定源头保证非空,可移除策略内部的冗余空指针检查,直接解引用;或者保持防御性编程,但需统一所有下游组件(包括 CancellableCollector)的防御标准

  • 3.代码性能(高效)✓

移除了 Qt::DirectConnection 的跨线程信号发射与槽函数调用,改为直接读取 std::atomic<bool>。原子变量的 load() 操作在 x86/ARM 架构下通常编译为普通内存读取指令,无锁无系统调用,极大提升了取消机制的响应速度,消除了事件队列延迟。
建议:无需额外优化

  • 4.代码安全(存在0个安全漏洞)✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个
代码消除了原有的竞态条件风险,通过共享原子标志实现了无锁的安全取消。虽然存在 m_cancelledRef 未统一使用 cancelledFlag 兜底的情况,但由于 SearchWorker::doSearch 在调用链入口处进行了严格的非空拦截,实际运行中不存在空指针解引用的攻击面。
建议:保持当前安全水位,修复上述逻辑不一致问题以防止未来重构引入真实漏洞

■ 【改进建议代码示例】

// 文件:contentsearch/contentstrategies/indexedstrategy.cpp
// 修复 CancellableCollector 传参不一致问题
void ContentIndexedStrategy::performContentSearch(const SearchQuery &query)
{
    // RAII 守护类:自动管理取消标志的生命周期
    // 若 m_cancelledRef 为空(非正常调用链),使用局部 dummy 标志避免空指针解引用
    std::atomic<bool> dummyCancelled { false };
    std::atomic<bool> *cancelledFlag = m_cancelledRef ? m_cancelledRef : &dummyCancelled;
    SearchCancellationGuard guard(cancelledFlag);

    try {
        // 获取索引目录
        // ...
        Collection<ScoreDocPtr> scoreDocs;
        try {
            // 修复:统一使用经过空指针保护的 cancelledFlag,而不是直接使用 m_cancelledRef
            boost::shared_ptr<CancellableCollector> collector = newLucene<CancellableCollector>(cancelledFlag, maxResults);

            // 执行搜索,使用自定义收集器
            qInfo() << "Content search execution start:" << query.keyword();
            // ...
// 文件:core/searchstrategy/searchworker.h
// 补充 setEngineCancelledFlag 的防御性检查,保持架构风格一致
void setEngineCancelledFlag(std::atomic<bool> *flag)
{
    if (!flag) {
        qWarning("SearchWorker::setEngineCancelledFlag: flag is null, search will abort on doSearch");
    }
    m_engineCancelled = flag;
}

@pppanghu77

Copy link
Copy Markdown
Contributor Author

/retest-required

@Johnson-zs

Copy link
Copy Markdown
Contributor

/merge

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Johnson-zs, pppanghu77

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@deepin-bot

deepin-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

This pr cannot be merged! (status: unstable)

@pppanghu77

Copy link
Copy Markdown
Contributor Author

/forcemegre

@Johnson-zs
Johnson-zs merged commit 83a8361 into linuxdeepin:develop/meagle-20260526 Aug 4, 2026
17 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants