Skip to content

fix: prevent crash when closing wayland popup surfaces#453

Merged
yixinshark merged 1 commit into
linuxdeepin:masterfrom
yixinshark:fix-windowCloseCrash
Apr 24, 2026
Merged

fix: prevent crash when closing wayland popup surfaces#453
yixinshark merged 1 commit into
linuxdeepin:masterfrom
yixinshark:fix-windowCloseCrash

Conversation

@yixinshark
Copy link
Copy Markdown
Contributor

@yixinshark yixinshark commented Apr 24, 2026

  • Problem: dde-tray-loader crashes with SEGFAULT in QWaylandWindow::decoration() during popup closing.

  • Reason: Reentrancy in QWaylandShmBackingStore::beginPaint(). It dispatches Wayland events synchronously, which can trigger plugin_popup_close() in the middle of a paint operation. Synchronously closing the window destroys the platform handle, causing beginPaint to crash when it resumes.

  • Solution 1: Use QMetaObject::invokeMethod with Qt::QueuedConnection in plugin_popup_close() to defer window destruction until the current paint operation completes.

  • Solution 2: Implement a global NullHandleGuard event filter to intercept and drop any UpdateRequest events directed at widgets or windows whose Wayland handles have been invalidated.

  • 问题: 弹窗关闭时 dde-tray-loader 崩溃,堆栈指向 QWaylandWindow::decoration()。

  • 原因: QWaylandShmBackingStore::beginPaint() 在绘制过程中同步分发 Wayland 事件,导致 plugin_popup_close() 被重入调用。同步关闭窗口会销毁底层 handle,导致 beginPaint 恢复执行时访问空指针崩溃。

  • 解决 1: 在 plugin_popup_close() 中使用 QMetaObject::invokeMethod 配合队列连接,将窗口销毁操作推迟到当前绘制周期结束之后。

  • 解决 2: 实现全局事件过滤器 NullHandleGuard,拦截并丢弃所有指向已销毁 Wayland 表面或无效窗口的 UpdateRequest 请求,防止僵尸绘制事件触发崩溃。

Log: prevent crash when closing wayland popup surfaces
Pms: BUG-358679
Change-Id: I2610d04675ee628080eab2c52171f39d9d752c91

Summary by Sourcery

Defer destruction of Wayland popup windows and guard against painting invalidated surfaces to prevent crashes when closing Wayland popups.

Bug Fixes:

  • Prevent a segmentation fault in dde-tray-loader caused by synchronous destruction of Wayland popup windows during QWaylandShmBackingStore paint operations.

Enhancements:

  • Introduce a global NullHandleGuard event filter that drops UpdateRequest events targeting widgets or windows whose underlying Wayland handles have been destroyed.

Documentation:

  • Update source file copyright years to extend through 2026.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 24, 2026

Reviewer's Guide

Defers Wayland popup window destruction to avoid reentrancy-related crashes in QWaylandShmBackingStore::beginPaint and adds a global event filter to drop paint requests for windows with invalid Wayland handles.

Sequence diagram for deferred Wayland popup close handling

sequenceDiagram
    participant WaylandCompositor
    participant QWaylandShmBackingStore
    participant PluginPopupSurface
    participant QApplication
    participant PopupWidget
    participant QWindow

    WaylandCompositor->>QWaylandShmBackingStore: frame begins
    QWaylandShmBackingStore->>QWaylandShmBackingStore: beginPaint
    QWaylandShmBackingStore->>WaylandCompositor: process Wayland events
    WaylandCompositor-->>PluginPopupSurface: plugin_popup_close

    PluginPopupSurface->>PluginPopupSurface: NullHandleGuard::install
    PluginPopupSurface->>QApplication: QMetaObject::invokeMethod
    note over QApplication: Queue lambda to close popup via Qt::QueuedConnection

    QWaylandShmBackingStore-->>QWaylandShmBackingStore: beginPaint resumes safely

    QApplication-->>A QApplication: next event loop iteration
    QApplication->>A QApplication: execute queued lambda

    A QApplication->>QApplication: find popup top level widget
    QApplication->>PopupWidget: hide
    QApplication->>QWindow: close
    QWindow->>WaylandCompositor: destroy Wayland surface
Loading

Updated class diagram for PluginPopupSurface and NullHandleGuard

