Skip to content

fix(security): harden ops log export via fd-based transfer and symlin… - #486

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
GongHeng2017:202607081012-master-fix
Jul 8, 2026
Merged

fix(security): harden ops log export via fd-based transfer and symlin…#486
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
GongHeng2017:202607081012-master-fix

Conversation

@GongHeng2017

Copy link
Copy Markdown
Contributor

…k-safe collection

Refactor the exportOpsLog flow to eliminate path exposure and symlink traversal risks:

  • Change exportOpsLog DBus API from returning a /tmp path to accepting a caller-provided file descriptor and streaming the zipped logs back over it (bool return), so the backend no longer exposes /var/log paths to callers and the frontend no longer needs a separate cleanup call.
  • Move user-permission log collection (per-user ~/.cache logs) out of the root backend into Utils::exportUserPermission* helpers in the application, while the backend OpsLogExport now only collects root-owned /var/log logs.
  • Create the backend temp dir under /var/log instead of /tmp, and clean it up with a TOCTOU-safe, fd-relative recursive removal (openat/fstatat/unlinkat with O_NOFOLLOW) instead of path-based deletion.
  • Skip symlink sources during copy and switch directory copy from "cp -rf" to "cp -rP" to preserve rather than follow symlinks.
  • Drop setDirectoryPermissionsSafe chown loop, no longer needed since ownership is not handed back to a non-root caller.
  • Add ReadWritePaths=/var/log to the systemd unit so the backend can create its temp export dir under the ProtectSystem=strict tree.
  • Update the D-Bus introspection XML for the new exportOpsLog signature.

Log: fix issue
Bug: https://pms.uniontech.com/bug-view-368003.html

…k-safe collection

Refactor the exportOpsLog flow to eliminate path exposure and symlink
traversal risks:

- Change exportOpsLog DBus API from returning a /tmp path to accepting a
  caller-provided file descriptor and streaming the zipped logs back over
  it (bool return), so the backend no longer exposes /var/log paths to
  callers and the frontend no longer needs a separate cleanup call.
- Move user-permission log collection (per-user ~/.cache logs) out of the
  root backend into Utils::exportUserPermission* helpers in the
  application, while the backend OpsLogExport now only collects root-owned
  /var/log logs.
- Create the backend temp dir under /var/log instead of /tmp, and clean it
  up with a TOCTOU-safe, fd-relative recursive removal
  (openat/fstatat/unlinkat with O_NOFOLLOW) instead of path-based deletion.
- Skip symlink sources during copy and switch directory copy from
  "cp -rf" to "cp -rP" to preserve rather than follow symlinks.
- Drop setDirectoryPermissionsSafe chown loop, no longer needed since
  ownership is not handed back to a non-root caller.
- Add ReadWritePaths=/var/log to the systemd unit so the backend can
  create its temp export dir under the ProtectSystem=strict tree.
- Update the D-Bus introspection XML for the new exportOpsLog signature.

Log: fix issue
Bug: https://pms.uniontech.com/bug-view-368003.html

@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.

Sorry @GongHeng2017, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
  • 检测到敏感词export变动
