From 443c96d597d9ed88cd9a62b6af558e7bc38c7c60 Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Sat, 7 Mar 2026 16:54:30 +0800 Subject: [PATCH] fix: prevent indefinite blocking in fetchWindowPreview pipe read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When KWin's screenshot service encounters an internal error, it may hold the write end of the pipe open without writing any data and without closing it. In this case, the previous blocking `QFile::read()` would wait forever, hanging the taskbar process. Three issues are fixed in fetchWindowPreview(): 1. Replace `pipe()` with `pipe2(fd, O_CLOEXEC)` to prevent the write fd from being inherited by child processes, which would also cause read() to block indefinitely. 2. Replace the blocking `QFile::read()` with a `poll()`-based read loop with a 5-second timeout. If KWin stalls or the pipe produces no data within the timeout, the function aborts gracefully and returns an empty QPixmap instead of hanging. 3. Fix the read buffer size calculation: use `imageHeight * imageStride` instead of `imageWidth * imageHeight * bpp / 8`. imageStride is the actual bytes-per-row (including alignment padding) as reported by KWin, and may be larger than width * bpp/8. The old calculation could result in reading too few bytes, leading to an out-of-bounds access when constructing QImage. Also added image.copy() to ensure the QImage owns its data independently of the fileContent buffer. 当 KWin 截图服务发生内部异常时,可能持有管道写端却不写入任何数据 也不关闭,导致原先的阻塞式 `QFile::read()` 永久等待,任务栏进程卡死。 本次修复了 fetchWindowPreview() 中的三个问题: 1. 将 `pipe()` 改为 `pipe2(fd, O_CLOEXEC)`,防止写端 fd 被 fork 出的子进程继承,否则子进程持有写端也会导致 read() 永久阻塞。 2. 将阻塞式 `QFile::read()` 替换为带 5 秒超时的 `poll()` 读取循环。 若 KWin 卡顿或管道在超时内无数据,函数会提前退出并返回空的 QPixmap,而不是使进程永久挂起。 3. 修正读取缓冲区大小计算:使用 `imageHeight * imageStride` 而非 `imageWidth * imageHeight * bpp / 8`。imageStride 是 KWin 返回的 每行实际字节数(含内存对齐 padding),可能大于 width*bpp/8。 原来的计算会导致读取字节数偏小,在构造 QImage 时发生越界访问。 同时添加 image.copy() 确保 QImage 独立持有数据。 Log: prevent indefinite blocking in fetchWindowPreview pipe read --- panels/dock/taskmanager/x11preview.cpp | 59 ++++++++++++++++++++------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/panels/dock/taskmanager/x11preview.cpp b/panels/dock/taskmanager/x11preview.cpp index 31ac8d495..34e494b7d 100644 --- a/panels/dock/taskmanager/x11preview.cpp +++ b/panels/dock/taskmanager/x11preview.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include @@ -73,7 +75,7 @@ QPixmap fetchWindowPreview(const uint32_t &winId) // pipe read write fd int fd[2]; - if (pipe(fd) < 0) { + if (pipe2(fd, O_CLOEXEC) < 0) { qDebug() << "failed to create pipe"; return QPixmap(); } @@ -113,20 +115,51 @@ QPixmap fetchWindowPreview(const uint32_t &winId) return QPixmap(); } - QFile file; - if (!file.open(fd[0], QIODevice::ReadOnly)) { - file.close(); - ::close(fd[0]); - return QPixmap(); - } - QImage::Format qimageFormat = static_cast(imageFormat); - int bitsCountPerPixel = QImage::toPixelFormat(qimageFormat).bitsPerPixel(); - QByteArray fileContent = file.read(imageHeight * imageWidth * bitsCountPerPixel / 8); - QImage image(reinterpret_cast(fileContent.data()), imageWidth, imageHeight, imageStride, qimageFormat); - // close read + // imageStride 是每行实际字节数(含内存对齐 padding),必须用它而非 width*bpp/8 + qsizetype expectedSize = static_cast(imageHeight) * imageStride; + QByteArray fileContent(expectedSize, Qt::Uninitialized); + qsizetype totalRead = 0; + + // 用 poll() + 超时保护替代阻塞的 QFile::read(): + // 若 KWin 内部异常(持有写端但不写也不关),read() 会永久阻塞; + // poll() 超时后直接放弃,避免主线程(此处为正常 UI 线程上下文)死锁。 + constexpr int kTimeoutMs = 5000; + while (totalRead < expectedSize) { + struct pollfd pfd{fd[0], POLLIN, 0}; + int ret = ::poll(&pfd, 1, kTimeoutMs); + if (ret == 0) { + qWarning(x11WindowPreview) << "fetchWindowPreview: pipe read timeout, KWin may have stalled"; + break; + } + if (ret < 0) { + qWarning(x11WindowPreview) << "fetchWindowPreview: poll error:" << strerror(errno); + break; + } + if (pfd.revents & (POLLERR | POLLNVAL)) { + qWarning(x11WindowPreview) << "fetchWindowPreview: pipe error, revents=" << pfd.revents; + break; + } + // POLLHUP(写端已关闭 EOF):继续读出剩余数据,下次 read 返回 0 退出 + ssize_t n = ::read(fd[0], fileContent.data() + totalRead, expectedSize - totalRead); + if (n <= 0) { + if (n < 0) + qWarning(x11WindowPreview) << "fetchWindowPreview: read error:" << strerror(errno); + break; // n==0: EOF + } + totalRead += n; + } + ::close(fd[0]); + + if (totalRead < expectedSize) { + qWarning(x11WindowPreview) << "fetchWindowPreview: incomplete data, got" << totalRead << "/ expected" << expectedSize; + return QPixmap(); + } + + QImage image(reinterpret_cast(fileContent.constData()), + imageWidth, imageHeight, imageStride, qimageFormat); auto pixmap = QPixmap::fromImage(image); if (!pixmap.isNull()) {