classDiagram
    class QObject
    class QApplication
    class QWidget
    class QWindow
    class QEvent

    class NullHandleGuard {
        +NullHandleGuard(parent : QObject)
        +static install() void
        +eventFilter(watched : QObject, event : QEvent) bool
    }

    class PluginSurface {
        +PluginSurface(manager : PluginManagerIntegration, window : QWindow)
    }

    class PluginPopupSurface {
        -m_window : QWindow*
        +PluginPopupSurface(manager : PluginManagerIntegration, window : QWindow)
        +~PluginPopupSurface()
        +plugin_popup_close() void
        +plugin_popup_geometry(x : int32_t, y : int32_t, width : int32_t, height : int32_t) void
    }

    class PluginManagerIntegration

    QObject <|-- NullHandleGuard
    QObject <|-- QApplication
    QObject <|-- QWidget
    QObject <|-- QWindow

    PluginSurface --> QWindow : owns window
    PluginPopupSurface --> QWindow : uses m_window
    PluginPopupSurface --> PluginManagerIntegration : uses

    NullHandleGuard ..> QApplication : installsEventFilter
    NullHandleGuard ..> QWidget : inspects
    NullHandleGuard ..> QWindow : inspects handle
    NullHandleGuard ..> QEvent : filters UpdateRequest

    PluginPopupSurface ..> NullHandleGuard : uses install
    PluginPopupSurface ..> QApplication : uses invokeMethod and topLevelWidgets
    PluginPopupSurface ..> QWidget : finds popupWidget
    PluginPopupSurface ..> QWindow : close via queued lambda
Loading

Flow diagram for NullHandleGuard UpdateRequest filtering

flowchart TD
    A[QApplication event loop receives event] --> B{event type is UpdateRequest}
    B -- no --> Z[Pass event to QObject eventFilter]
    B -- yes --> C[Determine watched object type]

    C --> D{watched is QWidget}
    C --> E{watched is QWindow}

    D -- yes --> F[Resolve QWindow from QWidget via windowHandle or window]
    D -- no --> E

    E -- yes --> G[Assign watched as QWindow]
    E -- no --> H[No QWindow and watched may be QWidget]

    F --> I{QWindow exists}
    G --> I

    I -- no --> J[Drop UpdateRequest return true]
    I -- yes --> K{QWindow handle is null}

    K -- yes --> J
    K -- no --> Z

    H --> L{watched is QWidget and no QWindow}
    L -- yes --> J
    L -- no --> Z

    Z[Fallback to QObject eventFilter return QObject::eventFilter]
Loading

File-Level Changes

Change Details Files
Introduce a global NullHandleGuard event filter to ignore UpdateRequest events targeting widgets/windows whose underlying QWindow handle or Wayland surface is no longer valid.
  • Add QApplication/QWidget/QEvent includes and define a QObject-derived NullHandleGuard class inside the Plugin namespace
  • Implement a static install() method that installs a single global event filter instance on qApp
  • In eventFilter, detect QEvent::UpdateRequest and resolve the associated QWindow from either QWidget or QWindow watchers
  • Return true (consume the event) when the resolved QWindow has no handle or when a QWidget has lost its QWindow entirely, otherwise delegate to the base implementation
src/tray-wayland-integration/pluginsurface.cpp
Make popup closure asynchronous and safer to prevent crashes caused by destroying the Wayland window during a paint cycle.
  • Update plugin_popup_close() to call NullHandleGuard::install() before closing
  • Wrap m_window in a QPointer to guard against it being deleted before the queued operation runs
  • Use QMetaObject::invokeMethod on qApp with Qt::QueuedConnection to defer window closing to the next event loop iteration via a lambda
  • In the queued lambda, locate the top-level QWidget associated with the QWindow, hide it if found, and then close the QWindow safely
src/tray-wayland-integration/pluginsurface.cpp
Update copyright header metadata.
  • Extend the SPDX-FileCopyrightText year range from 2023 to 2023-2026
