From 60e1fe7525ab61a1448337a99070acbd1f2c246b Mon Sep 17 00:00:00 2001 From: gongheng Date: Thu, 2 Jul 2026 21:25:13 +0800 Subject: [PATCH] fix(opslogexport): stop root service writing to user home, export via /var/log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root service no longer writes into the caller's home dir; it creates a random temp dir under /var/log, exports system logs there, and returns the path. The frontend collects user-permission logs itself and copies the root-exported logs out. cp -rP avoids following symlinks, target dir is validated under /var/log, perms tightened to 0755/0644. root 服务不再写入用户家目录,改为在 /var/log 下创建随机临时目录导出系统 日志并返回路径;前端自行收集用户权限日志并拷贝 root 导出的内容。cp -rP 不跟随符号链接,校验目标目录位于 /var/log,权限收紧为 0755/0644。 Log: 重构运维日志导出,root 不再写入用户家目录 Influence: 消除 root 服务写入用户家目录的符号链接攻击面,改由 root 在 /var/log 随机目录导出、前端拷贝,提升安全性。 --- application/dbusproxy/dldbushandler.cpp | 22 +- application/dbusproxy/dldbushandler.h | 3 +- application/dbusproxy/dldbusinterface.h | 8 +- application/logallexportthread.cpp | 19 +- application/opslogpaths.h | 73 ++++ application/utils.cpp | 319 +++++++++++++--- application/utils.h | 17 +- .../assets/data/com.deepin.logviewer.xml | 6 + .../data/deepin-log-viewer-daemon.service | 2 + logViewerService/logviewerservice.cpp | 179 ++++++++- logViewerService/logviewerservice.h | 17 +- logViewerService/opslogexport.cpp | 347 +++++++++--------- logViewerService/opslogexport.h | 15 +- 13 files changed, 757 insertions(+), 270 deletions(-) create mode 100644 application/opslogpaths.h diff --git a/application/dbusproxy/dldbushandler.cpp b/application/dbusproxy/dldbushandler.cpp index 457d1ca9..11cab3ba 100644 --- a/application/dbusproxy/dldbushandler.cpp +++ b/application/dbusproxy/dldbushandler.cpp @@ -170,18 +170,34 @@ bool DLDBusHandler::exportLog(const QString &outDir, const QString &in, bool isF return m_dbus->exportLog(outDir, in, isFile); } -bool DLDBusHandler::exportOpsLog() +QString DLDBusHandler::exportOpsLog() { m_dbus->setTimeout(1200000); - QDBusPendingReply reply = m_dbus->exportOpsLog(); + QDBusPendingReply reply = m_dbus->exportOpsLog(); reply.waitForFinished(); m_dbus->setTimeout(-1); if (reply.isError()) { qCritical(logDBusHandler) << "call dbus interface 'exportOpsLog' failed. error info:" << reply.error().message(); + return QString(); + } else { + qCDebug(logDBusHandler) << "exportOpsLog succeeded, root temp dir:" << reply.value(); + return reply.value(); + } +} + +bool DLDBusHandler::removeOpsLogTempDir() +{ + m_dbus->setTimeout(60000); + QDBusPendingReply reply = m_dbus->removeOpsLogTempDir(); + reply.waitForFinished(); + m_dbus->setTimeout(-1); + + if (reply.isError()) { + qCritical(logDBusHandler) << "call dbus interface 'removeOpsLogTempDir' failed. error info:" << reply.error().message(); return false; } else { - qCDebug(logDBusHandler) << "exportOpsLog succeeded:" << reply.value(); + qCDebug(logDBusHandler) << "removeOpsLogTempDir succeeded, result:" << reply.value(); return reply.value(); } } diff --git a/application/dbusproxy/dldbushandler.h b/application/dbusproxy/dldbushandler.h index a50536de..3dc1b8aa 100644 --- a/application/dbusproxy/dldbushandler.h +++ b/application/dbusproxy/dldbushandler.h @@ -22,7 +22,8 @@ class DLDBusHandler : public QObject int exitCode(); void quit(); bool exportLog(const QString &outDir, const QString &in, bool isFile); - bool exportOpsLog(); + QString exportOpsLog(); + bool removeOpsLogTempDir(); bool isFileExist(const QString &filePath); quint64 getFileSize(const QString &filePath); qint64 getLineCount(const QString &filePath); diff --git a/application/dbusproxy/dldbusinterface.h b/application/dbusproxy/dldbusinterface.h index f8ea6f84..8e8d2ac5 100644 --- a/application/dbusproxy/dldbusinterface.h +++ b/application/dbusproxy/dldbusinterface.h @@ -55,12 +55,18 @@ public Q_SLOTS: // METHODS return asyncCallWithArgumentList(QStringLiteral("exportLog"), argumentList); } - inline QDBusPendingReply exportOpsLog() + inline QDBusPendingReply exportOpsLog() { QList argumentList; return asyncCallWithArgumentList(QStringLiteral("exportOpsLog"), argumentList); } + inline QDBusPendingReply removeOpsLogTempDir() + { + QList argumentList; + return asyncCallWithArgumentList(QStringLiteral("removeOpsLogTempDir"), argumentList); + } + inline QDBusPendingReply getFileInfo(const QString &file, bool unzip) { QList argumentList; diff --git a/application/logallexportthread.cpp b/application/logallexportthread.cpp index 05e5174f..d21fb13a 100644 --- a/application/logallexportthread.cpp +++ b/application/logallexportthread.cpp @@ -232,12 +232,23 @@ void LogAllExportThread::run() // 不支持导出 sudo 权限的日志,且导出日志功能主要面向普通用户使用场景, // 因此当获取到的用户家目录为根目录时,认为是异常情况,不执行导出操作 if (!m_cancel && QDir::homePath() != "/" && QDir::homePath() != "/root") { - QString opsLogPath =tmpPath + "log-ops/"; + QString opsLogPath = tmpPath + "log-ops/"; Utils::mkMutiDir(opsLogPath); QString userHomePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - // 收集运维日志 - DLDBusHandler::instance(this)->exportOpsLog(); - Utils::exportSomeOpsLogs(opsLogPath, userHomePath); + // 收集运维日志:root 服务在 /var/log 下创建随机临时目录导出日志,并返回该目录路径 + QString rootTempDir = DLDBusHandler::instance(this)->exportOpsLog(); + if (!rootTempDir.isEmpty() && QDir(rootTempDir).exists()) { + // 将 root 导出的日志从 /var/log 临时目录拷贝到前端的 opsLogPath。 + // -rP:递归拷贝且不跟随符号链接,与 root 侧 copy_file_or_dir 的 cp -rP 保持一致, + // 避免临时目录内符号链接指向 FIFO/特殊文件导致前端进程阻塞或读到非预期内容。 + Utils::executeCmd("cp", QStringList() << "-rP" << rootTempDir + "/." << opsLogPath); + // 拷贝完成后及时清理 /var/log 下的 root 临时目录,避免遗留含系统日志的目录。 + // 无参:服务端清理自身缓存的本次导出路径,杜绝路径替换风险。 + DLDBusHandler::instance(this)->removeOpsLogTempDir(); + } else { + qCWarning(logExportAll) << "exportOpsLog returned empty or non-existent root temp dir, root-side ops logs may be missing"; + } + Utils::exportUserPermissionOpsLogs(opsLogPath, userHomePath); } if (!m_cancel) { diff --git a/application/opslogpaths.h b/application/opslogpaths.h new file mode 100644 index 00000000..dd877f24 --- /dev/null +++ b/application/opslogpaths.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef OPSLOGPATHS_H +#define OPSLOGPATHS_H + +// 运维日志导出的目录结构常量。 +// +// 前端(application/utils.cpp 的 exportUserPermissionOpsLogs 系列)与 +// root 服务(logViewerService/opslogexport.cpp 的 createOpsLogDirStruct)必须 +// 使用同一份目录结构,避免前后端导出路径不一致,故提取为公共头。 +// +// 注意:application 目标以 -std=c++11 编译,故使用 static constexpr(非 C++17 +// 的 inline constexpr 变量),命名空间作用域下内部链接,多 TU 包含无 ODR 问题。 + +static constexpr char kKernelPath[] { "/kernel" }; +static constexpr char kSystemPath[] { "/system" }; +static constexpr char kDDEPath[] { "/dde" }; +static constexpr char kAppPath[] { "/app" }; +static constexpr char kJournalPath[] { "/journal" }; +static constexpr char kAptPath[] { "/apt" }; +static constexpr char kUosStePath[] { "/uos-ste" }; +static constexpr char kUossteLogsPath[] { "/uosste_logs" }; +static constexpr char kHisiPath[] { "/hisi" }; +static constexpr char kDefenderPath[] { "/app/deepin-defender" }; +static constexpr char kCloudPrintPath[] { "/app/deepin-cloud-print" }; +static constexpr char kCloudScanPath[] { "/app/deepin-cloud-scan" }; +static constexpr char kPrinterPath[] { "/app/dde-printer" }; +static constexpr char kGraphicsDriverManagerPath[] { "/app/deepin-graphics-driver-manager" }; +static constexpr char kBootMakerPath[] { "/app/deepin-boot-maker" }; +static constexpr char kScanerPath[] { "/app/deepin-scaner" }; +static constexpr char kKMSPath[] { "/app/kms" }; +static constexpr char kCompressorPath[] { "/app/deepin-compressor" }; +static constexpr char kCalendarPath[] { "/app/dde-calendar" }; +static constexpr char kManualPath[] { "/app/deepin-manual" }; +static constexpr char kReaderPath[] { "/app/deepin-reader" }; +static constexpr char kFontManagerPath[] { "/app/deepin-font-manager" }; +static constexpr char kDebInstallerPath[] { "/app/deepin-deb-installer" }; +static constexpr char kTerminalPath[] { "/app/deepin-terminal" }; +static constexpr char kVoiceNotPath[] { "/app/deepin-voice-note" }; +static constexpr char kDevicemanagerPath[] { "/app/deepin-devicemanager" }; +static constexpr char kServiceSupportPath[] { "/app/uos-service-support" }; +static constexpr char kRemoteAssistancePath[] { "/app/uos-remote-assistance" }; +static constexpr char kSystemMonitorPath[] { "/app/deepin-system-monitor" }; +static constexpr char kEditorPath[] { "/app/deepin-editor" }; +static constexpr char kCalculatorPath[] { "/app/deepin-calculator" }; +static constexpr char kMailPath[] { "/app/deepin-mail" }; +static constexpr char kScreenRecorderPath[] { "/app/deepin-screen-recorder" }; +static constexpr char kDrawPath[] { "/app/deepin-draw" }; +static constexpr char kMusicPath[] { "/app/deepin-music" }; +static constexpr char kImageViewerPath[] { "/app/deepin-image-viewer" }; +static constexpr char kAlbumPath[] { "/app/deepin-album" }; +static constexpr char kMoviePath[] { "/app/deepin-movie" }; +static constexpr char kCameraPath[] { "/app/deepin-camera" }; +static constexpr char kChineseImePath[] { "/app/chineseime" }; +static constexpr char kDeepinInstallerPath[] { "/app/deepin-installer" }; +static constexpr char kDeepinRecoveryPath[] { "/app/deepin-recovery" }; +static constexpr char kOemCustomPath[] { "/app/oem-custom" }; +static constexpr char kUosActivatorPath[] { "/app/uos-activator" }; +static constexpr char kUosActivatorLogPath[] { "/app/uos-activator/log" }; +static constexpr char kFcitxPath[] { "/app/fcitx" }; +static constexpr char kDiskManagerPath[] { "/app/deepin-diskmanager" }; +static constexpr char kDownloaderPath[] { "/app/downloader" }; +static constexpr char kKwinPath[] { "/app/kwin" }; +static constexpr char kKboxPath[] { "/app/kbox" }; +static constexpr char kDeepinLogViewerPath[] { "/app/deepin-log-viewer" }; +static constexpr char kDdeDesktopPath[] { "/dde/dde-desktop" }; +static constexpr char kDdeFileManagerPath[] { "/dde/dde-file-manager" }; +static constexpr char kDdeDockPath[] { "/dde/dde-dock" }; +static constexpr char kSystemPulseaudioPath[] { "/system/pulseaudio" }; + +#endif // OPSLOGPATHS_H diff --git a/application/utils.cpp b/application/utils.cpp index bbd677b0..ac163178 100644 --- a/application/utils.cpp +++ b/application/utils.cpp @@ -6,6 +6,7 @@ #include "logsettings.h" #include "dbusmanager.h" #include "dbusproxy/dldbushandler.h" +#include "opslogpaths.h" #include #include @@ -558,46 +559,8 @@ void Utils::updateRepeatCoredumpExePaths(const QList & file.close(); } -static QByteArray processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args) -{ - QProcess process; - if (!workPath.isEmpty()) - process.setWorkingDirectory(workPath); - - process.setProgram(cmdStr); - process.setArguments(args); - process.setEnvironment({"LANG=en_US.UTF-8", "LANGUAGE=en_US"}); - process.start(); - // Wait for process to finish without timeout. - process.waitForFinished(-1); - QByteArray outPut = process.readAllStandardOutput(); - int nExitCode = process.exitCode(); - bool bRet = (process.exitStatus() == QProcess::NormalExit && nExitCode == 0); - if (!bRet) { - qDebug() << "run cmd error, caused by:" << process.errorString() << "output:" << outPut; - return QByteArray(); - } - return outPut; -} - -QByteArray Utils::executeCmd(const QString &cmdStr, const QStringList &args, const QString &workPath) -{ - return processCmdWithArgs(cmdStr, workPath, args); -} - -static void appendToFile(const QString &filePath, const QByteArray &content) -{ - QFile file(filePath); - - if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { - file.write(content); - } else { - qWarning() << "Error opening file for writing:" << file.errorString(); - } -} - // 查找所有物理网络接口 -QStringList getPhysicalInterfaces() +QStringList Utils::getPhysicalInterfaces() { QStringList physicalInterfaces; QDir netDir("/sys/class/net/"); @@ -622,7 +585,7 @@ QStringList getPhysicalInterfaces() return physicalInterfaces; } -static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcard) +QStringList Utils::expandPathWithWildcardIterator(const QString &pathWithWildcard) { // 1. 分离目录和文件名模式 QFileInfo fileInfo(pathWithWildcard); @@ -646,31 +609,283 @@ static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcar return matchedFiles; } -void Utils::exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir) +void Utils::appendToFile(const QString &filePath, const QByteArray &content) { - Q_UNUSED(userHomeDir) + QFile file(filePath); - std::string tmpCmd; + if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + file.write(content); + } else { + qWarning() << "Error opening file for writing:" << file.errorString(); + } +} - // app +QByteArray Utils::processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args) +{ + QProcess process; + if (!workPath.isEmpty()) + process.setWorkingDirectory(workPath); + + process.setProgram(cmdStr); + process.setArguments(args); + process.setEnvironment({"LANG=en_US.UTF-8", "LANGUAGE=en_US"}); + process.start(); + // 设定超时上限,防止因特殊文件(如 /dev/zero、无写端的 FIFO)或异常子进程导致 + // 无限阻塞。超时后主动 kill 并回收,避免僵尸进程与导出线程被永久卡死(拒绝服务)。 + static constexpr int kCmdTimeoutMs = 300000; + if (!process.waitForFinished(kCmdTimeoutMs)) { + qWarning() << "cmd timed out after" << kCmdTimeoutMs << "ms, killing:" << cmdStr << args; + process.kill(); + process.waitForFinished(3000); + return QByteArray(); + } + QByteArray outPut = process.readAllStandardOutput(); + int nExitCode = process.exitCode(); + bool bRet = (process.exitStatus() == QProcess::NormalExit && nExitCode == 0); + if (!bRet) { + qWarning() << "run cmd error, caused by:" << process.errorString() << "output:" << outPut; + return QByteArray(); + } + return outPut; +} + +QByteArray Utils::executeCmd(const QString &cmdStr, const QStringList &args, const QString &workPath) +{ + return processCmdWithArgs(cmdStr, workPath, args); +} + +// 安全拷贝辅助:对齐 root 侧 opslogexport.cpp 的符号链接防护策略。 +// 前端以普通用户权限收集家目录日志时,恶意用户可在对应路径下预埋指向 +// /dev/zero 或命名管道(FIFO)的符号链接。默认 cp 会跟随符号链接读取目标, +// 叠加 processCmdWithArgs 的超时机制前曾为无限等待,可永久阻塞导出线程(拒绝服务)。 +// 防护两层: +// 1. 拷贝前用 QFileInfo::isSymLink() 过滤源路径,符号链接源直接跳过; +// 2. 强制传入 -P,使递归拷贝目录时也不跟随树内符号链接(复制链接本身)。 +// sources 为源路径列表,dest 为目标路径,recursive 控制是否递归拷贝目录。 +static void safeCpSkipSymlinks(const QStringList &sources, const QString &dest, bool recursive) +{ + QStringList args; + args << "-P"; // 不跟随源参数自身及递归树内的符号链接 + if (recursive) + args << "-r"; + for (const QString &src : sources) { + if (QFileInfo(src).isSymLink()) { + qCWarning(logUtils) << "skip symlink source to avoid DoS:" << src; + continue; + } + args << src; + } + args << dest; + Utils::executeCmd("cp", args); +} + +void Utils::exportUserPermissionAppLogs(const QString &outDir, const QString &userHomeDir) +{ + // 安全中心 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-defender/deepin-defender.log", + userHomeDir + "/.cache/deepin/deepin-defender-daemon/deepin-defender-daemon.log", + userHomeDir + "/.cache/deepin/deepin-defender-datainterface/deepin-defender-datainterface.log"}, + outDir + kDefenderPath, false); + + // 云打印 + safeCpSkipSymlinks({userHomeDir + "/.cache/uniontech/deepin-cloud-print/deepin-cloud-print.log", + userHomeDir + "/.cache/uniontech/deepin-cloud-print-configurator/deepin-cloud-print-configurator.log"}, + outDir + kCloudPrintPath, false); + + // 云扫描 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-cloud-scan/deepin-cloud-scan.log"}, + outDir + kCloudScanPath, false); + + // 打印管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/dde-printer/dde-printer.log"}, + outDir + kPrinterPath, false); + + // 显卡驱动管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-graphics-driver-manager/deepin-graphics-driver-manager.log"}, + outDir + kGraphicsDriverManagerPath, false); + + // 启动盘制作工具 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-boot-maker/deepin-boot-maker.log"}, + outDir + kBootMakerPath, false); + + // 扫描管理 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/org.deepin.scanner/org.deepin.scanner"}, + outDir + kScanerPath, true); + + // KMS项目 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/kmsclient", + userHomeDir + "/.cache/deepin/kmstools"}, + outDir + kKMSPath, true); + + // 归档管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-compressor/deepin-compressor.log"}, + outDir + kCompressorPath, false); + + // 日历 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/dde-calendar-service/dde-calendar-service.log", + userHomeDir + "/.cache/deepin/dde-calendar/dde-calendar.log"}, + outDir + kCalendarPath, false); + + // 帮助手册 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-manual/deepin-manual.log"}, + outDir + kManualPath, false); + + // 文档查看器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-reader/deepin-reader.log"}, + outDir + kReaderPath, false); + + // 字体管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-font-manager/deepin-font-manager.log"}, + outDir + kFontManagerPath, false); + + // 软件包安装器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-deb-installer/deepin-deb-installer.log"}, + outDir + kDebInstallerPath, false); + + // 终端 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-terminal/deepin-terminal.log"}, + outDir + kTerminalPath, false); + + // 语音记事本 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-voice-note/deepin-voice-note.log"}, + outDir + kVoiceNotPath, false); + + // 设备管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-devicemanager/deepin-devicemanager.log"}, + outDir + kDevicemanagerPath, false); + + // 服务与支持 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/uos-service-support/uos-service-support.log"}, + outDir + kServiceSupportPath, false); + + // 远程协助 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/uos-remote-assistance/uos-remote-assistance.log"}, + outDir + kRemoteAssistancePath, false); + + // 系统监视器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-system-monitor/deepin-system-monitor.log"}, + outDir + kSystemMonitorPath, false); + + // 文本编辑器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-editor/deepin-editor.log"}, + outDir + kEditorPath, false); + + // 计算器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-calculator/deepin-calculator.log"}, + outDir + kCalculatorPath, false); + + // 邮箱 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-mail/deepin-mail.log"}, + outDir + kMailPath, false); + + // 截图录屏 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-screen-recorder/deepin-screen-recorder.log"}, + outDir + kScreenRecorderPath, false); + + // 画板 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-draw/deepin-draw.log"}, + outDir + kDrawPath, false); + + // 音乐 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-music/deepin-music.log"}, + outDir + kMusicPath, false); + + // 看图 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-image-viewer/deepin-image-viewer.log"}, + outDir + kImageViewerPath, false); + + // 相册 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-album/deepin-album.log"}, + outDir + kAlbumPath, false); + + // 影院 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-movie/deepin-movie.log"}, + outDir + kMoviePath, false); + + // 相机 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-camera/deepin-camera.log"}, + outDir + kCameraPath, false); + + // 中文输入法 + safeCpSkipSymlinks({userHomeDir + "/.cache/org.deepin.chineseime/ime/chineseime-qimpanel.log", + userHomeDir + "/.cache/org.deepin.chineseime/ime/fcitx-iflyime.log", + userHomeDir + "/.cache/org.deepin.chineseime/ime/ossp.log"}, + outDir + kChineseImePath, false); + + // 授权管理客户端 + safeCpSkipSymlinks({userHomeDir + "/.cache/uos/uos-activator", + userHomeDir + "/.cache/uos/uos-activator-cmd", + userHomeDir + "/.cache/uos-agent/uos-license-agent", + userHomeDir + "/.cache/uos-agent/uos-activator-kms"}, + outDir + kUosActivatorPath, true); + + // 输入法配置 + safeCpSkipSymlinks(expandPathWithWildcardIterator("/tmp/fcitx*.log"), + outDir + "/app/fcitx/", true); + + // 下载器 + safeCpSkipSymlinks({userHomeDir + "/.config/uos/downloader/Log"}, + outDir + kDownloaderPath, true); + + // 窗口管理器 appendToFile(outDir + "/app/kwin/glxinfo.log", executeCmd("glxinfo", { "-display", qEnvironmentVariable("DISPLAY"), "-B" })); - QStringList args = { "-rf" }; - args.append(expandPathWithWildcardIterator("/tmp/fcitx*.log")); - args.append(outDir + "/app/fcitx/"); - executeCmd("cp", args); + appendToFile(outDir + "/app/kwin/kwin_info.log", executeCmd("apt", { "policy", "kwin-x11", "dde-kwin" })); - args.clear(); - args << "policy" << "kwin-x11" << "dde-kwin"; - appendToFile(outDir + "/app/kwin/kwin_info.log", executeCmd("apt", args)); + // 安卓容器 + safeCpSkipSymlinks({userHomeDir + "/log/AospLog.log", + userHomeDir + "/log/KboxServer.log"}, + outDir + kKboxPath, false); - // kernel + // 日志收集工具 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/deepin-log-viewer/deepin-log-viewer.log"}, + outDir + kDeepinLogViewerPath, false); +} + +void Utils::exportUserPermissionSystemLogs(const QString &outDir, const QString &userHomeDir) +{ + // pulse audio /home/uos/pulse.log + safeCpSkipSymlinks({userHomeDir + "/pulse.log"}, outDir + kSystemPulseaudioPath, false); +} + +void Utils::exportUserPermissionKernelLogs(const QString &outDir) +{ + // ⽆法识别声卡问题⽇志 appendToFile(outDir + "/kernel/aplay.log", executeCmd("aplay", { "-l" })); + for (const QString &iface : getPhysicalInterfaces()) { appendToFile(outDir + "/kernel/eth_info.log", executeCmd("ethtool", { "-i", iface })); } + appendToFile(outDir + "/kernel/ifconfig.log", executeCmd("ifconfig", { })); +} + +void Utils::exportUserPermissionDDELogs(const QString &outDir, const QString &userHomeDir) +{ + // 文件管理器 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/dde-desktop/dde-desktop.log"}, + outDir + kDdeDesktopPath, false); + + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/dde-file-manager/dde-file-manager.log"}, + outDir + kDdeFileManagerPath, false); + + // 任务栏 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/dde-dock/dde-dock.log"}, + outDir + kDdeDockPath, false); + + //DDE + safeCpSkipSymlinks({userHomeDir + "/Desktop/DDE_LOG.zip"}, + outDir + kDDEPath, false); +} + +void Utils::exportUserPermissionOpsLogs(const QString &outDir, const QString &userHomeDir) +{ + Utils::exportUserPermissionAppLogs(outDir, userHomeDir); + Utils::exportUserPermissionSystemLogs(outDir, userHomeDir); + Utils::exportUserPermissionKernelLogs(outDir); + Utils::exportUserPermissionDDELogs(outDir, userHomeDir); // deb version + QStringList args; args.clear(); args << "-l"; appendToFile(outDir + "/deb-version.txt", executeCmd("dpkg", args)); diff --git a/application/utils.h b/application/utils.h index e1ab7b06..8e26752c 100755 --- a/application/utils.h +++ b/application/utils.h @@ -84,11 +84,24 @@ class Utils : public QObject static QStringList getRepeatCoredumpExePaths(); // 更新高频重复崩溃记录exe路径到文件 static void updateRepeatCoredumpExePaths(const QList &infos = QList()); + // 查找所有物理网络接口 + static QStringList getPhysicalInterfaces(); + static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcard); // 执行cmd命令 static QByteArray executeCmd(const QString& cmd, const QStringList& args = QStringList(), const QString& workPath = QString()); + static QByteArray processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args); + static void appendToFile(const QString &filePath, const QByteArray &content); + // 用户权限的APP日志收集 + static void exportUserPermissionAppLogs(const QString &outDir, const QString &userHomeDir); + // 用户权限的系统日志收集 + static void exportUserPermissionSystemLogs(const QString &outDir, const QString &userHomeDir); + // 用户权限的内核日志收集 + static void exportUserPermissionKernelLogs(const QString &outDir); + // 用户权限的DDE日志收集 + static void exportUserPermissionDDELogs(const QString &outDir, const QString &userHomeDir); + // 用户权限的运维日志收集 + static void exportUserPermissionOpsLogs(const QString &outDir, const QString &userHomeDir); - // 部分运维日志收集 - static void exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir); /** * @brief specialComType 是否是特殊机型,like huawei diff --git a/logViewerService/assets/data/com.deepin.logviewer.xml b/logViewerService/assets/data/com.deepin.logviewer.xml index 9eeae1e3..e0c2f944 100755 --- a/logViewerService/assets/data/com.deepin.logviewer.xml +++ b/logViewerService/assets/data/com.deepin.logviewer.xml @@ -22,6 +22,12 @@ + + + + + + diff --git a/logViewerService/assets/data/deepin-log-viewer-daemon.service b/logViewerService/assets/data/deepin-log-viewer-daemon.service index 7ce97a62..af2a5bb0 100644 --- a/logViewerService/assets/data/deepin-log-viewer-daemon.service +++ b/logViewerService/assets/data/deepin-log-viewer-daemon.service @@ -13,6 +13,8 @@ MemoryLimit=8G # 非root有阻塞,deepin-daemon启动后不能通过/proc/pid/exe获取启动进程全路径,下一阶段再按deepin-dameon启动 #User=deepin-daemon ProtectSystem=strict +# exportOpsLog 需要在 /var/log 下创建临时导出目录,须显式放开写权限 +ReadWritePaths=/var/log InaccessiblePaths=-/etc/shadow InaccessiblePaths=-/etc/NetworkManager/system-connections diff --git a/logViewerService/logviewerservice.cpp b/logViewerService/logviewerservice.cpp index 077ba8bb..eeb0d98c 100644 --- a/logViewerService/logviewerservice.cpp +++ b/logViewerService/logviewerservice.cpp @@ -7,6 +7,11 @@ #include #include +#include +#include +#include +#include +#include #include #include @@ -32,6 +37,8 @@ using namespace PolkitQt1; #include #include #include +#include +#include #ifdef QT_DEBUG Q_LOGGING_CATEGORY(logService, "org.deepin.log.viewer.service") @@ -1031,49 +1038,181 @@ bool LogViewerService::exportLog(const QString &outDir, const QString &in, bool return true; } -bool LogViewerService::exportOpsLog() +QString LogViewerService::exportOpsLog() { if(!checkAuth(s_Action_View)) { //非法调用 qCWarning(logService) << "Invalid authorization for export log"; - return false; + return QString(); } - QString callerHomeDir = getCallerHomeDir(); - if (callerHomeDir.isEmpty()) { - qCWarning(logService) << "Failed to get caller home directory for export log"; - return false; + QString callerHomeDir; + uint callerGid = 0; + if (!getCallerHomeAndGid(callerHomeDir, callerGid)) { + qCWarning(logService) << "Failed to get caller identity for export log"; + return QString(); } // 不支持导出 sudo 权限的日志,且导出日志功能主要面向普通用户使用场景, // 因此当获取到的用户家目录为根目录时,认为是异常情况,不执行导出操作 if (callerHomeDir == "/" || callerHomeDir == "/root") { qCWarning(logService) << "Invalid caller home directory for export log: " << callerHomeDir; - return false; + return QString(); } - QString newOutDir = callerHomeDir + "/.local/share/" - + kOrgNameForDeepinLogViewer + "/" - + kAppNameForDeepinLogViewer + "/tmp/log-ops/"; + // 安全改进:root 服务不再写入用户家目录(避免符号链接攻击面), + // 改为在 /var/log 下创建随机临时目录,由 root 完全控制。 + // 导出完成后将路径返回给前端,由前端以普通用户身份拷贝到自己的临时目录。 + // 临时目录的即时清理由前端拷贝完成后调用 removeOpsLogTempDir() 完成。 + + QTemporaryDir tmpOpsDir("/var/log/deepin-log-viewer-ops-log.XXXXXX"); + tmpOpsDir.setAutoRemove(false); + if (!tmpOpsDir.isValid()) { + qCWarning(logService) << "exportOpsLog: failed to create temporary dir under /var/log:" << tmpOpsDir.errorString(); + return QString(); + } - OpsLogExport ops(newOutDir.toStdString(), callerHomeDir.toStdString()); + const QString opsDir = tmpOpsDir.path(); + // 按调用方 D-Bus unique bus name 隔离缓存本次创建的临时目录路径, + // 供 removeOpsLogTempDir() 按当前调用者身份清理。若同一调用方存在未清理的旧目录, + // 先兜底清理,避免旧目录因被覆盖而泄露。 + const QString callerBusName = message().service(); + if (m_opsTempDirs.contains(callerBusName)) { + removeOpsTempDirByPath(m_opsTempDirs.value(callerBusName)); + } + m_opsTempDirs.insert(callerBusName, opsDir); + // 临时目录离开作用域后不会被自动删除(AutoRemove=false),路径交由 OpsLogExport 使用 + OpsLogExport ops(opsDir.toStdString(), callerGid); ops.run(); + return opsDir; +} + +// 基于 fd 的 TOCTOU 安全递归删除目录。 +// 全程相对父目录 fd 操作(openat/fstatat/unlinkat),不重新解析路径,消除「检查-使用」竞态。 +// - O_NOFOLLOW:若 name 是符号链接则 openat 直接失败,不跟随目标; +// - O_DIRECTORY:仅当为目录时打开; +// - fstatat(..., AT_SYMLINK_NOFOLLOW) 取条目自身 stat,符号链接为 S_ISLNK(非 S_ISDIR), +// 走 unlinkat 删链接本身而非跟随目标。 +static bool safeRemoveDirRecursive(int parentFd, const char *name) +{ + int fd = openat(parentFd, name, O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) { + // 非目录或符号链接:作为文件/链接直接 unlink(不跟随目标)。 + return unlinkat(parentFd, name, 0) == 0; + } + + DIR *dir = fdopendir(fd); + if (!dir) { + close(fd); + return false; + } + + // 先收集所有条目名,再处理,避免在迭代过程中修改目录导致遗漏。 + QVector entries; + struct dirent *entry = nullptr; + while ((entry = readdir(dir)) != nullptr) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + entries.append(QByteArray(entry->d_name)); + } + // 此时不再调用 readdir,可安全修改目录。 + + bool ok = true; + for (const QByteArray &childName : entries) { + struct stat st; + if (fstatat(fd, childName.constData(), &st, AT_SYMLINK_NOFOLLOW) != 0) + continue; // 条目已消失,跳过 + + if (S_ISDIR(st.st_mode)) { + if (!safeRemoveDirRecursive(fd, childName.constData())) + ok = false; + } else { + // 普通文件或符号链接——unlink 链接本身,不跟随。 + if (unlinkat(fd, childName.constData(), 0) != 0) + ok = false; + } + } + + closedir(dir); // 同时关闭 fd + // 目录已清空,移除目录项本身。 + if (unlinkat(parentFd, name, AT_REMOVEDIR) != 0) + ok = false; + return ok; +} + +bool LogViewerService::removeOpsTempDirByPath(const QString &path) +{ + // 防御性前缀校验:缓存路径由本服务设置,理论可信,但删除前再确认前缀。 + if (!path.startsWith("/var/log/deepin-log-viewer-ops-log.")) { + qCWarning(logService) << "removeOpsTempDirByPath: cached path not under expected prefix, refuse:" << path; + return false; + } + + // 基于 fd 的 TOCTOU 安全递归删除,替代 QDir::removeRecursively 的路径式操作。 + // 注意:safeRemoveDirRecursive 内部递归虽是 fd-relative,但其入口签名是 (parentFd, name), + // 若传 AT_FDCWD + 绝对路径,openat 会忽略 fd 并重新解析绝对路径,打破“全程不重新解析路径” + // 的设计承诺。这里先打开可信父目录 /var/log 的 fd,再以 basename 进行 fd-relative 操作, + // 目标路径自始至终不再被重新解析,真正消除“检查-使用”竞态。 + const QByteArray pathBytes = QFile::encodeName(path); + const int slash = pathBytes.lastIndexOf('/'); + if (slash <= 0) { + qCWarning(logService) << "removeOpsTempDirByPath: invalid path (no parent dir):" << path; + return false; + } + const QByteArray parentPath = pathBytes.left(slash); // /var/log + const QByteArray baseName = pathBytes.mid(slash + 1); // deepin-log-viewer-ops-log.XXX + + const int parentFd = open(parentPath.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (parentFd < 0) { + qCWarning(logService) << "removeOpsTempDirByPath: failed to open parent dir:" << parentPath << "errno:" << errno; + return false; + } + const bool removed = safeRemoveDirRecursive(parentFd, baseName.constData()); + close(parentFd); + + if (!removed) { + qCWarning(logService) << "removeOpsTempDirByPath: failed to remove safely:" << path; + return false; + } + + qCDebug(logService) << "removeOpsTempDirByPath: removed:" << path; return true; } -QString LogViewerService::getCallerHomeDir() +bool LogViewerService::removeOpsLogTempDir() +{ + // 授权校验:与 exportOpsLog 一致,使用 s_Action_View。 + if (!checkAuth(s_Action_View)) { + return false; + } + + // 无参设计:清理的是服务端在 exportOpsLog 中按当前调用者缓存的本进程临时目录路径, + // 不接受调用方传参,杜绝路径替换/越权删除风险。按 D-Bus unique bus name 查找, + // 只清理当前调用者自己的目录,互不影响其他用户/窗口的导出。 + const QString callerBusName = message().service(); + if (!m_opsTempDirs.contains(callerBusName)) { + return true; // 当前调用者无缓存目录,视为已清理 + } + + const QString path = m_opsTempDirs.value(callerBusName); + const bool removed = removeOpsTempDirByPath(path); + m_opsTempDirs.remove(callerBusName); + return removed; +} + +bool LogViewerService::getCallerHomeAndGid(QString &outHome, uint &outGid) { if (!calledFromDBus()) { - qWarning() << "getCallerHomeDir: called not from dbus."; - return QString(); + qWarning() << "getCallerHomeAndGid: called not from dbus."; + return false; } QString busName = message().service(); auto reply = connection().interface()->serviceUid(busName); if (!reply.isValid()) { - qCWarning(logService) << "getCallerHomeDir: failed to get caller UID via D-Bus:" + qCWarning(logService) << "getCallerHomeAndGid: failed to get caller UID via D-Bus:" << reply.error().message(); - return QString(); + return false; } uint callerUid = reply.value(); @@ -1089,11 +1228,13 @@ QString LogViewerService::getCallerHomeDir() int ret = getpwuid_r(static_cast(callerUid), &pwd, buf.data(), buf.size(), &result); if (ret == 0 && result) { - return QString::fromLocal8Bit(pwd.pw_dir); + outHome = QString::fromLocal8Bit(pwd.pw_dir); + outGid = static_cast(pwd.pw_gid); + return true; } - qCCritical(logService) << "getCallerHomeDir: failed to resolve uid:" << callerUid << "error:" << ret; - return QString(); + qCCritical(logService) << "getCallerHomeAndGid: failed to resolve uid:" << callerUid << "error:" << ret; + return false; } bool LogViewerService::checkAuth(const QString &actionId) diff --git a/logViewerService/logviewerservice.h b/logViewerService/logviewerservice.h index 98ec531b..232f054f 100755 --- a/logViewerService/logviewerservice.h +++ b/logViewerService/logviewerservice.h @@ -13,6 +13,7 @@ #include #include #include +#include class QTextStream; class DGioVolumeManager; @@ -36,7 +37,10 @@ public Q_SLOTS: Q_SCRIPTABLE QStringList getFileInfo(const QString &file, bool unzip = true); Q_SCRIPTABLE QStringList getOtherFileInfo(const QString &file, bool unzip = true); Q_SCRIPTABLE bool exportLog(const QString &outDir, const QString &in, bool isFile); - Q_SCRIPTABLE bool exportOpsLog(); + Q_SCRIPTABLE QString exportOpsLog(); + // 清理 exportOpsLog 在 /var/log 下创建的临时导出目录。无参:清理的是服务端缓存 + // 的本次创建路径,避免调用方传参带来的路径替换/越权删除风险。 + Q_SCRIPTABLE bool removeOpsLogTempDir(); Q_SCRIPTABLE QString openLogStream(const QString &filePath); Q_SCRIPTABLE QString readLogInStream(const QString &token); Q_SCRIPTABLE QString isFileExist(const QString &filePath); @@ -70,13 +74,20 @@ public Q_SLOTS: QTemporaryDir tmpDir; QProcess m_process; QString m_tmpDirPath; + // exportOpsLog 创建的 /var/log 临时目录路径,按 D-Bus 调用方 unique bus name 隔离存储, + // 供 removeOpsLogTempDir 按当前调用者身份清理。避免单一成员变量在多用户/多窗口并发 + // 导出时被覆盖,导致临时目录泄露或误清理他人目录。 + QMap m_opsTempDirs; QString m_actionId; QMap m_commands; QMap> m_logMap; QMap> m_logLineIndex; bool checkAuth(const QString &actionId); - // 获取当前调用该 DBus 接口的用户家目录路径 - QString getCallerHomeDir(); + // 按 fd-relative 安全方式删除 exportOpsLog 产生的 /var/log 临时目录(path 为缓存路径)。 + // 成功返回 true;path 为空或非预期前缀/删除失败返回 false。 + bool removeOpsTempDirByPath(const QString &path); + // 获取当前调用该 DBus 接口的用户家目录路径与主组 gid + bool getCallerHomeAndGid(QString &outHome, uint &outGid); QByteArray processCatFile(const QString &filePath); void processCmdArgs(const QString &cmdStr, const QStringList &args); }; diff --git a/logViewerService/opslogexport.cpp b/logViewerService/opslogexport.cpp index db6e3132..24450fd9 100644 --- a/logViewerService/opslogexport.cpp +++ b/logViewerService/opslogexport.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "opslogexport.h" +#include "opslogpaths.h" #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include @@ -20,6 +22,7 @@ #include #include #include +#include using namespace std; @@ -158,16 +161,21 @@ static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcar return matchedFiles; } -OpsLogExport::OpsLogExport(const string &target, const string &home) +OpsLogExport::OpsLogExport(const string &target, uint callerGid) : target_dir(target) - , home_dir(home) + , m_callerGid(callerGid) { } void OpsLogExport::run() { - // 注意:需要优先创建目录结构 - createDirStruct(); + // 安全加固:在执行任何拷贝操作前,验证目标目录路径安全(无符号链接、在 /var/log 下) + if (!ensureSafeTargetDir()) { + qWarning() << "OpsLogExport::run: target directory safety check failed, aborting export"; + return; + } + + createOpsLogDirStruct(QString::fromStdString(target_dir)); exportAppLogs(); exportSystemLogs(); @@ -184,17 +192,113 @@ void OpsLogExport::run() setDirectoryPermissionsSafe(target_dir); } +void OpsLogExport::createOpsLogDirStruct(const QString &outDir) +{ + // 创建目录结构 + QStringList dirs = { + outDir + QString(kKernelPath), + outDir + QString(kSystemPath), + outDir + QString(kDDEPath), + outDir + QString(kAppPath), + outDir + QString(kJournalPath), + outDir + QString(kAptPath), + outDir + QString(kUosStePath), + outDir + QString(kUossteLogsPath), + outDir + QString(kDefenderPath), + outDir + QString(kCloudPrintPath), + outDir + QString(kCloudScanPath), + outDir + QString(kPrinterPath), + outDir + QString(kGraphicsDriverManagerPath), + outDir + QString(kBootMakerPath), + outDir + QString(kScanerPath), + outDir + QString(kKMSPath), + outDir + QString(kCompressorPath), + outDir + QString(kCalendarPath), + outDir + QString(kManualPath), + outDir + QString(kReaderPath), + outDir + QString(kFontManagerPath), + outDir + QString(kDebInstallerPath), + outDir + QString(kTerminalPath), + outDir + QString(kVoiceNotPath), + outDir + QString(kDevicemanagerPath), + outDir + QString(kServiceSupportPath), + outDir + QString(kRemoteAssistancePath), + outDir + QString(kSystemMonitorPath), + outDir + QString(kEditorPath), + outDir + QString(kCalculatorPath), + outDir + QString(kMailPath), + outDir + QString(kScreenRecorderPath), + outDir + QString(kDrawPath), + outDir + QString(kMusicPath), + outDir + QString(kImageViewerPath), + outDir + QString(kAlbumPath), + outDir + QString(kMoviePath), + outDir + QString(kCameraPath), + outDir + QString(kChineseImePath), + outDir + QString(kDeepinInstallerPath), + outDir + QString(kDeepinRecoveryPath), + outDir + QString(kOemCustomPath), + outDir + QString(kUosActivatorPath), + outDir + QString(kUosActivatorLogPath), + outDir + QString(kFcitxPath), + outDir + QString(kDiskManagerPath), + outDir + QString(kDownloaderPath), + outDir + QString(kKwinPath), + outDir + QString(kKboxPath), + outDir + QString(kDeepinLogViewerPath), + outDir + QString(kDdeDesktopPath), + outDir + QString(kDdeFileManagerPath), + outDir + QString(kDdeDockPath), + outDir + QString(kSystemPulseaudioPath) + }; + + // hisi目录仅在源目录存在时创建 + if (path_exists("/var/log/hisi")) { + dirs.push_back(outDir + QString(kHisiPath)); + } + + for (const QString &dir : dirs) { + QDir(dir).mkpath("."); + } +} + bool OpsLogExport::path_exists(const string &path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } -bool OpsLogExport::create_directories(const string &path) +/*! + * \brief 验证目标目录在 /var/log 下。 + * + * target_dir 由 LogViewerService::exportOpsLog() 通过 QTemporaryDir 在 /var/log 下 + * 随机创建(root 控制、用户不可预测路径),此处仅做防御性前缀校验。 + * + * 使用 canonicalFilePath 解析符号链接到真实路径后再做前缀比对,避免 cleanPath 仅处理 + * “.”/“..” 而不解析符号链接、被构造路径逃逸到非预期目录。注意 /var/log 自身也可能 + * 是符号链接(指向独立分区),因此两端都用 canonicalFilePath 解析后在同一真实坐标系下比对。 + * + * \return true 表示路径在 /var/log 下;false 表示路径异常。 + */ +bool OpsLogExport::ensureSafeTargetDir() { - return QDir(QString::fromStdString(path)).mkpath("."); + const QString rawTarget = QString::fromStdString(target_dir); + const QString canonicalTarget = QFileInfo(rawTarget).canonicalFilePath(); + if (canonicalTarget.isEmpty()) { + qWarning() << "ensureSafeTargetDir: target_dir does not exist or cannot be resolved:" << rawTarget; + return false; + } + const QString canonicalVarLog = QFileInfo(QStringLiteral("/var/log")).canonicalFilePath(); + if (canonicalVarLog.isEmpty()) { + qWarning() << "ensureSafeTargetDir: /var/log cannot be resolved"; + return false; + } + if (!canonicalTarget.startsWith(canonicalVarLog + "/")) { + qWarning() << "ensureSafeTargetDir: target_dir is not under /var/log:" << canonicalTarget; + return false; + } + return true; } - void OpsLogExport::copy_file_or_dir(const string &src, const string &dst_dir) { if (!path_exists(src)) return; @@ -203,6 +307,9 @@ void OpsLogExport::copy_file_or_dir(const string &src, const string &dst_dir) QString qDst = QString::fromStdString(dst_dir); QFileInfo srcInfo(qSrc); + // 符号链接源一律不拷贝:避免引入指向特殊文件/系统文件的链接,防止后续前端 cp + // 或 root 清理时跟随链接造成阻塞或误删。目录内残留的链接会在权限整理阶段再删一次。 + if (srcInfo.isSymLink()) return; if (srcInfo.isFile()) { // 单个文件:确保目标目录存在后用 QFile::copy QDir().mkpath(qDst); @@ -210,8 +317,9 @@ void OpsLogExport::copy_file_or_dir(const string &src, const string &dst_dir) QFile::remove(dstFile); // QFile::copy 要求目标不存在 QFile::copy(qSrc, dstFile); } else { - // 目录:使用 cp -rf 通过 QProcess 参数数组传递 - runProcess({"cp", "-rf", qSrc, qDst}); + // 目录:-rP 复制链接本身而非目标内容;目录内的符号链接条目会在 + // setDirectoryPermissionsSafe 中被统一删除,最终导出目录不含任何符号链接。 + runProcess({"cp", "-rP", qSrc, qDst}); } } @@ -222,180 +330,82 @@ void OpsLogExport::execute_command(const QStringList &args, const string &output void OpsLogExport::setDirectoryPermissionsSafe(const string &dir_path) { - // 收缩到最小权限:仅 owner 可读写执行/写入,避免向 group 和 other 暴露导出目录。 - const QFile::Permissions dirPerms = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner; - const QFile::Permissions filePerms = QFile::ReadOwner | QFile::WriteOwner; + // 基本校验:确认路径在 /var/log 下。目录由 root 通过 QTemporaryDir 在 /var/log 下 + // 随机创建(mkdtemp,初始 0700 root:root,路径用户不可预测)。 + // 使用 canonicalFilePath 解析符号链接到真实路径后再比对,避免 cleanPath 不解析符号链接 + // 被构造路径逃逸;/var/log 自身也可能是符号链接,故两端都解析后比对。 + const QString rawDir = QString::fromStdString(dir_path); + const QString canonicalDir = QFileInfo(rawDir).canonicalFilePath(); + if (canonicalDir.isEmpty()) { + qWarning() << "setDirectoryPermissionsSafe: dir_path cannot be resolved:" << rawDir; + return; + } + const QString canonicalVarLog = QFileInfo(QStringLiteral("/var/log")).canonicalFilePath(); + if (canonicalVarLog.isEmpty()) { + qWarning() << "setDirectoryPermissionsSafe: /var/log cannot be resolved"; + return; + } + if (!canonicalDir.startsWith(canonicalVarLog + "/")) { + qWarning() << "setDirectoryPermissionsSafe: dir_path is not under /var/log, refusing:" << canonicalDir; + return; + } - const QFileInfo homeInfo(QString::fromStdString(home_dir)); - const uint ownerId = static_cast(homeInfo.ownerId()); - const uint groupId = static_cast(homeInfo.groupId()); + // 权限策略:属主保持 root,仅将 group 改为 callerGid,目录 0750、文件 0640。 + // + // - 不把 owner 改成 caller:caller 对目录只有 group 的 r-x(无 w),无法在其中创建 + // 或替换符号链接,从而杜绝 caller 植入指向系统核心文件的符号链接、待 root 下次 + // 清理 removeRecursively 时误删目标的攻击面。 + // - 0750/0640 而非 0755/0644:仅 caller 同组可读,避免任意本地用户在 exportOpsLog + // 返回至前端拷贝完成的时间窗内读取 auth.log/syslog 等敏感系统日志。 + // + // 仍需递归处理:copy_file_or_dir() 对目录用 `cp -rP` 会保留源目录 mode(如 + // /var/log/journal 常为 0700/02755),必须统一刷成 0750/0640。 + const QFile::Permissions dirPerms = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup; + const QFile::Permissions filePerms = QFile::ReadOwner | QFile::WriteOwner + | QFile::ReadGroup; + + const gid_t callerGid = static_cast(m_callerGid); + // uid 传 -1 表示不改变 owner(保持 root),仅设置 group。 + const uid_t keepOwner = static_cast(-1); auto applySafeOwnership = [&](const QString &path, bool isDir) { - if (::chown(QFile::encodeName(path).constData(), ownerId, groupId) != 0) { - qWarning() << "Failed to chown export path:" << path << "error:" << strerror(errno); + if (::chown(QFile::encodeName(path).constData(), keepOwner, callerGid) != 0) { + qWarning() << "Failed to chgrp export path:" << path << "error:" << strerror(errno); return; } QFile::setPermissions(path, isDir ? dirPerms : filePerms); }; - applySafeOwnership(QString::fromStdString(dir_path), true); + // 顶层临时目录本身(QTemporaryDir 经 mkdtemp 创建,非符号链接) + applySafeOwnership(canonicalDir, true); - QDirIterator it(QString::fromStdString(dir_path), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + // QDirIterator 默认不跟随符号链接(NoIteratorFlags) + QDirIterator it(canonicalDir, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); const QFileInfo &info = it.fileInfo(); - if (info.isSymLink()) + if (info.isSymLink()) { + // 符号链接不保留:直接删除链接条目(不跟随目标),确保导出目录不含任何 + // 符号链接——前端 cp 与 root 下次清理均无链接可跟随,杜绝相关风险。 + QFile::remove(info.filePath()); continue; + } applySafeOwnership(info.filePath(), info.isDir()); } } -void OpsLogExport::createDirStruct() -{ - // 创建目录结构 - vector dirs = { - target_dir + "/kernel", - target_dir + "/system", - target_dir + "/dde", - target_dir + "/app", - target_dir + "/app/deepin-defender", - target_dir + "/app/deepin-cloud-print", - target_dir + "/app/deepin-cloud-scan", - target_dir + "/app/dde-printer", - target_dir + "/app/deepin-graphics-driver-manager", - target_dir + "/app/deepin-boot-maker", - target_dir + "/app/deepin-scaner", - target_dir + "/app/kms", - target_dir + "/app/deepin-compressor", - target_dir + "/app/dde-calendar", - target_dir + "/app/deepin-manual", - target_dir + "/app/deepin-reader", - target_dir + "/app/deepin-font-manager", - target_dir + "/app/deepin-deb-installer", - target_dir + "/app/deepin-terminal", - target_dir + "/app/deepin-voice-note", - target_dir + "/app/deepin-devicemanager", - target_dir + "/app/uos-service-support", - target_dir + "/app/uos-remote-assistance", - target_dir + "/app/deepin-system-monitor", - target_dir + "/app/deepin-editor", - target_dir + "/app/deepin-calculator", - target_dir + "/app/deepin-mail", - target_dir + "/app/deepin-screen-recorder", - target_dir + "/app/deepin-draw", - target_dir + "/app/deepin-music", - target_dir + "/app/deepin-image-viewer", - target_dir + "/app/deepin-album", - target_dir + "/app/deepin-movie", - target_dir + "/app/deepin-camera", - target_dir + "/app/chineseime", - target_dir + "/app/deepin-installer", - target_dir + "/app/deepin-recovery", - target_dir + "/app/oem-custom", - target_dir + "/app/uos-activator", - target_dir + "/app/uos-activator/log", - target_dir + "/app/fcitx", - target_dir + "/app/deepin-diskmanager", - target_dir + "/app/downloader", - target_dir + "/app/kwin", - target_dir + "/app/kbox", - target_dir + "/app/deepin-log-viewer", - target_dir + "/dde/dde-desktop", - target_dir + "/dde/dde-file-manager", - target_dir + "/dde/dde-dock", - target_dir + "/system/pulseaudio", - target_dir + "/journal", - target_dir + "/apt", - target_dir + "/uos-ste", - target_dir + "/uosste_logs" - }; - - // hisi目录仅在源目录存在时创建 - if (path_exists("/var/log/hisi")) { - dirs.push_back(target_dir + "/hisi"); - } - - for (const auto& dir : dirs) { - create_directories(dir); - } -} - void OpsLogExport::exportAppLogs() { - // 安全中心 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-defender/deepin-defender.log", target_dir + "/app/deepin-defender/"); - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-defender-daemon/deepin-defender-daemon.log", target_dir + "/app/deepin-defender/"); - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-defender-datainterface/deepin-defender-datainterface.log", target_dir + "/app/deepin-defender/"); // 云打印 - copy_file_or_dir(home_dir + "/.cache/uniontech/deepin-cloud-print/deepin-cloud-print.log", target_dir + "/app/deepin-cloud-print/"); copy_file_or_dir("/var/log/cups/dcp_log", target_dir + "/app/deepin-cloud-print/"); - copy_file_or_dir(home_dir + "/.cache/uniontech/deepin-cloud-print-configurator/deepin-cloud-print-configurator.log", target_dir + "/app/deepin-cloud-print/"); - // 云扫描 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-cloud-scan/deepin-cloud-scan.log", target_dir + "/app/deepin-cloud-scan/"); // 打印管理器 copy_file_or_dir("/var/log/cups/error_log", target_dir + "/app/dde-printer/"); - copy_file_or_dir(home_dir + "/.cache/deepin/dde-printer/dde-printer.log", target_dir + "/app/dde-printer/"); // 显卡驱动管理器 copy_file_or_dir("/var/log/deepin-graphics-driver-manager-server.log", target_dir + "/app/deepin-graphics-driver-manager/"); - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-graphics-driver-manager/deepin-graphics-driver-manager.log", target_dir + "/app/deepin-graphics-driver-manager/"); // 启动盘制作工具 copy_file_or_dir("/var/log/deepin/deepin-boot-maker-service.log", target_dir + "/app/deepin-boot-maker/"); - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-boot-maker/deepin-boot-maker.log", target_dir + "/app/deepin-boot-maker/"); - // 扫描管理 - copy_file_or_dir(home_dir + "/.cache/deepin/org.deepin.scanner/org.deepin.scanner", target_dir + "/app/deepin-scaner/"); - // KMS项目 - copy_file_or_dir(home_dir + "/.cache/deepin/kmsclient", target_dir + "/app/kms/"); - copy_file_or_dir(home_dir + "/.cache/deepin/kmstools", target_dir + "/app/kms/"); - // 归档管理器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-compressor/deepin-compressor.log", target_dir + "/app/deepin-compressor/"); - // 日历 - copy_file_or_dir(home_dir + "/.cache/deepin/dde-calendar-service/dde-calendar-service.log", target_dir + "/app/dde-calendar/"); - copy_file_or_dir(home_dir + "/.cache/deepin/dde-calendar/dde-calendar.log", target_dir + "/app/dde-calendar/"); - // 帮助手册 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-manual/deepin-manual.log", target_dir + "/app/deepin-manual/"); - // 文档查看器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-reader/deepin-reader.log", target_dir + "/app/deepin-reader/"); - // 字体管理器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-font-manager/deepin-font-manager.log", target_dir + "/app/deepin-font-manager/"); - // 软件包安装器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-deb-installer/deepin-deb-installer.log", target_dir + "/app/deepin-deb-installer/"); - // 终端 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-terminal/deepin-terminal.log", target_dir + "/app/deepin-terminal/"); - // 语音记事本 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-voice-note/deepin-voice-note.log", target_dir + "/app/deepin-voice-note/"); - // 设备管理器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-devicemanager/deepin-devicemanager.log", target_dir + "/app/deepin-devicemanager/"); - // 服务与支持 - copy_file_or_dir(home_dir + "/.cache/deepin/uos-service-support/uos-service-support.log", target_dir + "/app/uos-service-support/"); - // 远程协助 - copy_file_or_dir(home_dir + "/.cache/deepin/uos-remote-assistance/uos-remote-assistance.log", target_dir + "/app/uos-remote-assistance/"); - // 系统监视器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-system-monitor/deepin-system-monitor.log", target_dir + "/app/deepin-system-monitor/"); - // 文本编辑器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-editor/deepin-editor.log", target_dir + "/app/deepin-editor/"); - // 计算器 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-calculator/deepin-calculator.log", target_dir + "/app/deepin-calculator/"); - // 邮箱 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-mail/deepin-mail.log", target_dir + "/app/deepin-mail/"); - // 截图录屏 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-screen-recorder/deepin-screen-recorder.log", target_dir + "/app/deepin-screen-recorder/"); - // 画板 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-draw/deepin-draw.log", target_dir + "/app/deepin-draw/"); - // 音乐 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-music/deepin-music.log", target_dir + "/app/deepin-music/"); - // 看图 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-image-viewer/deepin-image-viewer.log", target_dir + "/app/deepin-image-viewer/"); - // 相册 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-album/deepin-album.log", target_dir + "/app/deepin-album/"); - // 影院 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-movie/deepin-movie.log", target_dir + "/app/deepin-movie/"); - // 相机 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-camera/deepin-camera.log", target_dir + "/app/deepin-camera/"); - // 中文输入法 - copy_file_or_dir(home_dir + "/.cache/org.deepin.chineseime/ime/chineseime-qimpanel.log", target_dir + "/app/chineseime/"); - copy_file_or_dir(home_dir + "/.cache/org.deepin.chineseime/ime/fcitx-iflyime.log", target_dir + "/app/chineseime/"); - copy_file_or_dir(home_dir + "/.cache/org.deepin.chineseime/ime/ossp.log", target_dir + "/app/chineseime/"); // 安装器 copy_file_or_dir("/var/log/deepin-installer.log", target_dir + "/app/deepin-installer/"); copy_file_or_dir("/var/log/deepin-installer-first-boot.log", target_dir + "/app/deepin-installer/"); @@ -406,30 +416,16 @@ void OpsLogExport::exportAppLogs() copy_file_or_dir("/var/local/oem-custom-tool/oem-custom-tool.log", target_dir + "/app/oem-custom/"); copy_file_or_dir("/var/local/oem-custom-tool/oem-custom-tool-bk.log", target_dir + "/app/oem-custom/"); copy_file_or_dir("/root/.cache/isocustomizer-agent/iso-customizer-agent/iso-customizer-agent.log", target_dir + "/app/oem-custom/"); - // 授权管理客户端 - copy_file_or_dir(home_dir + "/.cache/uos/uos-activator", target_dir + "/app/uos-activator/"); - copy_file_or_dir(home_dir + "/.cache/uos/uos-activator-cmd", target_dir + "/app/uos-activator/"); - copy_file_or_dir(home_dir + "/.cache/uos-agent/uos-license-agent", target_dir + "/app/uos-activator/"); - copy_file_or_dir(home_dir + "/.cache/uos-agent/uos-activator-kms", target_dir + "/app/uos-activator/"); // 授权管理客户端(1020及之后版本日志) copy_file_or_dir("/var/log/uos/uos-license-agent", target_dir + "/app/uos-activator/log/"); copy_file_or_dir("/var/log/uos/uos-activator-kms", target_dir + "/app/uos-activator/log/"); - // 输入法配置 - // executCmd(("cp -rf /tmp/fcitx*.log " + target_dir + "/app/fcitx/").c_str()); // 磁盘管理器 copy_file_or_dir("/var/log/deepin/deepin-diskmanager-service/Log", target_dir + "/app/deepin-diskmanager/"); - // 下载器 - copy_file_or_dir(home_dir + "/.config/uos/downloader/Log", target_dir + "/app/downloader/"); - // 窗口管理器(查看内核显卡驱动、查看核外驱动、查看窗管版本) + // 窗口管理器 runProcess({"lspci", "-v"}, (target_dir + "/app/kwin/lspci_VGA.log").c_str(), "VGA", 19); - // execute_command("glxinfo -B", target_dir + "/app/kwin/glxinfo.log"); -// execute_command({"apt", "policy", "kwin-x11", "dde-kwin"}, target_dir + "/app/kwin/kwin_info.log"); // 安卓容器 copy_file_or_dir("/usr/share/log/log.txt", target_dir + "/app/kbox/"); - copy_file_or_dir(home_dir + "/log/AospLog.log", target_dir + "/app/kbox/"); - copy_file_or_dir(home_dir + "/log/KboxServer.log", target_dir + "/app/kbox/"); // 日志收集工具 - copy_file_or_dir(home_dir + "/.cache/deepin/deepin-log-viewer/deepin-log-viewer.log", target_dir + "/app/deepin-log-viewer/"); copy_file_or_dir("/var/log/deepin/deepin-log-viewer-service", target_dir + "/app/deepin-log-viewer/"); } @@ -455,8 +451,6 @@ void OpsLogExport::exportSystemLogs() runProcess(cpArgs); } } - // pulse audio /home/uos/pulse.log - copy_file_or_dir(home_dir + "/pulse.log", target_dir + "/system/pulseaudio/"); } void OpsLogExport::exportKernelLogs() @@ -476,12 +470,9 @@ void OpsLogExport::exportKernelLogs() } } runProcess({"lspci", "-vvv"}, (target_dir + "/kernel/lspci_VGA.log").c_str(), "VGA c", 12); - // execute_command("ifconfig", target_dir + "/kernel/ifconfig.log"); - // execute_command("ethtool -i $(ifconfig | grep --max-count=1 ^en | awk -F ':' '{print $1}')", target_dir + "/kernel/eth_info.log"); runProcess({"dmesg"}, (target_dir + "/kernel/dmesg_network.log").c_str(), "iwlwifi", 0); execute_command({"journalctl", "--system"}, target_dir + "/kernel/journalctl_system.log"); // ⽆法识别声卡问题⽇志 -// execute_command("aplay -l", target_dir + "/kernel/aplay.log"); execute_command({"lshw", "-c", "sound"}, target_dir + "/kernel/sound_info.log"); // 龙芯内核 copy_file_or_dir("/var/log/kern.log", target_dir + "/kernel/"); @@ -490,9 +481,6 @@ void OpsLogExport::exportKernelLogs() void OpsLogExport::exportDDELogs() { // 文件管理器 - copy_file_or_dir(home_dir + "/.cache/deepin/dde-desktop/dde-desktop.log", target_dir + "/dde/dde-desktop/"); - copy_file_or_dir(home_dir + "/.cache/deepin/dde-file-manager/dde-file-manager.log", target_dir + "/dde/dde-file-manager/"); - copy_file_or_dir(home_dir + "/.cache/deepin/dde-dock/dde-dock.log", target_dir + "/dde/dde-dock/"); copy_file_or_dir("/var/log/deepin/dde-file-manager-daemon", target_dir + "/dde/dde-file-manager/"); copy_file_or_dir("/var/log/messages", target_dir + "/dde/"); copy_file_or_dir("/var/log/syslog", target_dir + "/dde/"); @@ -500,9 +488,6 @@ void OpsLogExport::exportDDELogs() execute_command({"free", "-m"}, target_dir + "/dde/free-m.log"); // 查看内存情况,输出内容截图或保存 execute_command({"udisksctl", "dump"}, target_dir + "/dde/udiskctl_dump.txt"); execute_command({"df", "-h"}, target_dir + "/dde/df-h.txt"); - // DDE - runProcess({"cp", QString::fromStdString(home_dir + "/Desktop/DDE_LOG.zip"), - QString::fromStdString(target_dir + "/dde/")}); copy_file_or_dir("/var/log/journalLog", target_dir + "/dde/"); } diff --git a/logViewerService/opslogexport.h b/logViewerService/opslogexport.h index e3c5395a..19e74294 100644 --- a/logViewerService/opslogexport.h +++ b/logViewerService/opslogexport.h @@ -10,21 +10,28 @@ class OpsLogExport { public: - OpsLogExport(const std::string& target, const std::string& home); + // callerGid:调用 DBus 的前端用户主组。导出目录属主保持 root,仅将 group 改为 + // callerGid 并设为 0750/0640——caller 以同组身份获得 r-x(可读不可写),既避免 + // 0755/0644 的世界可读泄露,又使 caller 无法在临时目录内植入符号链接(无 w 权限), + // 从而消除 root 后续清理时跟随符号链接误删系统文件的风险。 + explicit OpsLogExport(const std::string& target, uint callerGid); void run(); private: std::string target_dir; - std::string home_dir; + uint m_callerGid; + + void createOpsLogDirStruct(const QString &outDir); bool path_exists(const std::string& path); - bool create_directories(const std::string& path); void copy_file_or_dir(const std::string& src, const std::string& dst_dir); void execute_command(const QStringList &args, const std::string &output_file); void setDirectoryPermissionsSafe(const std::string& dir_path); - void createDirStruct(); + // 验证目标目录在 /var/log 下(目录由 root 通过 QTemporaryDir 随机创建) + bool ensureSafeTargetDir(); + void exportAppLogs(); void exportSystemLogs(); void exportKernelLogs();