Skip to content

fix: resolve mutex deadlock in async file info query#3772

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
liyigang1:master-fix
May 18, 2026
Merged

fix: resolve mutex deadlock in async file info query#3772
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
liyigang1:master-fix

Conversation

@liyigang1
Copy link
Copy Markdown
Contributor

@liyigang1 liyigang1 commented May 14, 2026

The async initQuerierAsync methods in SyncFileInfo and AsyncFileInfo used QMutexLocker (RAII lock guard) which keeps the mutex locked until the function returns. However, the async callback may be invoked while the mutex is still held, and if the callback or its callers attempt to acquire the same lock, a deadlock occurs. Changed to manual lock management and unlock the mutex in the wrapper callback before invoking the user callback to prevent the deadlock.

Log: Fixed deadlock in async file info initialization

Influence:

  1. Test async file info query during file operations
  2. Verify no deadlock occurs under concurrent access
  3. Check file info query works correctly with sync and async paths
  4. Test with files on various filesystem types
  5. Verify lock is properly released in all callback paths

fix: 修复异步文件信息查询中的互斥锁死锁问题

SyncFileInfo 和 AsyncFileInfo 中的异步 initQuerierAsync 方法使用了 QMutexLocker(RAII锁守卫),该守卫在函数返回前一直持有互斥锁。然而,
异步回调可能在互斥锁仍被持有时就被调用,如果回调或其调用者尝试获取同一
把锁,就会发生死锁。改为手动管理锁的生命周期,并在调用用户回调函数之前
先在包装回调中释放互斥锁,以防止死锁发生。

Log: 修复异步文件信息初始化时的死锁问题

Influence:

  1. 测试文件操作期间的异步文件信息查询
  2. 验证并发访问下不会发生死锁
  3. 检查文件信息查询在同步和异步路径下均正常工作
  4. 测试各种文件系统类型
  5. 验证锁在所有回调路径中被正确释放

Summary by Sourcery

Bug Fixes:

  • Fix potential mutex deadlock when invoking async file info querier callbacks under concurrent access.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 14, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Replaces RAII-based mutex locking with explicit lock/unlock and wrapper callbacks in AsyncFileInfo and SyncFileInfo async initQuerierAsync paths to avoid deadlocks when async callbacks re-enter the same lock.

Sequence diagram for updated async initQuerierAsync locking behavior

sequenceDiagram
    actor Caller
    participant AsyncFileInfo
    participant DfmFileInfo
    participant d_lock
    participant UserCallback

    Caller->>AsyncFileInfo: asyncQueryDfmFileInfo(ioPriority, func, userData)
    AsyncFileInfo->>d_lock: lock()
    AsyncFileInfo->>DfmFileInfo: initQuerierAsync(ioPriority, callback, userData)
    AsyncFileInfo->>d_lock: unlock()

    DfmFileInfo-->>AsyncFileInfo: callback(success, data)
    AsyncFileInfo->>d_lock: unlock()
    AsyncFileInfo-->>UserCallback: func(success, data)
Loading

File-Level Changes

Change Details Files
Avoid deadlock in AsyncFileInfo::asyncQueryDfmFileInfo by replacing QMutexLocker with manual lock management and a wrapper callback that unlocks before invoking the user callback.
  • Remove QMutexLocker usage around async initQuerierAsync invocation.
  • Introduce a capturing lambda callback that first unlocks the internal mutex and then calls the user-provided callback.
  • Lock the mutex before calling initQuerierAsync on dfmFileInfo and adjust queringAttribute flag handling accordingly.
  • Add an explicit unlock after scheduling the async query to avoid holding the lock across the async boundary.
src/dfm-base/file/local/asyncfileinfo.cpp
Avoid deadlock in SyncFileInfo::initQuerierAsync by wrapping the async callback to unlock the mutex before user code runs and using explicit lock/unlock instead of QMutexLocker.
  • Replace QMutexLocker with explicit lock/unlock around the async initQuerierAsync call.
  • Create a lambda wrapper that unlocks the mutex before dispatching to the original callback.
  • Ensure the mutex is released after scheduling the async operation, rather than being held until function return.