src/tray-wayland-integration/pluginsurface.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 1 issue, and left some high level feedback:

  • Consider scoping NullHandleGuard to only watch the specific tray/popup windows instead of installing a global filter on qApp, to reduce the risk of dropping legitimate UpdateRequest events for unrelated widgets/windows.
  • In NullHandleGuard::eventFilter, the branch that drops UpdateRequest for any QWidget that has lost its QWindow is quite broad; it may be safer to tighten the condition (e.g., check widget type or ownership) so that transient states during normal widget lifetime are not accidentally blocked.
  • The NullHandleGuard::install static flag is not thread-safe; if popup close can be triggered from different threads in the future, consider guarding installation with std::call_once or ensuring it is only ever called from the GUI thread.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider scoping `NullHandleGuard` to only watch the specific tray/popup windows instead of installing a global filter on `qApp`, to reduce the risk of dropping legitimate `UpdateRequest` events for unrelated widgets/windows.
- In `NullHandleGuard::eventFilter`, the branch that drops `UpdateRequest` for any `QWidget` that has lost its `QWindow` is quite broad; it may be safer to tighten the condition (e.g., check widget type or ownership) so that transient states during normal widget lifetime are not accidentally blocked.
- The `NullHandleGuard::install` static flag is not thread-safe; if popup close can be triggered from different threads in the future, consider guarding installation with `std::call_once` or ensuring it is only ever called from the GUI thread.

## Individual Comments