详情
    {
    "export": {
        "application/dbusproxy/dldbushandler.cpp": {
            "a": [
                "QString DLDBusHandler::exportOpsLog()",
                "    QDBusPendingReply<QString> reply = m_dbus->exportOpsLog();",
                "        qCWarning(logApp) << \"call dbus iterface 'exportOpsLog()' failed. error info:\" << reply.error().message();"
            ],
            "b": [
                "bool DLDBusHandler::exportOpsLog(const QString &zipFilePath)",
                "        qCritical() << \"exportOpsLog: failed to open zip file for writing:\" << zipFilePath",
                "        qCritical() << \"exportOpsLog: invalid file descriptor for:\" << zipFilePath;",
                "    QDBusPendingReply<bool> reply = m_dbus->exportOpsLog(dbusFd);",
                "        qCritical() << \"call dbus interface 'exportOpsLog' failed. error info:\" << reply.error().message();"
            ]
        },
        "application/dbusproxy/dldbushandler.h": {
            "a": [
                "    QString exportOpsLog();"
            ],
            "b": [
                "    bool exportOpsLog(const QString &zipFilePath);"
            ]
        },
        "application/dbusproxy/dldbusinterface.h": {
            "a": [
                "    inline QDBusPendingReply<QString> exportOpsLog()"
            ],
            "b": [
                "    inline QDBusPendingReply<bool> exportOpsLog(const QDBusUnixFileDescriptor &fd)"
            ]
        },
        "application/logallexportthread.cpp": {
            "a": [
                "        QString opsLogPath = DLDBusHandler::instance(this)->exportOpsLog();",
                "            Utils::exportSomeOpsLogs(opsLogPath, userHomePath);"
            ],
            "b": [
                "            emit exportFinsh(false);",
                "        bool opsOk = DLDBusHandler::instance(this)->exportOpsLog(opsZipPath);",
                "            qCCritical(logApp) << \"exportOpsLog failed or zip not produced\";",
                "            emit exportFinsh(false);",
                "        Utils::exportUserPermissionOpsLogs(tmpOpsDirPath, userHomePath);"
            ]
        },
        "application/opslogpaths.h": {
            "b": [
                "// \u524d\u7aef\uff08application/utils.cpp \u7684 exportUserPermissionOpsLogs \u7cfb\u5217\uff09\u4e0e",
                "// root \u670d\u52a1\uff08logViewerService/opslogexport.cpp \u7684 createOpsLogDirStruct\uff09\u5fc5\u987b"
            ]
        },
        "application/utils.cpp": {
            "a": [
                "void Utils::exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir)"
            ],
            "b": [
                "void Utils::exportUserPermissionAppLogs(const QString &outDir, const QString &userHomeDir)",
                "void Utils::exportUserPermissionSystemLogs(const QString &outDir, const QString &userHomeDir)",
                "void Utils::exportUserPermissionKernelLogs(const QString &outDir)",
                "void Utils::exportUserPermissionDDELogs(const QString &outDir, const QString &userHomeDir)",
                "void Utils::exportUserPermissionOpsLogs(const QString &outDir, const QString &userHomeDir)",
                "    Utils::exportUserPermissionAppLogs(outDir, userHomeDir);",
                "    Utils::exportUserPermissionSystemLogs(outDir, userHomeDir);",
                "    Utils::exportUserPermissionKernelLogs(outDir);",
                "    Utils::exportUserPermissionDDELogs(outDir, userHomeDir);"
            ]
        },
        "application/utils.h": {
            "a": [
                "    static void exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir);"
            ],
            "b": [
                "    static void exportUserPermissionAppLogs(const QString &outDir, const QString &userHomeDir);",
                "    static void exportUserPermissionSystemLogs(const QString &outDir, const QString &userHomeDir);",
                "    static void exportUserPermissionKernelLogs(const QString &outDir);",
                "    static void exportUserPermissionDDELogs(const QString &outDir, const QString &userHomeDir);",
                "    static void exportUserPermissionOpsLogs(const QString &outDir, const QString &userHomeDir);"
            ]
        },
        "logViewerService/assets/data/com.deepin.logviewer.xml": {
            "b": [
                "    <method name=\"exportOpsLog\">"
            ]
        },
        "logViewerService/assets/data/deepin-log-viewer-daemon.service": {
            "b": [
                "# exportOpsLog \u9700\u8981\u5728 /var/log \u4e0b\u521b\u5efa\u4e34\u65f6\u5bfc\u51fa\u76ee\u5f55\uff0c\u987b\u663e\u5f0f\u653e\u5f00\u5199\u6743\u9650"
            ]
        },
        "logViewerService/logviewerservice.cpp": {
            "a": [
                "QString LogViewerService::exportOpsLog()"
            ],
            "b": [
                "// \u57fa\u4e8e fd \u7684 TOCTOU \u5b89\u5168\u65b9\u5f0f\u5220\u9664 exportOpsLog \u4ea7\u751f\u7684 /var/log \u4e34\u65f6\u76ee\u5f55\uff08opsDir\uff09\u3002",
                "// \u7531 exportOpsLog \u5728\u5199\u5165 fd \u5b8c\u6210\u540e\u81ea\u52a8\u8c03\u7528\uff0c\u786e\u4fdd /var/log \u4e0b\u4e0d\u6b8b\u7559\u542b\u7cfb\u7edf\u65e5\u5fd7\u7684\u76ee\u5f55\u3002",
                "bool LogViewerService::exportOpsLog(const QDBusUnixFileDescriptor &fd)",
                "        qCWarning(logService) << \"exportOpsLog: invalid file descriptor from caller\";",
                "        qCWarning(logService) << \"exportOpsLog: failed to create temporary dir under /var/log:\" << tmpOpsDir.errorString();",
                "        qCWarning(logService) << \"exportOpsLog: failed to create log collect dir:\" << logCollectDir;",
                "        qCWarning(logService) << \"exportOpsLog: zip failed, exitCode:\" << zipProc.exitCode()",
                "            qCWarning(logService) << \"exportOpsLog: failed to open temp zip for reading:\" << tmpZipPath;",
                "                qCWarning(logService) << \"exportOpsLog: failed to open caller fd for writing\";",
                "                // \u4e0e\u672c\u6587\u4ef6 exportLog() \u4e2d 1 MiB \u7f13\u51b2\u533a\u7684\u5904\u7406\u65b9\u5f0f\u4fdd\u6301\u4e00\u81f4\u3002",
                "                    qCWarning(logService) << \"exportOpsLog: write to caller fd failed\";",
                "        qCWarning(logService) << \"exportOpsLog: aborted, failed to write zip to caller fd\";",
                "    qCDebug(logService) << \"exportOpsLog: ops logs zipped and written to caller fd successfully\";"
            ]
        },
        "logViewerService/logviewerservice.h": {
            "a": [
                "    Q_SCRIPTABLE QString exportOpsLog();"
            ],
            "b": [
                "    Q_SCRIPTABLE bool exportOpsLog(const QDBusUnixFileDescriptor &fd);",
                "    // \u57fa\u4e8e fd \u7684 TOCTOU \u5b89\u5168\u65b9\u5f0f\u5220\u9664 exportOpsLog \u4ea7\u751f\u7684 /var/log \u4e34\u65f6\u76ee\u5f55\uff08opsDir\uff09\u3002",
                "    // \u7531 exportOpsLog \u5199\u5165 fd \u5b8c\u6210\u540e\u81ea\u52a8\u8c03\u7528\u56de\u6536\uff0c\u6210\u529f\u8fd4\u56de true\u3002"
            ]
        },
        "logViewerService/opslogexport.cpp": {
            "a": [
                "            qWarning() << \"Failed to chown export path:\" << path << \"error:\" << strerror(errno);"
            ]
        }
    }
}