src/dfm-base/file/local/syncfileinfo.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

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

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

  • The new manual lock/unlock flow in both async callbacks risks double-unlocking or unlocking an unlocked mutex if initQuerierAsync invokes the callback synchronously (callback unlocks, then the outer function unlocks again), which is undefined behavior; consider limiting the lock to local state changes and not holding it across the async call instead of unlocking from within the callback.
  • The callbacks capture this and access d after releasing the lock without any lifetime guarantees for the owning object, which may lead to use-after-free if the object is destroyed before the async callback runs; consider using a QPointer, std::weak_ptr, or equivalent to check liveness before accessing members.
  • If initQuerierAsync can fail or throw before registering the callback, the current pattern leaves the lock in an unlocked-but-expected-to-be-locked state for future operations; it would be safer to use RAII (e.g., QMutexLocker) for the critical section around local state and remove the cross-callback unlock responsibility altogether.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new manual lock/unlock flow in both async callbacks risks double-unlocking or unlocking an unlocked mutex if `initQuerierAsync` invokes the callback synchronously (callback unlocks, then the outer function unlocks again), which is undefined behavior; consider limiting the lock to local state changes and not holding it across the async call instead of unlocking from within the callback.
- The callbacks capture `this` and access `d` after releasing the lock without any lifetime guarantees for the owning object, which may lead to use-after-free if the object is destroyed before the async callback runs; consider using a `QPointer`, `std::weak_ptr`, or equivalent to check liveness before accessing members.
- If `initQuerierAsync` can fail or throw before registering the callback, the current pattern leaves the lock in an unlocked-but-expected-to-be-locked state for future operations; it would be safer to use RAII (e.g., `QMutexLocker`) for the critical section around local state and remove the cross-callback unlock responsibility altogether.

## Individual Comments

### Comment 1
<location path="src/dfm-base/file/local/asyncfileinfo.cpp" line_range="644-646" />
<code_context>
-
-    QMutexLocker lk(&d->lock);
-    d->dfmFileInfo->initQuerierAsync(ioPriority, func, userData);
+    auto callback = [this, func](bool success, void *data) {
+        d->lock.unlock();
+        func(success, data);
+    };
+    d->lock.lock();
</code_context>
<issue_to_address>
**issue (bug_risk):** Unlocking `d->lock` in both the callback and after `initQuerierAsync` risks double-unlock and UB.

The flow currently is:
1) `d->lock.lock();`
2) `d->dfmFileInfo->initQuerierAsync(ioPriority, callback, userData);`
3) `d->queringAttribute = false;`
4) `d->lock.unlock();`

Your callback also calls `d->lock.unlock();`. If `initQuerierAsync` ever invokes the callback synchronously (or before step 4), you will unlock the same mutex twice from the same thread, which is UB. Even if the callback is always asynchronous, it will still attempt to unlock a mutex that is not locked at that time, which is also UB.

Please avoid manual unlocks here. Keep the `QMutexLocker` RAII pattern, and if you need the lock released before calling `func`, use a separate callback that is invoked after leaving the locked region.
</issue_to_address>

### Comment 2
<location path="src/dfm-base/file/local/asyncfileinfo.cpp" line_range="648-645" />
<code_context>
+        d->lock.unlock();
+        func(success, data);
+    };
+    d->lock.lock();
+    d->dfmFileInfo->initQuerierAsync(ioPriority, callback, userData);
     d->queringAttribute = false;
+    d->lock.unlock();
     return true;
 }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Replacing `QMutexLocker` with manual lock/unlock increases risk of early-return paths leaking the lock.

Using `QMutexLocker` ensured the mutex was always released on every exit path. With manual `lock()`/`unlock()`, any new early return or exception here could easily leave the mutex locked and cause a deadlock. If you need to release the lock before calling `func`, you can still use `QMutexLocker` and explicitly call `locker.unlock()` at the desired point instead of managing the lock manually.

Suggested implementation:

```cpp
    QMutexLocker locker(&d->lock);

    auto callback = [this, func](bool success, void *data) {
        QMutexLocker callbackLocker(&d->lock);
        callbackLocker.unlock();
        func(success, data);
    };

    d->dfmFileInfo->initQuerierAsync(ioPriority, callback, userData);
    d->queringAttribute = false;
    locker.unlock();

```

1. Ensure `QMutexLocker` is already included in this translation unit (usually via `<QMutexLocker>`). If not, add the appropriate include at the top of the file.
2. If the outer scope already uses a `QMutexLocker` for `d->lock` before this snippet, you should adapt the code to reuse that locker instead of creating a new one, to avoid double-locking.
</issue_to_address>

### Comment 3
<location path="src/dfm-base/file/local/syncfileinfo.cpp" line_range="90-91" />
<code_context>
-
-    QMutexLocker lk(&d->lock);
-    d->dfmFileInfo->initQuerierAsync(ioPriority, func, userData);
+    auto callback = [this, func](bool success, void *data) {
+        d->lock.unlock();
+        func(success, data);
+    };
</code_context>
<issue_to_address>
**issue (bug_risk):** The same double-unlock / unlock-without-lock issue exists here as in `AsyncFileInfo`.