### Comment 1
<location path="src/tray-wayland-integration/pluginsurface.cpp" line_range="19" />
<code_context>

 namespace Plugin {
+
+class NullHandleGuard : public QObject {
+public:
+    static void install() {
</code_context>
<issue_to_address>
**issue (complexity):** Consider scoping the crash guard and widget handling directly to the popup window to avoid global hooks and implicit searches.

You can keep the crash workaround but make it more local and explicit with two small refactors:

1. **Scope the guard to the popup window instead of qApp**

Rather than a global event filter inspecting every `UpdateRequest`, track the specific popup window(s) and filter only those. This keeps the behavior but makes the flow more obvious.

```c++
class NullHandleGuard : public QObject {
public:
    explicit NullHandleGuard(QWindow *window, QObject *parent = nullptr)
        : QObject(parent), m_window(window) {
        if (m_window)
            m_window->installEventFilter(this);
    }

protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (watched != m_window)
            return QObject::eventFilter(watched, event);

        if (event->type() == QEvent::UpdateRequest) {
            // Drop UpdateRequest to prevent QWaylandShmBackingStore from crashing
            // in beginPaint/decoration() when trying to paint a destroyed window.
            if (m_window && !m_window->handle())
                return true;
        }
        return QObject::eventFilter(watched, event);
    }

private:
    QPointer<QWindow> m_window;
};
```

Then, in `PluginPopupSurface` (e.g. ctor or a dedicated init method), attach the guard:

```c++
PluginPopupSurface::PluginPopupSurface(...)
    : ...
{
    // ...
    m_nullHandleGuard.reset(new NullHandleGuard(m_window, m_window));
}
```

(Where `m_nullHandleGuard` is a `std::unique_ptr<NullHandleGuard>` or `QScopedPointer<NullHandleGuard>` member.)

This removes the global `qApp` filter and the QWidget/QWindow probing logic while preserving the “drop paint on destroyed window” behavior for the affected popup.

2. **Avoid `topLevelWidgets()` scan by storing the popup widget**

Instead of scanning all top-level widgets in the deferred lambda, store an explicit association when the popup is created. For example, if you can get the widget at construction time:

```c++
class PluginPopupSurface : public QtWaylandClient::QWaylandShellSurface {
    // ...
    QPointer<QWidget> m_popupWidget;
};

// In the ctor or setup:
PluginPopupSurface::PluginPopupSurface(...)
    : ...
{
    // Example: if PluginPopup::get(m_window) returns/knows the widget:
    if (auto popup = PluginPopup::get(m_window))
        m_popupWidget = popup->widget(); // adjust to your actual API
}
```

Then `plugin_popup_close()` becomes simpler and avoids the global search:

```c++
void PluginPopupSurface::plugin_popup_close()
{
    QPointer<QWindow> safeWindow(m_window);
    QPointer<QWidget> safeWidget(m_popupWidget);

    QMetaObject::invokeMethod(qApp, [safeWindow, safeWidget]() {
        if (!safeWindow)
            return;

        if (safeWidget)
            safeWidget->hide();

        safeWindow->close();
    }, Qt::QueuedConnection);
}
```

This keeps the deferred destruction and crash avoidance intact, but removes the cross-cutting globals (`qApp` event filter + `topLevelWidgets()` scan) and makes the lifecycle easier to reason about.
</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/tray-wayland-integration/pluginsurface.cpp
@yixinshark yixinshark force-pushed the fix-windowCloseCrash branch from ac598d8 to 243971f Compare April 24, 2026 03:43
- Problem: dde-tray-loader crashes with SEGFAULT in QWaylandWindow::decoration() during popup closing.
- Reason: Reentrancy in QWaylandShmBackingStore::beginPaint(). It dispatches Wayland events synchronously, which can trigger plugin_popup_close() in the middle of a paint operation. Synchronously closing the window destroys the platform handle, causing beginPaint to crash when it resumes.
- Solution 1: Use QMetaObject::invokeMethod with Qt::QueuedConnection in plugin_popup_close() to defer window destruction until the current paint operation completes.
- Solution 2: Implement a global NullHandleGuard event filter to intercept and drop any UpdateRequest events directed at widgets or windows whose Wayland handles have been invalidated.

- 问题: 弹窗关闭时 dde-tray-loader 崩溃,堆栈指向 QWaylandWindow::decoration()。
- 原因: QWaylandShmBackingStore::beginPaint() 在绘制过程中同步分发 Wayland 事件,导致 plugin_popup_close() 被重入调用。同步关闭窗口会销毁底层 handle,导致 beginPaint 恢复执行时访问空指针崩溃。
- 解决 1: 在 plugin_popup_close() 中使用 QMetaObject::invokeMethod 配合队列连接,将窗口销毁操作推迟到当前绘制周期结束之后。
- 解决 2: 实现全局事件过滤器 NullHandleGuard,拦截并丢弃所有指向已销毁 Wayland 表面或无效窗口的 UpdateRequest 请求,防止僵尸绘制事件触发崩溃。

Log: prevent crash when closing wayland popup surfaces
Pms: BUG-358679
Change-Id: I2610d04675ee628080eab2c52171f39d9d752c91
@yixinshark yixinshark force-pushed the fix-windowCloseCrash branch from 243971f to 6157880 Compare April 24, 2026 05:05
@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

这份代码 diff 主要是为了修复 Wayland 环境下,在窗口关闭过程中因 Wayland Surface 被销毁而导致的崩溃问题。引入了 NullHandleGuard 事件过滤器来拦截非法的绘制请求,并修改了 plugin_popup_close 的逻辑,将窗口销毁操作推迟到下一个事件循环。

以下是对这段代码的详细审查和改进建议:

1. 语法与逻辑审查

  • 逻辑正确性

    • NullHandleGuard 的逻辑:该类通过拦截 QEvent::UpdateRequest 事件,检查窗口的 handle() 是否为空。这是处理 Wayland 窗口生命周期问题的常见且有效的手段,防止在底层 Surface 销毁后 Qt 仍尝试进行绘制操作。
    • plugin_popup_close 的逻辑:原代码直接调用 m_window->close()。新代码使用 QMetaObject::invokeMethod 配合 Qt::QueuedConnection 将关闭操作推迟。这非常关键,因为 Wayland 的回调(如 plugin_popup_close)往往是在处理绘制缓冲区的过程中同步触发的,此时立即销毁窗口会导致栈上的绘制函数访问已释放的内存。改进后的逻辑是合理的。
  • 潜在逻辑问题

    • 查找 popupWidget 的方式:在 plugin_popup_close 的 lambda 中,通过遍历 QApplication::topLevelWidgets() 来查找对应的 QWidget。这种方式效率较低(O(N)),且依赖于 topLevelWidgets 的顺序。如果该窗口不是顶级窗口,或者存在多个相同 windowHandle 的引用(虽然少见),可能会出问题。通常 QWindow 可以通过 QWindow::fromWinId 或者直接持有对应的 QWidget 指针来获取,但考虑到这是 Wayland 插件层,可能无法直接访问 Widget 层。目前的做法虽然不够优雅,但在隔离层可能是必要的妥协。

2. 代码质量审查

  • 单例模式的实现
    • NullHandleGuard::install() 使用了静态局部变量 installed 来确保只安装一次。这是线程安全的 C++11 特性,用法得当。
  • 内存管理
    • qApp->installEventFilter(new NullHandleGuard(qApp)):这里 new 出来的对象被 qApp 接管,会在 qApp 析构时自动删除。这是 Qt 的标准做法,没有内存泄漏风险。
    • QPointer 的使用:非常好。QPointer 会在对象被销毁时自动置空,有效防止了在 lambda 异步执行时访问野指针。
  • 日志输出
    • 使用了 qDebug()。建议根据项目规范,如果这是关键路径上的错误处理,考虑使用 qWarning(),以便在 Release 模式下或日志过滤中更容易被注意到。

3. 代码性能审查

  • 事件过滤器开销
    • NullHandleGuard 被安装到了 qApp 上,这意味着应用程序中的每一个 QObject 的事件都会经过这个过滤器。
    • 虽然第一行就判断了 event->type() == QEvent::UpdateRequest,这是一个快速整数比较,开销不大。但在高频事件(如鼠标移动、Socket 通知)中,这仍然增加了一次函数调用和分支判断。
    • 建议:如果可能,尽量将事件过滤器安装到特定的窗口对象上,而不是全局 qApp。但在 Wayland 插件这种底层架构中,可能很难追踪所有相关的窗口,全局过滤可能是最稳妥的兜底方案。目前的实现是可以接受的。

4. 代码安全审查

  • 空指针检查
    • if (!installed && qApp):检查了 qApp 是否存在,安全。
    • if (!safeWindow):使用了 QPointer 并在执行前检查,安全。
  • 类型安全
    • qobject_cast 的使用是安全的,如果类型不匹配会返回 nullptr,代码中对此做了处理。

5. 改进意见

优化建议 1:优化 Widget 查找逻辑

plugin_popup_close 中,遍历所有顶级窗口来查找 Widget 的方式效率不高。如果 PluginSurfacePluginPopupSurface 在初始化时有机会获取到对应的 QWidget,应该保存下来。

如果无法保存,建议利用 QWindow 的属性或动态属性来关联 Widget,避免遍历。但鉴于当前代码上下文,如果必须保持现状,建议添加注释说明为什么需要遍历。

优化建议 2:日志级别调整

建议将 qDebug 改为 qWarning,因为拦截绘制请求通常意味着程序处于不正常的状态(窗口正在销毁但还在请求绘制),这是一个值得关注的警告。

// 修改前
qDebug() << "NullHandleGuard Dropped UpdateRequest for" << watched << "(null Wayland handle)";

// 修改后
qWarning() << "NullHandleGuard Dropped UpdateRequest for" << watched << "(null Wayland handle)";

优化建议 3:代码注释与格式

代码注释非常详细,解释了为什么要推迟销毁(防止 beginPaint 崩溃),这非常好。
建议在 NullHandleGuard 类前添加一段详细的类说明,解释其作为"全局守卫"的角色和职责。

/**
 * @brief NullHandleGuard
 * 全局事件过滤器,用于防止在 Wayland 窗口销毁过程中因底层 Surface 失效
 * 而导致的绘制崩溃。
 * 
 * 它拦截 UpdateRequest 事件,并检查 QWindow 的 handle 是否有效。
 * 如果无效,则丢弃该事件,防止 QWaylandShmBackingStore 访问空指针。
 */
class NullHandleGuard : public QObject {
    // ...
};

优化建议 4:install 方法的线程安全

虽然 static bool installed 的初始化是线程安全的,但 if (!installed && qApp) 的检查和赋值 installed = true 在多线程环境下并非原子操作(尽管 qApp 通常在主线程初始化)。
如果担心极端情况,可以使用 QAtomicIntstd::atomic<bool>,但在 Qt 应用初始化阶段通常运行在主线程,目前的写法在 99% 的场景下是安全的。

总结

这段代码的修改是为了解决一个棘手的 Wayland 崩溃问题(生命周期竞态)。引入 NullHandleGuard 和推迟 close 调用是正确的解决方案。代码整体质量较高,逻辑清晰。主要改进点在于微调日志级别、补充类注释以及考虑是否有更优的 Widget 查找方式。


protected:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::UpdateRequest) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

每个widget的每一帧都要判断么?

@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: 18202781743, yixinshark

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

@yixinshark yixinshark merged commit 97d5fcf into linuxdeepin:master Apr 24, 2026
10 of 11 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