@github-actions
github-actions Bot requested a review from liujianqiang-niu July 8, 2026 02:18
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:95分

■ 【总体评价】

代码重构了运维日志导出流程,彻底消除了TOCTOU竞态和符号链接跟随等严重安全漏洞
逻辑基本正确但存在解压返回值未校验的轻微瑕疵,安全设计优秀

■ 【详细分析】

  • 1.语法逻辑(基本正确)✓

代码整体逻辑严密,前后端交互通过fd传递替代了路径传递,后端清理逻辑使用fd-relative系统调用保证原子性。但在LogAllExportThread::run中调用Utils::executeCmd解压后端生成的压缩包时,未检查返回值,若解压失败会导致空目录被打入最终zip包,造成导出数据不完整
潜在问题:解压失败时缺乏中断机制,导致静默生成不完整的导出文件
建议:校验executeCmd的返回值,若解压失败则中断导出并报错

  • 2.代码质量(良好)✓

代码结构优化显著,提取了opslogpaths.h作为前后端共享的目录结构常量,避免了硬编码不一致。将原有冗长的exportSomeOpsLogs拆分为exportUserPermissionAppLogs等职责单一的函数,提升了可读性和可维护性。注释详尽,清晰解释了安全设计意图
潜在问题:无
建议:无

  • 3.代码性能(高效)✓

后端写入fd时采用1MiB堆缓冲区进行分块拷贝,平衡了内存占用与IO效率。为processCmdWithArgs引入300秒超时机制,有效防止了因特殊文件或异常进程导致的无限阻塞。safeRemoveDirRecursive预先收集目录项再删除,避免了迭代器失效带来的重试开销
潜在问题:无
建议:无

  • 4.代码安全(存在0个安全漏洞)✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 4 个,持平 0 个
本次重构彻底修复了原有的路径暴露、TOCTOU竞态、符号链接跟随和临时目录残留漏洞。后端采用openat/fstatat/unlinkat配合O_NOFOLLOW实现TOCTOU安全的目录清理;前端与后端均增加了符号链接跳过逻辑,并使用cp -rP防止跟随链接;通过D-Bus传递fd替代路径,消除了权限泄露面

  • 建议:继续保持当前的安全编码标准,在后续涉及文件系统操作的功能中复用safeRemoveDirRecursive和safeCpSkipSymlinks模式

■ 【改进建议代码示例】

// application/logallexportthread.cpp
// 增加对 unzip 解压结果的校验,防止解压失败时将空目录打入最终压缩包
QByteArray unzipOutput = Utils::executeCmd("unzip", QStringList() << "-n" << opsZipPath << "-d" << tmpOpsDirPath);
if (unzipOutput.isEmpty()) {
    qCCritical(logApp) << "unzip failed or produced no output for:" << opsZipPath;
    zipClose(m_zipFile, nullptr);
    QFile::remove(m_outfile);
    emit exportFinsh(false);
    return;
}
// 及时清理 opsZipPath 压缩包,避免将该压缩包也导出给用户
QFile::remove(opsZipPath);

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: GongHeng2017, 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

@GongHeng2017

Copy link
Copy Markdown
Contributor Author

/forcemerge

@deepin-bot

deepin-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This pr force merged! (status: unstable)

@deepin-bot
deepin-bot Bot merged commit 80a47da into linuxdeepin:master Jul 8, 2026
22 of 23 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