The sequence here is:
1) `d->lock.lock();`
2) `d->dfmFileInfo->initQuerierAsync(ioPriority, callback, userData);`
3) `d->lock.unlock();`

and the callback also calls `d->lock.unlock()`. This can cause undefined behavior if the callback runs synchronously or after the lock was already released.

Keep using `QMutexLocker` and, if you need to release the lock before calling `func`, explicitly unlock just before invoking `func` rather than from within the async callback.
</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-base/file/local/asyncfileinfo.cpp Outdated
Comment thread src/dfm-base/file/local/asyncfileinfo.cpp Outdated
Comment thread src/dfm-base/file/local/syncfileinfo.cpp Outdated
@liyigang1 liyigang1 force-pushed the master-fix branch 3 times, most recently from b0ac263 to f68f40f Compare May 14, 2026 11:02
@deepin-bot
Copy link
Copy Markdown
Contributor

deepin-bot Bot commented May 14, 2026

TAG Bot

New tag: 6.5.137
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #3774

The async initQuerierAsync methods in SyncFileInfo and AsyncFileInfo used
QMutexLocker (RAII lock guard) which keeps the mutex locked until the
function returns. However, the async callback may be invoked while the
mutex is still held, and if the callback or its callers attempt to
acquire the same lock, a deadlock occurs. Changed to manual lock
management and unlock the mutex in the wrapper callback before invoking
the user callback to prevent the deadlock.

Log: Fixed deadlock in async file info initialization

Influence:
1. Test async file info query during file operations
2. Verify no deadlock occurs under concurrent access
3. Check file info query works correctly with sync and async paths
4. Test with files on various filesystem types
5. Verify lock is properly released in all callback paths

fix: 修复异步文件信息查询中的互斥锁死锁问题

SyncFileInfo 和 AsyncFileInfo 中的异步 initQuerierAsync 方法使用了
QMutexLocker(RAII锁守卫),该守卫在函数返回前一直持有互斥锁。然而,
异步回调可能在互斥锁仍被持有时就被调用,如果回调或其调用者尝试获取同一
把锁,就会发生死锁。改为手动管理锁的生命周期,并在调用用户回调函数之前
先在包装回调中释放互斥锁,以防止死锁发生。

Log: 修复异步文件信息初始化时的死锁问题

Influence:
1. 测试文件操作期间的异步文件信息查询
2. 验证并发访问下不会发生死锁
3. 检查文件信息查询在同步和异步路径下均正常工作
4. 测试各种文件系统类型
5. 验证锁在所有回调路径中被正确释放
@deepin-ci-robot
Copy link
Copy Markdown
Contributor

deepin pr auto review

这是一份针对您提供的 Git Diff 的代码审查报告。本次修改主要涉及对异步/同步文件信息查询接口的空指针防护以及回调生命周期/锁作用域的优化。

总体而言,这次修改修复了一个非常关键的多线程并发与对象生命周期引发的崩溃问题,并增加了必要的入参校验。以下是详细的审查意见:


1. 语法与逻辑

文件:asyncfileinfo.cpp

-    if (d->queringAttribute)
+    if (d->queringAttribute || !func)
         return false;
  • 审查意见:逻辑正确。在原有的防重入判断基础上,增加了对回调函数 func 的空指针校验,属于防御性编程,避免了后续调用空函数指针导致的段错误。

文件:syncfileinfo.cpp

-    QMutexLocker locker(&d->lock);
-    if (d->dfmFileInfo)
-        d->dfmFileInfo->initQuerierAsync(ioPriority, func, userData);
+    if (!func)
+        return;
+
+    auto lockGuard = std::make_shared<std::unique_lock<QMutex>>(d->lock);
+    if (d->dfmFileInfo) {
+        auto wrapper = [lockGuard, func](bool success, void *data) {
+            lockGuard->unlock();
+            func(success, data);
+        };
+        d->dfmFileInfo->initQuerierAsync(ioPriority, wrapper, userData);
+    }
  • 审查意见:逻辑改进非常关键。原有的代码存在严重的并发逻辑缺陷:QMutexLocker locker(&d->lock) 会在函数结束时(即 initQuerierAsync 调用返回后)立即析构释放锁。但此时异步操作并未完成,如果异步回调在其他线程执行时访问了相关对象,极易引发崩溃或数据竞争。
  • 修改后,通过 std::shared_ptr 传递锁的所有权到 lambda 表达式中,确保了互斥锁会一直保持锁定状态,直到异步回调被触发时才显式 unlock()。这保证了在异步等待期间,相关资源不会被并发修改,逻辑上是正确的。

