Skip to content

fix: resolve multiple critical crashes in Qt Quick rendering#665

Merged
zccrs merged 1 commit intolinuxdeepin:masterfrom
Dami-star:crash
Dec 25, 2025
Merged

fix: resolve multiple critical crashes in Qt Quick rendering#665
zccrs merged 1 commit intolinuxdeepin:masterfrom
Dami-star:crash

Conversation

@Dami-star
Copy link
Collaborator

@Dami-star Dami-star commented Dec 25, 2025

This commit fixes three critical crash scenarios reported in issue #659:

  1. DataManager CleanJob use-after-free crash ([Bug]: coredump--关闭窗口崩溃 #659 comment 1)

    Problem:

    • In CleanJob::run(), calling manager->get()->destroy() could trigger shared_ptr destruction, which might delete the DataManager itself
    • Subsequent loop iterations would access the freed manager pointer

    Solution:

    • Collect items to destroy in a separate list first
    • Only destroy items after the loop completes and we're done accessing manager
    • Add null check before destruction to handle edge cases
  2. Buffer double-unlock crash ([Bug]: coredump--关闭窗口崩溃 #659 comment 5) Problem:

    • When Wayland surface commits the same buffer repeatedly (pointer unchanged), the code would call pendingBuffer.reset(newBuffer) where newBuffer points to the same object as pendingBuffer.get()
    • std::unique_ptr::reset() ALWAYS calls the deleter (unlocker) even when old_ptr == new_ptr, causing:
      • Step 1: deleter(old_ptr) → wlr_buffer_unlock(A) → n_locks: 1 → 0
      • Step 2: ptr = new_ptr (same object A) * Step 3: lock() → wlr_buffer_lock(A) → n_locks: 0 → 1 - The unlock in step 1 triggers assert(buffer->n_locks > 0) in wlroots

    Why not lock before reset?

    • Scenario 1 (same pointer): Creates temporary incorrect ref count

      • lock() → n_locks: 1 → 2 (but only 1 actual holder!)
      • reset() → unlock() → n_locks: 2 → 1 * Result: Temporarily inaccurate ref count, potential race conditions
    • Scenario 2 (different pointer): Locks wrong buffer! * pendingBuffer → A, newBuffer → B * lock() → A.n_locks: 1 → 2 (locking old buffer A!) * reset(B) → unlock(A) → A.n_locks: 2 → 1 * Result: B has n_locks=0 (not locked!), A has leaked reference

    Solution:

    • Check if pointer actually changed before calling reset()
    • Only perform reset + lock sequence when pointers differ
    • When pointers are same, skip the operation entirely
  3. QSGNode markDirty crash during window destruction ([Bug]: coredump--关闭窗口崩溃 #659 comment 6)

    Problem:

    • releaseResources() is called during window destruction
    • Calling QQuickItemPrivate::dirty() tries to mark scene graph nodes dirty
    • But window and scene graph may already be destroyed at this point
    • Accessing destroyed nodes causes crash

    Solution:

    • Only call dirty() if window() returns non-null
    • This ensures scene graph is still valid before marking nodes dirty

Summary by Sourcery

Fix multiple crash scenarios in Qt Quick Wayland rendering by hardening buffer management, deferred destruction, and scene graph updates during window teardown.

Bug Fixes:

  • Prevent double-unlock and inconsistent reference counting by only resetting Wayland buffers when the underlying pointer actually changes in live and non-live modes.
  • Avoid use-after-free in DataManager cleanup by deferring destruction of released items until after manager data structures are no longer being accessed.
  • Prevent crashes during window destruction by only marking items dirty when a valid window and scene graph are still available in WSurfaceItemContent and WBufferItem.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 25, 2025

Reviewer's Guide

Fixes three crash scenarios in Qt Quick rendering by guarding buffer resets against same-pointer cases to avoid double-unlock, deferring DataManager item destruction until after iteration to avoid use-after-free, and only marking items dirty when a valid window exists during resource release to avoid accessing destroyed scene graph nodes.

Sequence diagram for buffer commit handling in WSurfaceItemContent

sequenceDiagram
    participant WaylandSurface as WaylandSurface
    participant WSurfaceItemContent as WSurfaceItemContent
    participant WSurfaceItemContentPrivate as WSurfaceItemContentPrivate
    participant PendingBuffer as pendingBuffer
    participant Buffer as buffer
    participant WlrBuffer as wlr_buffer

    WaylandSurface->>WSurfaceItemContent: commit()
    WSurfaceItemContent->>WSurfaceItemContentPrivate: handleSurfaceCommit()
    WSurfaceItemContentPrivate->>WaylandSurface: buffer()
    WaylandSurface-->>WSurfaceItemContentPrivate: newBuffer

    alt non_live_mode
        WSurfaceItemContentPrivate->>PendingBuffer: get()
        PendingBuffer-->>WSurfaceItemContentPrivate: oldPtr
        WSurfaceItemContentPrivate->>WSurfaceItemContentPrivate: compare oldPtr != newBuffer
        alt pointer_changed
            WSurfaceItemContentPrivate->>PendingBuffer: reset(newBuffer)
            PendingBuffer-->>WlrBuffer: unlock(oldPtr)
            PendingBuffer->>WlrBuffer: lock(newBuffer)
        else pointer_unchanged
            WSurfaceItemContentPrivate->>WSurfaceItemContentPrivate: skip reset and lock
        end
    else live_mode
        WSurfaceItemContentPrivate->>Buffer: get()
        Buffer-->>WSurfaceItemContentPrivate: oldPtr
        WSurfaceItemContentPrivate->>WSurfaceItemContentPrivate: compare oldPtr != newBuffer
        alt pointer_changed
            WSurfaceItemContentPrivate->>Buffer: reset(newBuffer)
            Buffer-->>WlrBuffer: unlock(oldPtr)
            Buffer->>WlrBuffer: lock(newBuffer)
            WSurfaceItemContentPrivate->>WSurfaceItemContent: update()
        else pointer_unchanged
            WSurfaceItemContentPrivate->>WSurfaceItemContentPrivate: skip reset and lock
        end
    end
Loading

Sequence diagram for DataManager CleanJob deferred destruction

sequenceDiagram
    participant RenderThread as RenderThread
    participant CleanJob as CleanJob
    participant DataManager as DataManager
    participant Data as Data
    participant DataType as DataType

    RenderThread->>CleanJob: run()
    CleanJob->>DataManager: manager pointer
    CleanJob->>DataManager: cleanJob = nullptr

    CleanJob->>DataManager: tmp.swap(dataList)
    CleanJob->>DataManager: dataList.reserve(tmp.size())

    loop for each shared Data in tmp
        CleanJob->>Data: check released
        alt released > 2
            CleanJob->>CleanJob: itemsToDestroy.append(data.data)
        else released <= 2
            CleanJob->>DataManager: dataList << data
            CleanJob->>Data: update released counter
        end
    end

    CleanJob->>CleanJob: check manager is not null
    alt manager not null
        loop for each item in itemsToDestroy
            CleanJob->>DataManager: get()
            DataManager-->>CleanJob: resourceManager
            CleanJob->>DataType: destroy(item)
        end
    else manager null
        CleanJob->>CleanJob: skip destruction to avoid use-after-free
    end
Loading

Class diagram for updated Qt Quick rendering components

classDiagram
    class QQuickItem {
    }

    class QQuickItemPrivate {
        +void dirty(int flags)
    }

    class WSurfaceItemContent {
        +void releaseResources()
        +QWindow window()
    }

    class WSurfaceItemContentPrivate {
        -std_unique_ptr~WlrBuffer~ pendingBuffer
        -std_unique_ptr~WlrBuffer~ buffer
        +void handleSurfaceCommit()
    }

    class WBufferItem {
        +void releaseResources()
        +QWindow window()
    }

    class DataManagerBase {
    }

    class DataManager {
        +QPointer~DataManager~ manager
        +QList~shared_ptr_Data~~ dataList
        +QPointer~CleanJob~ cleanJob
        +ResourceManager get()
    }

    class CleanJob {
        +void run()
        -QPointer~DataManager~ manager
    }

    class Data {
        +int released
        +DataType* data
    }

    class DataType {
    }

    WSurfaceItemContent --|> QQuickItem
    WBufferItem --|> QQuickItem
    WSurfaceItemContent ..> QQuickItemPrivate : uses
    WBufferItem ..> QQuickItemPrivate : uses

    DataManager --|> DataManagerBase
    DataManager o-- CleanJob
    DataManager o-- Data
    Data --> DataType

    WSurfaceItemContent *-- WSurfaceItemContentPrivate
    WSurfaceItemContentPrivate o-- WlrBuffer

    class WlrBuffer {
        +void lock()
        +void unlock()
    }
Loading

File-Level Changes

Change Details Files
Prevent double-unlock and incorrect ref-counting when updating Wayland surface buffers held in unique_ptrs.
  • Introduce a local newBuffer pointer from surface->buffer() and compare it with existing unique_ptr-managed pointers before resetting.
  • Guard pendingBuffer.reset(...) and subsequent lock() in non-live mode with a pointer inequality check.
  • Apply the same pointer inequality check to buffer.reset(...) in live mode before resetting and locking the buffer, keeping existing update() behavior.
waylib/src/server/qtquick/wsurfaceitem.cpp
Avoid use-after-free in DataManager clean job when destroying released data entries.
  • Swap manager->dataList into a temporary list and pre-reserve capacity on the manager list.
  • Accumulate DataType* instances to be destroyed into a separate itemsToDestroy list instead of destroying them inside the iteration.
  • After finishing iteration and only if manager is still valid, iterate itemsToDestroy and call manager->get()->destroy(item) for each.
waylib/src/server/qtquick/private/wrenderbuffernode.cpp
Prevent crashes when marking QQuickItems dirty during window/scene graph teardown.
  • Wrap QQuickItemPrivate::get(this)->dirty(QQuickItemPrivate::Content) in a window() null check in WSurfaceItemContent::releaseResources().
  • Apply the same window() guard around the dirty() call in WBufferItem::releaseResources() to avoid accessing invalid scene graph nodes during destruction.
waylib/src/server/qtquick/wsurfaceitem.cpp
waylib/src/server/qtquick/wbufferitem.cpp

Possibly linked issues


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

@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

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `waylib/src/server/qtquick/private/wrenderbuffernode.cpp:234-236` </location>
<code_context>
+
+            // Destroy items after we're done accessing manager
+            // This prevents crashes if manager gets deleted during destruction
+            if (manager) {
+                for (auto item : std::as_const(itemsToDestroy)) {
+                    manager->get()->destroy(item);
+                }
+            }
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against manager being deleted reentrantly during item destruction loop.

Deferring `destroy()` avoids reentrancy during the `dataList` iteration, but there’s still a lifetime hazard: `manager` is a `QPointer`, and `manager->get()->destroy(item)` may delete the manager (or its target) mid-loop. If that happens, `manager` becomes null, yet subsequent iterations still call `manager->get()` because the `if (manager)` is only checked once before the loop, leading to a crash.

To make this safe:
- Re-check `manager` on every iteration and break if it’s null:
  ```cpp
  for (auto item : std::as_const(itemsToDestroy)) {
      if (!manager)
          break;
      manager->get()->destroy(item);
  }
  ```
- Or, if valid in this context, capture a raw `DataManager*` whose lifetime is guaranteed for the whole cleanup, and use that instead of the `QPointer` in the loop.
</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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes three critical crash scenarios in Qt Quick Wayland rendering related to buffer management, resource cleanup, and scene graph node lifecycle:

  • Fixes a use-after-free crash in DataManager's CleanJob by deferring item destruction until after manager data structures are no longer being accessed
  • Resolves a buffer double-unlock assertion failure in wlroots by only calling unique_ptr::reset() when the buffer pointer actually changes
  • Prevents crashes during window destruction by checking if the window is still valid before marking scene graph nodes as dirty

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
waylib/src/server/qtquick/wsurfaceitem.cpp Adds buffer pointer comparison checks before reset operations to prevent double-unlock; adds window validity check in releaseResources() to prevent scene graph access during destruction
waylib/src/server/qtquick/wbufferitem.cpp Adds window validity check in releaseResources() to prevent scene graph access during window destruction
waylib/src/server/qtquick/private/wrenderbuffernode.cpp Refactors CleanJob to collect items for destruction in a separate list before destroying them, preventing use-after-free when manager gets deleted during item destruction

This commit fixes three critical crash scenarios reported in issue linuxdeepin#659:

1. DataManager CleanJob use-after-free crash (linuxdeepin#659 comment 1)

   Problem:
   - In CleanJob::run(), calling manager->get()->destroy() could trigger
     shared_ptr<Data> destruction, which might delete the DataManager itself
   - Subsequent loop iterations would access the freed manager pointer

   Solution:
   - Collect items to destroy in a separate list first
   - Only destroy items after the loop completes and we're done accessing manager
   - Add null check before destruction to handle edge cases

2. Buffer double-unlock crash (linuxdeepin#659 comment 5)
   Problem:
   - When Wayland surface commits the same buffer repeatedly (pointer unchanged),
     the code would call pendingBuffer.reset(newBuffer) where newBuffer points
     to the same object as pendingBuffer.get()
   - std::unique_ptr::reset() ALWAYS calls the deleter (unlocker) even when
     old_ptr == new_ptr, causing:
       * Step 1: deleter(old_ptr) → wlr_buffer_unlock(A) → n_locks: 1 → 0
       * Step 2: ptr = new_ptr (same object A)
       * Step 3: lock() → wlr_buffer_lock(A) → n_locks: 0 → 1
     - The unlock in step 1 triggers assert(buffer->n_locks > 0) in wlroots

   Why not lock before reset?
   - Scenario 1 (same pointer): Creates temporary incorrect ref count
     * lock() → n_locks: 1 → 2 (but only 1 actual holder!)
     * reset() → unlock() → n_locks: 2 → 1
     * Result: Temporarily inaccurate ref count, potential race conditions

   - Scenario 2 (different pointer): Locks wrong buffer!
     * pendingBuffer → A, newBuffer → B
     * lock() → A.n_locks: 1 → 2 (locking old buffer A!)
     * reset(B) → unlock(A) → A.n_locks: 2 → 1
     * Result: B has n_locks=0 (not locked!), A has leaked reference

   Solution:
   - Check if pointer actually changed before calling reset()
   - Only perform reset + lock sequence when pointers differ
   - When pointers are same, skip the operation entirely

3. QSGNode markDirty crash during window destruction (linuxdeepin#659 comment 6)

   Problem:
   - releaseResources() is called during window destruction
   - Calling QQuickItemPrivate::dirty() tries to mark scene graph nodes dirty
   - But window and scene graph may already be destroyed at this point
   - Accessing destroyed nodes causes crash

   Solution:
   - Only call dirty() if window() returns non-null
   - This ensures scene graph is still valid before marking nodes dirty
@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Dami-star, zccrs

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

@zccrs zccrs merged commit 920c53f into linuxdeepin:master Dec 25, 2025
12 of 14 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.

4 participants