Skip to content

fix(reader): pin render task lifetime to renderer ref and uuid - #386

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
add-uos:fix-pin-render-task-lifetime
Sep 15, 2026
Merged

deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
add-uos:fix-pin-render-task-lifetime

Conversation

@add-uos

@add-uos add-uos commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

修改说明

修复文档关闭/切换时的悬空指针(UAF)崩溃:渲染任务结构体改为携带渲染器共享引用与入队时快照的 uuid,worker 线程仅解引用这两者,并经 existSheetByUuid 跳过已销毁文档的任务。

  • DocSheet 改用 QSharedPointer 持有 SheetRenderer,析构前排空排队任务(clearAllTasksForSheet / clearAllTasksForPage
  • worker 线程约定:任务结构体仅允许解引用 renderer(共享引用,生命周期安全)与值类型快照数据,sheet/page 等裸指针仅供主线程回调使用
  • BrowserPage 回写渲染结果前校验页面存活
  • 捏合缩放定时器回调绑定 receiver,消除悬空指针

关联 PR

后续改动「侧栏缩略图仅在深色主题下反色」基于本分支堆叠,请先合入本 PR。

自测

  • 单元测试(tests/browser)通过
  • 打开/快速关闭文档、连续缩放场景无崩溃

Summary by Sourcery

Prevent rendering-related use-after-free crashes by making worker tasks lifetime-safe and ignoring stale document or page results.

Bug Fixes:

  • Prevent use-after-free crashes when documents or pages are closed or switched while rendering tasks are pending.
  • Discard stale rendering results and document tasks when their associated sheet or page is no longer alive.

Enhancements:

  • Make rendering tasks independent of document and page lifetimes by retaining shared renderer references and enqueue-time value snapshots.
  • Route document opening through immutable task data and return worker-created process state to the UI thread safely.
  • Guard delayed pinch-zoom callbacks against destruction and extend browser render task cleanup.

Tests:

  • Update browser, UI frame, and renderer unit tests for the revised task ownership and document-opening interfaces.

Render task structs now carry a renderer shared reference and a uuid
snapshot; workers dereference only those and skip tasks whose sheet is
gone (existSheetByUuid). DocSheet owns SheetRenderer via QSharedPointer
and purges queued tasks (clearAllTasksForSheet/Page) before
destruction. BrowserPage validates page aliveness before writing render
results back, and the pinch-zoom timer callback is bound to its
receiver to avoid UAF.

渲染任务结构体改为携带渲染器共享引用与入队时快照的 uuid:worker 线
程仅解引用这两者,并经 existSheetByUuid 跳过已销毁文档的任务。
DocSheet 改用 QSharedPointer 持有 SheetRenderer,析构前排空排队任务
(clearAllTasksForSheet/Page);BrowserPage 回写渲染结果前校验页面
存活;捏合缩放定时器回调绑定 receiver,消除悬空指针。

Log: 渲染任务生命周期加固,消除文档销毁后 worker/回包路径悬空指针
PMS: BUG-377151
Influence: 文档关闭/缩放过程中渲染回调不再有悬空指针风险;正常渲染行为不变。
@sourcery-ai

sourcery-ai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR eliminates document/page UAF risks by giving worker tasks shared ownership of a parentless renderer, replacing worker-side object access with UUID and value snapshots, validating document/page liveness before processing or returning results, and cleaning stale queues during destruction; it also makes pinch-zoom timer callbacks destruction-safe and updates affected tests.

Sequence diagram for UAF-safe page render task processing

sequenceDiagram
    participant BrowserPage
    participant PageRenderThread
    participant DocSheet
    participant SheetRenderer

    BrowserPage->>DocSheet: rendererPtr()
    BrowserPage->>PageRenderThread: appendTask(renderer, uuid, pageIndex, page)
    PageRenderThread->>DocSheet: existSheetByUuid(uuid)
    alt document exists and renderer is valid
        PageRenderThread->>SheetRenderer: getImage(pageIndex, ...)
        SheetRenderer-->>PageRenderThread: rendered image
        PageRenderThread-->>BrowserPage: sigDocPageNormalImageTaskFinished(task, pixmap)
        BrowserPage->>BrowserPage: existPage(page)
        alt page is alive
            BrowserPage->>BrowserPage: handleRenderFinished(...)
        end
    else document is gone
        PageRenderThread-->>PageRenderThread: skip task
    end
Loading

Flow diagram for document and page destruction cleanup

flowchart TD
    DestroySheet[DocSheet destructor] --> ClearSheet[clearAllTasksForSheet]
    DestroyPage[BrowserPage destructor] --> ClearImages[clearImageTasks]
    DestroyPage --> ClearPage[clearAllTasksForPage]
    ClearSheet --> Queues[Remove pending sheet tasks]
    ClearImages --> Queues
    ClearPage --> Queues
    Queues --> Worker[Worker skips stale or safely owned tasks]
    Worker --> Callback[Main-thread result callback]
    Callback --> PageCheck[BrowserPage existPage]
    PageCheck -->|dead page| Drop[Drop result]
Loading

File-Level Changes

Change Details Files
Make queued rendering independent of DocSheet and BrowserPage lifetimes by capturing renderer ownership and immutable task snapshots.
  • Added QSharedPointer, document UUID, page index, and render-dimension snapshots to worker task types.
  • Replaced worker-side sheet/page dereferences with renderer and snapshot data across image, text, annotation, thumbnail, and open tasks.
  • Moved document-open inputs and QProcess handoff into task snapshots, with sheet mutation confined to the main-thread completion handler.
reader/browser/PageRenderThread.h
reader/browser/PageRenderThread.cpp
reader/browser/BrowserPage.cpp
reader/sidebar/SideBarImageViewModel.cpp
reader/uiframe/DocSheet.cpp
reader/uiframe/DocSheet.h
reader/uiframe/SheetRenderer.cpp
reader/uiframe/SheetRenderer.h
Prevent stale queued work and completion callbacks from touching destroyed documents or pages.
  • Added UUID-based document existence checks in worker execution paths.
  • Added main-thread page liveness registry checks before applying render, word, and annotation results.
  • Added queue cleanup for sheet destruction and page destruction, covering image, word, annotation, and thumbnail queues.
reader/browser/BrowserPage.cpp
reader/browser/BrowserPage.h
reader/browser/PageRenderThread.cpp
reader/browser/PageRenderThread.h
reader/uiframe/DocSheet.cpp
reader/uiframe/DocSheet.h
Remove an independent delayed-callback lifetime hazard during pinch zoom.
  • Bound the delayed single-shot timer to SheetBrowser as its receiver so the callback is canceled when the browser is destroyed.
reader/browser/SheetBrowser.cpp
Update tests and stubs for the new ownership and task-snapshot interfaces.
  • Adjusted SheetRenderer construction and open-method stubs to the parentless/shared-ownership API.
  • Updated test fixtures and fake objects to provide valid sheet storage and long-lived page ownership where callbacks retain raw routing pointers.
  • Added the renderRect stub required by the updated BrowserPage path.
tests/browser/ut_browserpage.cpp
tests/browser/ut_pagerenderthread.cpp
tests/browser/ut_sheetbrowser.cpp
tests/uiframe/ut_docsheet.cpp
tests/uiframe/ut_sheetrenderer.cpp
tests/ut_mainwindow.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 1 issue

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

## Individual Comments

### Comment 1
<location path="reader/browser/PageRenderThread.cpp" line_range="856" />
<code_context>
+    //getDocument出参改为局部变量,由worker带回、主线程回调写回sheet,避免跨线程写sheet成员
+    QProcess *process = nullptr;
+    deepin_reader::Document *document = deepin_reader::DocumentFactory::getDocument(task.fileType, filePath, task.convertedFileDir, task.password, &process, error);
+    task.process = process;

     if (nullptr == document) {
</code_context>
<issue_to_address>
**issue (bug_risk):** When a document is destroyed while its open task is executing, `execNextDocOpenTask` stores the newly allocated `QProcess` in `task.process`, but `onDocOpenTask` returns after the sheet-liveness check without deleting or terminating it. The process is therefore leaked and its child process can remain running after the document has been closed.

**Triggers:** When an asynchronous document open completes after its DocSheet has been destroyed.

**Suggested fix:** Delete or terminate and delete `task.process` in the dropped-task path, while also cleaning up any document-opening resources that are no longer transferred to a live sheet.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

//getDocument出参改为局部变量,由worker带回、主线程回调写回sheet,避免跨线程写sheet成员
QProcess *process = nullptr;
deepin_reader::Document *document = deepin_reader::DocumentFactory::getDocument(task.fileType, filePath, task.convertedFileDir, task.password, &process, error);
task.process = process;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): When a document is destroyed while its open task is executing, execNextDocOpenTask stores the newly allocated QProcess in task.process, but onDocOpenTask returns after the sheet-liveness check without deleting or terminating it. The process is therefore leaked and its child process can remain running after the document has been closed.

Triggers: When an asynchronous document open completes after its DocSheet has been destroyed.

Suggested fix: Delete or terminate and delete task.process in the dropped-task path, while also cleaning up any document-opening resources that are no longer transferred to a live sheet.

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

AI 代码审查报告

总体评分: 97 分 (通过阈值: 70分)

Pass


总体评价

项目 结果
PR 地址 #386
PR 标题 fix(reader): pin render task lifetime to renderer ref and uuid
作者 add-uos
项目名称 linuxdeepin/deepin-reader
分析模式 全量分析
审查结论 代码审查通过
评分详情 代码修复了渲染管线中的 Use-After-Free (UAF) 漏洞,通过 QSharedPointer 共享引用、UUID 校验和页面存活注册表三重机制确保线程安全,实现质量高,无新增安全漏洞。

详细分析

1. 语法逻辑 ✓ (25/25)

评价: 语法正确,逻辑清晰 ✓

分析:

本 PR 修复了渲染管线中的多个 UAF 漏洞,核心逻辑设计合理:

  1. 共享引用生命周期管理:SheetRenderer 从 DocSheet 的子对象改为无父对象的共享所有权对象(QSharedPointer),worker 线程通过任务中持有的 QSharedPointer 副本确保渲染器在使用期间不被析构。
  2. UUID 校验机制:新增 DocSheet::existSheetByUuid() 基于 UUID 而非指针地址校验 sheet 存活,避免指针地址复用导致的误判。worker 线程在执行任务前通过 existSheetByUuid(task.uuid) 快速跳过已销毁文档的任务。
  3. 页面存活注册表:新增 g_alivePages 全局集合和 BrowserPage::existPage() 校验,在 5 个回调 handler(normalImage/sliceImage/bigImage/word/annotation)中解引用 task.page 前先校验页面存活。
  4. 任务数据快照:worker 线程不再解引用 task.page->itemIndex(),而是使用入队时快照的 task.pageIndex;尺寸计算使用 task.scaleFactortask.originSize 快照值。
  5. 线程安全模型清晰:g_alivePages 仅主线程增删(无需加锁),existSheetByUuid 使用读锁,任务队列使用 per-queue 互斥锁。
  6. 边界条件处理完善:null renderer 检查(task.renderer.isNull())、null page 检查(nullptr != task.page)、空 UUID 检查(task.uuid.isEmpty())均正确处理。

潜在问题:


2. 代码质量 ✓ (23/25)

评价: 代码结构清晰,注释完整 ✓

分析:

  1. 注释完整性(5/5):优秀。每个新增函数均有 Doxygen 风格文档注释,任务结构体中明确标注了"worker 线程只读数据"和"仅供主线程回调使用"的分区注释,线程安全契约在代码注释中清晰阐述。
  2. 代码重复(3/5):PageRenderThread::clearAllTasksForSheet() 函数中存在 6 个近乎相同的 lock+iterate+remove 代码块(分别处理 normalImage/sliceImage/bigImage/word/annotation/thumbnail 六种任务队列),代码重复度较高,建议通过模板函数消除重复。
  3. 结构合理性(5/5):良好。worker 线程与主线程的数据流向清晰,任务结构体明确区分了只读数据和回调专用数据。SheetRenderer 去除了对 DocSheet 的反向依赖,符合最小耦合原则。
  4. 调试信息清理(5/5):干净,无残留调试代码。注释掉的 qCDebug 日志是原有代码风格,非本次新增。
  5. 命名规范(5/5):命名清晰,existSheetByUuidclearAllTasksForPagerendererPtr 等函数名准确表达意图。

潜在问题:

  1. reader/browser/PageRenderThread.cpp 第 133-177 行,clearAllTasksForSheet 函数中存在 6 个近乎相同的 lock+iterate+remove 代码块(分别处理 normalImage/sliceImage/bigImage/word/annotation/thumbnail 六种任务队列),代码重复度较高,建议通过模板函数或辅助函数消除重复。

改进建议:
建议将 clearAllTasksForSheet 中的 6 个重复代码块提取为模板辅助函数,例如:

template<typename TaskT>
static void removeTasksForSheet(QList<TaskT> &tasks, QMutex &mutex, DocSheet *sheet) {
    QMutexLocker locker(&mutex);
    for (int i = tasks.count() - 1; i >= 0; --i) {
        if (tasks[i].sheet == sheet)
            tasks.removeAt(i);
    }
}

3. 代码性能 ✓ (19/20)

评价: 性能良好,资源使用合理 ✓

分析:

  1. QSharedPointer 引用计数开销:每个渲染任务结构体现在携带 QSharedPointer<SheetRenderer>,引入原子引用计数操作(每次入队和出队各一次原子增减)。相比实际渲染工作(文档解码、图像渲染),此开销可忽略。
  2. g_alivePages 查找性能QSet::contains 平均 O(1) 复杂度,适合回调中频繁校验。
  3. existSheetByUuid 性能:对 g_uuidList 进行线性扫描(带读锁),典型文档数量 1-10 个,开销可忽略。
  4. 任务清理性能clearAllTasksForSheetclearAllTasksForPage 在析构时线性扫描任务队列,属于清理路径非热路径,可接受。
  5. 任务数据拷贝:任务入队时额外拷贝 QString(uuid)、int(pageIndex)、qreal(scaleFactor)、QSizeF(originSize)等值类型数据,开销极小。
  6. 无性能瓶颈引入:所有新增操作均为 O(1) 或 O(n)(n 为任务队列长度,通常较小)。

潜在问题:

  1. reader/browser/PageRenderThread.h 第 17-75 行,多个任务结构体新增 QSharedPointer<SheetRenderer> 字段,引入原子引用计数操作。在高频渲染场景下有轻微开销,但当前场景下可接受。

改进建议:
QSharedPointer 的原子操作开销在当前渲染场景下可接受,无需优化。若未来渲染频率显著提升,可考虑在任务队列层面批量管理引用。


4. 代码安全 ✓ (30/30)

评价: 存在0个安全漏洞,安全合规 ✓

存在0个安全漏洞

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个

分析:

本 PR 是一项重要的安全改进,修复了渲染管线中的多个 Use-After-Free (UAF) 漏洞。以下是对安全机制的详细评估:

  1. QSharedPointer 共享引用(修复 UAF):worker 线程持有的 QSharedPointer<SheetRenderer> 副本确保渲染器在使用期间不会被析构,从根本上消除了 renderer 的 UAF 风险。
  2. existSheetByUuid 替代 existSheet(修复地址复用风险):基于 UUID 而非指针地址校验,避免已析构 sheet 的指针地址被新对象复用导致误判。
  3. g_alivePages 注册表 + existPage 校验(修复 page UAF):在 5 个回调 handler 中解引用 task.page 前校验页面存活,防止页面析构后回调悬空访问。
  4. 任务数据快照(消除跨线程解引用):worker 线程使用入队时快照的 pageIndex/scaleFactor/originSize 替代解引用 task.page,从源头消除跨线程裸指针访问。
  5. QTimer::singleShot 修复(修复定时器 UAF):SheetBrowser::pinchTriggerged 中传递 receiver=this,当 browser 在定时器触发前被销毁时,Qt 自动取消该定时器,避免 lambda 执行写已死对象。
  6. QProcess 跨线程写修复(消除跨线程写):getDocument 的 QProcess 出参改为局部变量,由主线程回调 onDocOpenTask 中写回 sheet->m_process,消除 worker 线程对 sheet 成员的跨线程写。
  7. 线程安全:g_alivePages 仅主线程访问(无竞争),existSheetByUuid 使用 QReadWriteLock 读锁,任务队列使用 per-queue QMutex 互斥锁,同步机制正确。

安全漏洞详情:
无安全漏洞

安全建议:
本次 PR 是一项安全改进,通过以下机制修复了多个 UAF 漏洞:

  1. QSharedPointer 共享引用确保 worker 线程使用期间渲染器不会被析构
  2. existSheetByUuid 替代 existSheet 避免指针地址复用导致的误判
  3. g_alivePages 注册表 + existPage 校验防止回调中解引用已析构页面
  4. 任务数据快照(pageIndex/scaleFactor/originSize)消除 worker 线程对 BrowserPage 的跨线程解引用
  5. QTimer::singleShot 传递 receiver 防止定时器回调 UAF
  6. QProcess 出参改为局部变量由主线程回调写回,消除跨线程写 sheet 成员

评分汇总

维度 得分 满分 标记 评价词
语法逻辑 25 25 语法正确,逻辑清晰
代码质量 23 25 代码结构清晰,注释完整
代码性能 19 20 性能良好,资源使用合理
代码安全 30 30 存在0个安全漏洞
总分 97 100 优秀

审查文件清单

序号 文件路径 类型
1 reader/browser/BrowserPage.cpp 源码
2 reader/browser/BrowserPage.h 源码
3 reader/browser/PageRenderThread.cpp 源码
4 reader/browser/PageRenderThread.h 源码
5 reader/browser/SheetBrowser.cpp 源码
6 reader/sidebar/SideBarImageViewModel.cpp 源码
7 reader/uiframe/DocSheet.cpp 源码
8 reader/uiframe/DocSheet.h 源码
9 reader/uiframe/SheetRenderer.cpp 源码
10 reader/uiframe/SheetRenderer.h 源码
11 tests/browser/ut_browserpage.cpp 测试
12 tests/browser/ut_pagerenderthread.cpp 测试
13 tests/browser/ut_sheetbrowser.cpp 测试
14 tests/uiframe/ut_docsheet.cpp 测试
15 tests/uiframe/ut_sheetrenderer.cpp 测试
16 tests/ut_mainwindow.cpp 测试

关键变更说明

本 PR 修复了 deepin-reader 渲染管线中的多个 Use-After-Free (UAF) 漏洞,核心变更如下:

  1. SheetRenderer 共享所有权改造:从 DocSheet 的子对象(new SheetRenderer(this))改为无父对象的共享所有权对象(QSharedPointer<SheetRenderer>::create()),去除了对 DocSheet 的反向依赖,使 renderer 可被 worker 线程安全持有。

  2. 渲染任务结构体重构:所有 6 种任务结构体(DocPageNormalImageTask/DocPageSliceImageTask/DocPageBigImageTask/DocPageWordTask/DocPageAnnotationTask/DocPageThumbnailTask)新增 renderer(QSharedPointer)、uuid(QString)、pageIndex(int)等快照字段。DocOpenTask 新增 filePath/convertedFileDir/fileType/process 字段。

  3. worker 线程解引用消除:worker 线程不再访问 task.sheet->renderer()task.page->itemIndex(),改为使用 task.renderer->task.pageIndex

  4. 页面存活注册表:新增 g_alivePages 全局集合,BrowserPage 构造时注册、析构时移除,回调中通过 BrowserPage::existPage() 校验后再解引用。

  5. 任务清理机制:新增 clearAllTasksForSheet()(析构时排空所有引用该 sheet 的任务)和 clearAllTasksForPage()(析构时排空引用该 page 的文字/注释任务)。

  6. QTimer UAF 修复SheetBrowser::pinchTriggergedQTimer::singleShot(10, this, [this](){...}) 传递 receiver,防止 browser 析构后 lambda 执行。

  7. QProcess 跨线程写消除getDocument 出参改为局部 QProcess 指针,由主线程回调 onDocOpenTask 写回 sheet->m_process


本报告由 AI 代码审查工具自动生成
扫描时间:2026-09-15 17:58:00

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: add-uos, lzwind

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

@add-uos

add-uos commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

/forcemerge

@deepin-bot

deepin-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

This pr force merged! (status: unstable)

@deepin-bot
deepin-bot Bot merged commit 8cd3399 into linuxdeepin:master Sep 15, 2026
5 of 8 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