2. 代码质量

  • 智能指针与锁的结合:使用 std::shared_ptr<std::unique_lock> 来延长锁的生命周期是一种常见且有效的异步编程技巧。代码结构清晰,lambda 捕获列表明确。
  • 提前返回:两处都采用了 if (!func) return; 的提前返回策略,减少了嵌套层级,符合优秀代码规范。

3. 代码性能

  • 堆分配开销std::make_shared<std::unique_lock<QMutex>>() 会在堆上分配内存以创建控制块和锁对象。相比于原本栈上的 QMutexLocker,这引入了微小的堆分配开销。
  • 建议:在 I/O 异步操作的场景下,堆分配的开销(纳秒级别)相比于磁盘 I/O 耗时(毫秒级别)完全可以忽略不计,为了换取内存安全,这种 trade-off 是绝对值得的。无需为此修改

4. 代码安全

这里有几个潜在的安全隐患和需要确认的细节:

  1. 异常安全与锁的泄露
    在当前的 wrapper 实现中:

    auto wrapper = [lockGuard, func](bool success, void *data) {
        lockGuard->unlock();
        func(success, data); // 如果 func 抛出异常怎么办?
    };

    如果 func 抛出异常,或者由于某些原因导致回调未正常执行完,lockGuard 本身会在 lambda 析构时自动析构,由于 unique_lock 在析构时如果还持有锁会自动释放,所以不会造成死锁。但是,显式调用 unlock() 后,unique_lock 的状态变为“未持有锁”,其析构时就不会再调用 unlock(),这是正确的。不过,更符合 RAII 规范的做法是让锁对象自然析构释放,而不是显式调用 unlock(),除非有特殊的时序要求(例如要求在执行 func 之前就必须解锁)。

  2. 锁释放与回调执行的时序问题(关键)
    当前代码的执行顺序是:先解锁,再执行用户的回调 func

    • 潜在风险:一旦解锁,其他等待该锁的线程会立即获取到锁并可能修改相关数据。如果 func 内部依赖这些受锁保护的数据状态,可能会引发竞争。
    • 改进建议:如果业务逻辑允许,建议先执行回调,再释放锁(或者让锁随 lambda 析构自然释放)。这样可以保证在回调执行期间,对象状态依然是受保护的。
  3. 边界条件:d->dfmFileInfo 为空时的锁行为
    如果 d->dfmFileInfonullptr,代码不会进入 if 分支,wrapper 不会被使用。此时 lockGuard 会在函数结束时随 shared_ptr 引用计数归零而析构并释放锁。这是安全的,没有死锁风险。


改进建议代码

针对上述安全分析,建议对 syncfileinfo.cpp 的 wrapper 做微调,优先保证回调执行时的数据一致性,并利用 RAII 自动释放锁,避免显式 unlock() 带来的心智负担:

void SyncFileInfo::initQuerierAsync(int ioPriority, FileInfo::initQuerierAsyncCallback func, void *userData)
{
    if (!func)
        return;

    auto lockGuard = std::make_shared<std::unique_lock<QMutex>>(d->lock);
    if (d->dfmFileInfo) {
        // 捕获 lockGuard,延长生命周期至回调结束
        auto wrapper = [lockGuard, func](bool success, void *data) {
            // 先执行回调,确保回调执行期间锁仍然持有(如果业务允许)
            // lockGuard 会在 lambda 析构时自动释放锁,符合 RAII 规范
            func(success, data); 
        };
        d->dfmFileInfo->initQuerierAsync(ioPriority, wrapper, userData);
    }
    // 如果 d->dfmFileInfo 为空,lockGuard 在此自然析构释放锁,无死锁风险
}

注意:如果业务上严格要求“在调用 func 之前必须让其他线程有机会获取锁”,则保留原 Diff 中的 lockGuard->unlock();func 之前的写法;否则,建议采用上述让锁随 lambda 生命周期自然释放的写法,更加安全可靠。

@deepin-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

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

@Johnson-zs
Copy link
Copy Markdown
Contributor

/forcemerge

@deepin-bot
Copy link
Copy Markdown
Contributor

deepin-bot Bot commented May 18, 2026

This pr force merged! (status: behind)

@deepin-bot deepin-bot Bot merged commit 963c51b into linuxdeepin:master May 18, 2026
22 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