From a6dc73852331e812f177c0c471fd28537b043fce Mon Sep 17 00:00:00 2001 From: gongheng Date: Wed, 8 Jul 2026 10:13:31 +0800 Subject: [PATCH] fix(security): harden ops log export via fd-based transfer and symlink-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 --- application/dbusproxy/dldbushandler.cpp | 52 +++- application/dbusproxy/dldbushandler.h | 2 +- application/dbusproxy/dldbusinterface.h | 3 +- application/logallexportthread.cpp | 144 +++++----- application/opslogpaths.h | 73 ++++++ application/utils.cpp | 245 ++++++++++++++++-- application/utils.h | 20 +- .../assets/data/com.deepin.logviewer.xml | 4 + .../data/deepin-log-viewer-daemon.service | 2 + logViewerService/logviewerservice.cpp | 206 ++++++++++++++- logViewerService/logviewerservice.h | 10 +- logViewerService/opslogexport.cpp | 137 +--------- logViewerService/opslogexport.h | 4 +- 13 files changed, 659 insertions(+), 243 deletions(-) create mode 100644 application/opslogpaths.h diff --git a/application/dbusproxy/dldbushandler.cpp b/application/dbusproxy/dldbushandler.cpp index 334d1539..9430f5cb 100644 --- a/application/dbusproxy/dldbushandler.cpp +++ b/application/dbusproxy/dldbushandler.cpp @@ -179,20 +179,6 @@ QStringList DLDBusHandler::getOtherFileInfo(const QString &flag, bool unzip) return filePathList; } -QString DLDBusHandler::exportOpsLog() -{ - m_dbus->setTimeout(1200000); - QDBusPendingReply reply = m_dbus->exportOpsLog(); - reply.waitForFinished(); - m_dbus->setTimeout(-1); - - if (reply.isError()) { - qCWarning(logApp) << "call dbus iterface 'exportOpsLog()' failed. error info:" << reply.error().message(); - return QString(); - } - return reply.value(); -} - bool DLDBusHandler::exportLog(const QString &outDir, const QString &in, bool isFile) { qCDebug(logApp) << "DLDBusHandler::exportLog called with outDir:" << outDir << "in:" << in << "isFile:" << isFile; @@ -252,3 +238,41 @@ void DLDBusHandler::releaseFilePathCacheFile(const QString &cacheFilePath) QFile::remove(cacheFilePath); } } + +bool DLDBusHandler::exportOpsLog(const QString &zipFilePath) +{ + // 前端创建压缩包目标文件并以写方式打开,将 fd 通过 D-Bus 传给后端。 + // 后端在 root 权限下收集 /var/log 等运维日志,整体压缩后写入该 fd, + // 并自行清理 /var/log 下的随机临时目录,前端无需再感知后端临时目录路径。 + QFile zipFile(zipFilePath); + if (!zipFile.open(QIODevice::WriteOnly)) { + qCritical() << "exportOpsLog: failed to open zip file for writing:" << zipFilePath + << "error:" << zipFile.errorString(); + return false; + } + + const int fd = zipFile.handle(); + if (fd <= 0) { + qCritical() << "exportOpsLog: invalid file descriptor for:" << zipFilePath; + zipFile.close(); + return false; + } + + QDBusUnixFileDescriptor dbusFd(fd); + + m_dbus->setTimeout(1200000); + QDBusPendingReply reply = m_dbus->exportOpsLog(dbusFd); + reply.waitForFinished(); + m_dbus->setTimeout(-1); + + // 后端写完后会关闭其 dup 的 fd,但前端打开的 QFile 仍需由前端关闭。 + zipFile.close(); + + if (reply.isError()) { + qCritical() << "call dbus interface 'exportOpsLog' failed. error info:" << reply.error().message(); + return false; + } + + const bool ok = reply.value(); + return ok; +} diff --git a/application/dbusproxy/dldbushandler.h b/application/dbusproxy/dldbushandler.h index 1fa258b1..9781148d 100644 --- a/application/dbusproxy/dldbushandler.h +++ b/application/dbusproxy/dldbushandler.h @@ -22,7 +22,6 @@ class DLDBusHandler : public QObject int exitCode(); void quit(); bool exportLog(const QString &outDir, const QString &in, bool isFile); - QString exportOpsLog(); bool isFileExist(const QString &filePath); quint64 getFileSize(const QString &filePath); qint64 getLineCount(const QString &filePath); @@ -30,6 +29,7 @@ class DLDBusHandler : public QObject QString openLogStream(const QString &filePath); QString readLogInStream(const QString &token); QStringList whiteListOutPaths(); + bool exportOpsLog(const QString &zipFilePath); private: explicit DLDBusHandler(QObject *parent = nullptr); diff --git a/application/dbusproxy/dldbusinterface.h b/application/dbusproxy/dldbusinterface.h index 3a76ce67..cfaa78b4 100644 --- a/application/dbusproxy/dldbusinterface.h +++ b/application/dbusproxy/dldbusinterface.h @@ -151,9 +151,10 @@ public Q_SLOTS: // METHODS return asyncCallWithArgumentList(QStringLiteral("whiteListOutPaths"), argumentList); } - inline QDBusPendingReply exportOpsLog() + inline QDBusPendingReply exportOpsLog(const QDBusUnixFileDescriptor &fd) { QList argumentList; + argumentList << QVariant::fromValue(fd); return asyncCallWithArgumentList(QStringLiteral("exportOpsLog"), argumentList); } Q_SIGNALS: // SIGNALS diff --git a/application/logallexportthread.cpp b/application/logallexportthread.cpp index 299f3ebd..9fb69183 100644 --- a/application/logallexportthread.cpp +++ b/application/logallexportthread.cpp @@ -369,7 +369,7 @@ void LogAllExportThread::run() } // Add task for ops log - totalTasks += 10; + totalTasks += 20; emit updateTolProcess(totalTasks); int completedTasks = 0; @@ -470,80 +470,98 @@ void LogAllExportThread::run() // 不支持导出 sudo 权限的日志,且导出日志功能主要面向普通用户使用场景, // 因此当获取到的用户家目录为根目录时,认为是异常情况,不执行导出操作 if (!m_cancel.load() && QDir::homePath() != "/" && QDir::homePath() != "/root") { - // Create a fixed temporary directory for ops logs QString userHomePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - QString opsLogPath = DLDBusHandler::instance(this)->exportOpsLog(); - if (!opsLogPath.isEmpty()) { - completedTasks += 5; - emit updatecurrentProcess(completedTasks); - Utils::exportSomeOpsLogs(opsLogPath, userHomePath); - completedTasks += 2; - emit updatecurrentProcess(completedTasks); - - // Add ops logs to zip file with directory structure preserved - QDir opsDir(opsLogPath); - if (opsDir.exists()) { - // Recursive function to add directory structure to zip - std::function addDirToZip = [&](const QDir& currentDir, const QString& basePath) { - if (m_cancel.load()) return; - - // First, add all files in current directory - QStringList files = currentDir.entryList(QDir::Files | QDir::NoDotAndDotDot); - for (const QString &file : files) { - if (m_cancel.load()) break; - - QString filePath = currentDir.filePath(file); - QString entryName = "log-ops/" + basePath + file; - ensureDirectoriesExist(entryName); - if (!addFileToZip(filePath, entryName)) { - qCWarning(logApp) << "Failed to add ops log file to zip:" << entryName; - } + QTemporaryDir tmpOpsDir(Utils::getAppDataPath() + "/" + "tmp-ops-log.XXXXXX"); + if (!tmpOpsDir.isValid()) { + qCCritical(logApp) << "failed to create temporary dir under app data dir"; + zipClose(m_zipFile, nullptr); + QFile::remove(m_outfile); + emit exportFinsh(false); + return; + } + const QString tmpOpsDirPath = tmpOpsDir.path(); + + // 收集运维日志:前端创建压缩包目标文件并以写方式打开,将 fd 通过 D-Bus 传给 root 服务。 + // root 服务在 /var/log 下创建随机临时目录收集日志,整体压缩后写入该 fd,并自行清理临时目录。 + // 前端拿到写入完成的压缩包后,解压到 opsLogPath,再删除该压缩包。 + QString opsZipPath = tmpOpsDirPath + "/" + "log-ops.zip"; + bool opsOk = DLDBusHandler::instance(this)->exportOpsLog(opsZipPath); + if (!opsOk || !QFileInfo(opsZipPath).exists()) { + qCCritical(logApp) << "exportOpsLog failed or zip not produced"; + zipClose(m_zipFile, nullptr); + QFile::remove(m_outfile); + emit exportFinsh(false); + return; + } + completedTasks += 10; + emit updatecurrentProcess(completedTasks); + + // 解压 root 写入的压缩包到 tmpOpsDirPath -n 永不覆盖已存在项,-d 指定目标目录)。 + Utils::executeCmd("unzip", QStringList() << "-n" << opsZipPath << "-d" << tmpOpsDirPath); + // 及时清理 opsZipPath 压缩包,避免将该压缩包也导出给用户 + QFile::remove(opsZipPath); + // 继续收集用户权限的运维日志 + Utils::exportUserPermissionOpsLogs(tmpOpsDirPath, userHomePath); + completedTasks += 5; + emit updatecurrentProcess(completedTasks); + + // Add ops logs to zip file with directory structure preserved + QDir opsDir(tmpOpsDirPath); + if (opsDir.exists()) { + // Recursive function to add directory structure to zip + std::function addDirToZip = [&](const QDir& currentDir, const QString& basePath) { + if (m_cancel.load()) return; + + // First, add all files in current directory + QStringList files = currentDir.entryList(QDir::Files | QDir::NoDotAndDotDot); + for (const QString &file : files) { + if (m_cancel.load()) break; + + QString filePath = currentDir.filePath(file); + QString entryName = "log-ops/" + basePath + file; + ensureDirectoriesExist(entryName); + + if (!addFileToZip(filePath, entryName)) { + qCWarning(logApp) << "Failed to add ops log file to zip:" << entryName; } + } - // Create directory entry for current directory if it's empty or has subdirectories - if (files.isEmpty()) { - QString dirEntryName = "log-ops/" + basePath; - if (!dirEntryName.endsWith('/')) { - dirEntryName += '/'; - } + // Create directory entry for current directory if it's empty or has subdirectories + if (files.isEmpty()) { + QString dirEntryName = "log-ops/" + basePath; + if (!dirEntryName.endsWith('/')) { + dirEntryName += '/'; + } - // Create empty directory entry - zip_fileinfo zfi = {}; - QDateTime currentTime = QDateTime::currentDateTime(); - zfi.tmz_date = dateTimeToTmZip(currentTime); - zfi.dosDate = 0; + // Create empty directory entry + zip_fileinfo zfi = {}; + QDateTime currentTime = QDateTime::currentDateTime(); + zfi.tmz_date = dateTimeToTmZip(currentTime); + zfi.dosDate = 0; - if (zipOpenNewFileInZip64(m_zipFile, dirEntryName.toUtf8().constData(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, ZIP_COMPRESSION_LEVEL, 1) == ZIP_OK) { - zipCloseFileInZip(m_zipFile); - } + if (zipOpenNewFileInZip64(m_zipFile, dirEntryName.toUtf8().constData(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, ZIP_COMPRESSION_LEVEL, 1) == ZIP_OK) { + zipCloseFileInZip(m_zipFile); } + } - // Then, recursively process subdirectories - QStringList subDirs = currentDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString &subDir : subDirs) { - if (m_cancel.load()) break; + // Then, recursively process subdirectories + QStringList subDirs = currentDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &subDir : subDirs) { + if (m_cancel.load()) break; - QDir subDirectory = currentDir; - if (subDirectory.cd(subDir)) { - QString subPath = basePath + subDir + "/"; - addDirToZip(subDirectory, subPath); - } + QDir subDirectory = currentDir; + if (subDirectory.cd(subDir)) { + QString subPath = basePath + subDir + "/"; + addDirToZip(subDirectory, subPath); } - }; + } + }; - // Start recursive addition from the ops directory - addDirToZip(opsDir, ""); - } - completedTasks += 2; - emit updatecurrentProcess(completedTasks); - } else { - qCCritical(logApp) << "Failed to create temporary ops log directory"; - completedTasks += 9; - emit updatecurrentProcess(completedTasks); + // Start recursive addition from the ops directory + addDirToZip(opsDir, ""); } - - completedTasks += 1; + completedTasks += 5; emit updatecurrentProcess(completedTasks); } 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 09a389bf..60ccf2df 100644 --- a/application/utils.cpp +++ b/application/utils.cpp @@ -5,6 +5,7 @@ #include "utils.h" #include "logsettings.h" #include "dbusmanager.h" +#include "opslogpaths.h" #include "dbusproxy/dldbushandler.h" #include "qtcompat.h" @@ -103,6 +104,8 @@ QString Utils::getAppDataPath() .filePath(qApp->organizationName())); QString path = dir.filePath(qApp->applicationName()); + if (!dir.exists(path)) + dir.mkpath(path); qCDebug(logApp) << "App data path:" << path; return path; } @@ -617,7 +620,7 @@ void Utils::updateRepeatCoredumpExePaths(const QList & file.close(); } -static QByteArray processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args) +QByteArray Utils::processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args) { qCDebug(logApp) << "Executing command:" << cmdStr << "with arguments:" << args << "in working directory:" << workPath; QProcess process; @@ -628,8 +631,15 @@ static QByteArray processCmdWithArgs(const QString &cmdStr, const QString &workP 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); + // 设定超时上限,防止因特殊文件(如 /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); @@ -646,7 +656,7 @@ QByteArray Utils::executeCmd(const QString &cmdStr, const QStringList &args, con return processCmdWithArgs(cmdStr, workPath, args); } -static void appendToFile(const QString &filePath, const QByteArray &content) +void Utils::appendToFile(const QString &filePath, const QByteArray &content) { QFile file(filePath); @@ -657,8 +667,25 @@ static void appendToFile(const QString &filePath, const QByteArray &content) } } +void Utils::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()) { + qWarning() << "skip symlink source to avoid DoS:" << src; + continue; + } + args << src; + } + args << dest; + Utils::executeCmd("cp", args); +} + // 查找所有物理网络接口 -QStringList getPhysicalInterfaces() +QStringList Utils::getPhysicalInterfaces() { QStringList physicalInterfaces; QDir netDir("/sys/class/net/"); @@ -683,7 +710,7 @@ QStringList getPhysicalInterfaces() return physicalInterfaces; } -static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcard) +QStringList Utils::expandPathWithWildcardIterator(const QString &pathWithWildcard) { // 1. 分离目录和文件名模式 QFileInfo fileInfo(pathWithWildcard); @@ -707,31 +734,213 @@ static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcar return matchedFiles; } -void Utils::exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir) +void Utils::exportUserPermissionAppLogs(const QString &outDir, const QString &userHomeDir) { - Q_UNUSED(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); - std::string tmpCmd; + // 远程协助 + safeCpSkipSymlinks({userHomeDir + "/.cache/deepin/uos-remote-assistance/uos-remote-assistance.log"}, + outDir + kRemoteAssistancePath, false); - // app + // 系统监视器 + 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 3e25830c..690920cd 100755 --- a/application/utils.h +++ b/application/utils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2019 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -80,10 +80,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 void exportSomeOpsLogs(const QString &outDir, const QString &userHomeDir); + static QByteArray processCmdWithArgs(const QString &cmdStr, const QString &workPath, const QStringList &args); + static void appendToFile(const QString &filePath, const QByteArray &content); + static void safeCpSkipSymlinks(const QStringList &sources, const QString &dest, bool recursive); + // 用户权限的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); /** * @brief specialComType 是否是特殊机型,like huawei * 取值有3种(-1,0,>0),默认为-1(未知),0(不是特殊机型),>0(特殊机型) diff --git a/logViewerService/assets/data/com.deepin.logviewer.xml b/logViewerService/assets/data/com.deepin.logviewer.xml index 9eeae1e3..1d3aa949 100755 --- a/logViewerService/assets/data/com.deepin.logviewer.xml +++ b/logViewerService/assets/data/com.deepin.logviewer.xml @@ -54,5 +54,9 @@ + + + + diff --git a/logViewerService/assets/data/deepin-log-viewer-daemon.service b/logViewerService/assets/data/deepin-log-viewer-daemon.service index ff3163b4..2cbeac1d 100644 --- a/logViewerService/assets/data/deepin-log-viewer-daemon.service +++ b/logViewerService/assets/data/deepin-log-viewer-daemon.service @@ -13,6 +13,8 @@ ExecStart=/usr/lib/deepin-daemon/log-view-service #User=deepin-daemon User=root ProtectSystem=strict +# exportOpsLog 需要在 /var/log 下创建临时导出目录,须显式放开写权限 +ReadWritePaths=/var/log MemoryMax=2G IOWeight=200 OOMScoreAdjust=-500 diff --git a/logViewerService/logviewerservice.cpp b/logViewerService/logviewerservice.cpp index f51aebb9..995a0773 100644 --- a/logViewerService/logviewerservice.cpp +++ b/logViewerService/logviewerservice.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include @@ -322,6 +325,97 @@ QString LogViewerService::getCallerHomeDir() return QString(); } +// 基于 fd 的 TOCTOU 安全方式删除 exportOpsLog 产生的 /var/log 临时目录(opsDir)。 +// 由 exportOpsLog 在写入 fd 完成后自动调用,确保 /var/log 下不残留含系统日志的目录。 +bool LogViewerService::removeOpsTempDirByPathInternal(const QString &path) +{ + // 防御性前缀校验:路径由本服务创建,删除前再确认前缀。 + if (!path.startsWith("/var/log/deepin-log-viewer-ops-log.")) { + qCWarning(logService) << "removeOpsTempDirByPathInternal: path not under expected prefix, refuse:" << path; + return false; + } + + // 先打开可信父目录 /var/log 的 fd,再以 basename 进行 fd-relative 操作, + // 目标路径自始至终不再被重新解析,真正消除“检查-使用”竞态。 + const QByteArray pathBytes = QFile::encodeName(path); + const int slash = pathBytes.lastIndexOf('/'); + if (slash <= 0) { + qCWarning(logService) << "removeOpsTempDirByPathInternal: 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) << "removeOpsTempDirByPathInternal: failed to open parent dir:" << parentPath << "errno:" << errno; + return false; + } + const bool removed = safeRemoveDirRecursive(parentFd, baseName.constData()); + close(parentFd); + + if (!removed) { + qCWarning(logService) << "removeOpsTempDirByPathInternal: failed to remove safely:" << path; + return false; + } + + qCDebug(logService) << "removeOpsTempDirByPathInternal: removed:" << path; + return true; +} + +// 基于 fd 的 TOCTOU 安全递归删除目录。 +// 全程相对父目录 fd 操作(openat/fstatat/unlinkat),不重新解析路径,消除「检查-使用」竞态。 +// - O_NOFOLLOW:若 name 是符号链接则 openat 直接失败,不跟随目标; +// - O_DIRECTORY:仅当为目录时打开; +// - fstatat(..., AT_SYMLINK_NOFOLLOW) 取条目自身 stat,符号链接为 S_ISLNK(非 S_ISDIR), +// 走 unlinkat 删链接本身而非跟随目标。 +bool LogViewerService::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; +} + /** @brief Polkit action authorization check. Use com.deepin.pkexec.logViewerAuth.policy config file. @@ -1226,35 +1320,125 @@ bool LogViewerService::checkAuth(const QString &actionId) return bAuthVaild; } -QString LogViewerService::exportOpsLog() +bool LogViewerService::exportOpsLog(const QDBusUnixFileDescriptor &fd) { if(!checkAuth(s_Action_View)) { qCWarning(logService) << "Invalid authorization for export log"; - return QString(); + return false; } QString callerHomeDir = getCallerHomeDir(); if (callerHomeDir.isEmpty()) { qCWarning(logService) << "Failed to get caller home directory for export log"; - return QString(); + return false; } // 不支持导出 sudo 权限的日志,且导出日志功能主要面向普通用户使用场景, // 因此当获取到的用户家目录为根目录时,认为是异常情况,不执行导出操作 if (callerHomeDir == "/" || callerHomeDir == "/root") { qCWarning(logService) << "Invalid caller home directory for export log: " << callerHomeDir; - return QString(); + return false; } - QByteArray tmpTemplate = "/tmp/deepin-log-viewer-ops-logs.XXXXXX"; - if (!mkdtemp(tmpTemplate.data())) { - qCCritical(logService) << "Failed to create secure temp directory"; - return QString(); + // 前端创建压缩包目标文件并以写方式打开,将 fd 通过 D-Bus 传入。 + // 后端在 root 权限下于 /var/log 创建随机临时目录收集运维日志,整体压缩后写入该 fd, + // 随后自行清理临时目录,全程不向调用方暴露 /var/log 路径,前端也无需再调用清理接口。 + int fdi = fd.fileDescriptor(); + if (fdi <= 0) { + qCWarning(logService) << "exportOpsLog: invalid file descriptor from caller"; + return false; + } + + 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 false; } - QString newOutDir = tmpTemplate; - OpsLogExport ops(newOutDir.toStdString(), callerHomeDir.toStdString()); + const QString opsDir = tmpOpsDir.path(); + // 在 opsDir 内创建子目录 log-ops,专用于 OpsLogExport 收集日志。 + // 收集完成后压缩该子目录,压缩包同样落在 opsDir 内, + // 全程不向 /var/log 暴露压缩包或日志内容,无需调整目录权限。 + const QString logCollectDir = opsDir + QStringLiteral("/log-ops"); + if (!QDir().mkpath(logCollectDir)) { + qCWarning(logService) << "exportOpsLog: failed to create log collect dir:" << logCollectDir; + removeOpsTempDirByPathInternal(opsDir); + return false; + } + + OpsLogExport ops(logCollectDir.toStdString()); ops.run(); - return newOutDir; + // 将收集到的 log-ops 子目录内容整体压缩。压缩包放在 opsDir 内(log-ops.zip), + // 与被压缩内容同处一个随机目录,不暴露在 /var/log 下,避免被其它用户短暂读取。 + // 直接以最终路径调用 zip 创建新压缩包,无需先创建空文件再删除。 + const QString tmpZipPath = opsDir + QStringLiteral("/log-ops.zip"); + QProcess zipProc; + zipProc.setWorkingDirectory(logCollectDir); + zipProc.start(QStringLiteral("zip"), QStringList() << QStringLiteral("-r") + << tmpZipPath << QStringLiteral(".")); + static constexpr int kZipTimeoutMs = 600000; + if (!zipProc.waitForFinished(kZipTimeoutMs) || zipProc.exitCode() != 0) { + qCWarning(logService) << "exportOpsLog: zip failed, exitCode:" << zipProc.exitCode() + << "stderr:" << zipProc.readAllStandardError(); + zipProc.kill(); + QFile::remove(tmpZipPath); + removeOpsTempDirByPathInternal(opsDir); + return false; + } + + // 将压缩包内容写入前端传入的 fd(内核经 SCM_RIGHTS 复制了句柄,root 写入即落入前端文件)。 + bool writeOk = false; + { + QFile zipIn(tmpZipPath); + if (!zipIn.open(QIODevice::ReadOnly)) { + qCWarning(logService) << "exportOpsLog: failed to open temp zip for reading:" << tmpZipPath; + } else { + QFile fdOut; + if (!fdOut.open(fdi, QIODevice::WriteOnly)) { + qCWarning(logService) << "exportOpsLog: failed to open caller fd for writing"; + } else { + constexpr qint64 bufSize = 1 << 20; // 1 MiB + // 缓冲区较大,改用堆分配,避免占用过多线程栈空间; + // 与本文件 exportLog() 中 1 MiB 缓冲区的处理方式保持一致。 + QScopedPointer buf(new char[bufSize]); + qint64 n = 0; + bool error = false; + while ((n = zipIn.read(buf.data(), bufSize)) > 0) { + qint64 written = 0; + while (written < n) { + qint64 w = fdOut.write(buf.data() + written, n - written); + if (w < 0) { + error = true; + break; + } + written += w; + } + if (error) + break; + } + if (error || n < 0) { + qCWarning(logService) << "exportOpsLog: write to caller fd failed"; + } else { + fdOut.flush(); + writeOk = true; + } + fdOut.close(); // 关闭后端持有的 fd 句柄,前端 QFile 仍保留自己的句柄 + } + zipIn.close(); + } + } + + // 清理压缩包与 /var/log 临时目录,无论写入是否成功都需回收。 + QFile::remove(tmpZipPath); + removeOpsTempDirByPathInternal(opsDir); + + if (!writeOk) { + qCWarning(logService) << "exportOpsLog: aborted, failed to write zip to caller fd"; + return false; + } + + qCDebug(logService) << "exportOpsLog: ops logs zipped and written to caller fd successfully"; + return true; } diff --git a/logViewerService/logviewerservice.h b/logViewerService/logviewerservice.h index c85ab13e..7bf1c5f6 100755 --- a/logViewerService/logviewerservice.h +++ b/logViewerService/logviewerservice.h @@ -44,8 +44,9 @@ public Q_SLOTS: // 仅能执行特定合法命令 Q_SCRIPTABLE QString executeCmd(const QString &cmd); Q_SCRIPTABLE QStringList whiteListOutPaths(); - - Q_SCRIPTABLE QString exportOpsLog(); + // 通过前端传入的文件描述符导出运维日志:后端在 /var/log 下创建随机临时目录收集日志, + // 整体压缩后写入 fd,随后自行清理临时目录,不再向调用方返回路径。 + Q_SCRIPTABLE bool exportOpsLog(const QDBusUnixFileDescriptor &fd); public: // 获取用户家目录 @@ -67,6 +68,11 @@ public Q_SLOTS: // 获取当前调用该 DBus 接口的用户家目录路径 QString getCallerHomeDir(); + // 基于 fd 的 TOCTOU 安全方式删除 exportOpsLog 产生的 /var/log 临时目录(opsDir)。 + // 由 exportOpsLog 写入 fd 完成后自动调用回收,成功返回 true。 + bool removeOpsTempDirByPathInternal(const QString &path); + // 基于 fd 的 TOCTOU 安全递归删除目录。 + bool safeRemoveDirRecursive(int parentFd, const char *name); private: bool checkAuthorization(const QString &actionId); diff --git a/logViewerService/opslogexport.cpp b/logViewerService/opslogexport.cpp index c43c879f..e030e5c0 100644 --- a/logViewerService/opslogexport.cpp +++ b/logViewerService/opslogexport.cpp @@ -161,9 +161,8 @@ static QStringList expandPathWithWildcardIterator(const QString &pathWithWildcar return matchedFiles; } -OpsLogExport::OpsLogExport(const string &target, const string &home) +OpsLogExport::OpsLogExport(const string &target) : target_dir(target) - , home_dir(home) { } @@ -181,9 +180,6 @@ void OpsLogExport::run() exportAptLogs(); exportUosSteLogs(); exportUosSteTwoLogs(); - - // 递归设置目录及文件的权限 - setDirectoryPermissionsSafe(target_dir); } bool OpsLogExport::path_exists(const string &path) @@ -205,6 +201,10 @@ void OpsLogExport::copy_file_or_dir(const string &src, const string &dst_dir) QString qDst = QString::fromStdString(dst_dir); QFileInfo srcInfo(qSrc); + // 符号链接源一律不拷贝:避免引入指向特殊文件/系统文件的链接。新流程下整个收集、 + // 压缩、回传、清理均在 root 一次 D-Bus 调用内闭环完成,无前端直接读取临时目录的环节, + // 故无需再对目录内残留的符号链接做统一清理。 + if (srcInfo.isSymLink()) return; if (srcInfo.isFile()) { // 单个文件:确保目标目录存在后用 QFile::copy QDir().mkpath(qDst); @@ -212,8 +212,10 @@ 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 复制链接本身而非目标内容。导出目录最终整体压缩写入 fd, + // zip 默认不跟随符号链接(仅存储链接路径),前端 unzip 也仅还原链接条目, + // 不存在跟随链接读到非预期内容或阻塞的风险。 + runProcess({"cp", "-rP", qSrc, qDst}); } } @@ -222,37 +224,6 @@ void OpsLogExport::execute_command(const QStringList &args, const string &output runProcess(args, output_file.c_str()); } -void OpsLogExport::setDirectoryPermissionsSafe(const std::string &dir_path) -{ - // 收缩到最小权限:仅 owner 可读写执行/写入,避免向 group 和 other 暴露导出目录。 - const QFile::Permissions dirPerms = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner; - const QFile::Permissions filePerms = QFile::ReadOwner | QFile::WriteOwner; - - const QFileInfo homeInfo(QString::fromStdString(home_dir)); - const uint ownerId = static_cast(homeInfo.ownerId()); - const uint groupId = static_cast(homeInfo.groupId()); - - 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); - return; - } - QFile::setPermissions(path, isDir ? dirPerms : filePerms); - }; - - applySafeOwnership(QString::fromStdString(dir_path), true); - - QDirIterator it(QString::fromStdString(dir_path), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - const QFileInfo &info = it.fileInfo(); - if (info.isSymLink()) - continue; - applySafeOwnership(info.filePath(), info.isDir()); - } -} - void OpsLogExport::createDirStruct() { // 创建目录结构 @@ -319,79 +290,14 @@ void OpsLogExport::createDirStruct() 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/"); @@ -402,11 +308,6 @@ 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/"); @@ -414,18 +315,11 @@ void OpsLogExport::exportAppLogs() // 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/"); } @@ -451,8 +345,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() @@ -472,12 +364,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/"); @@ -486,9 +375,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/"); @@ -496,9 +382,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 e22d27fb..a8ac63e9 100644 --- a/logViewerService/opslogexport.h +++ b/logViewerService/opslogexport.h @@ -10,19 +10,17 @@ class OpsLogExport { public: - OpsLogExport(const std::string& target, const std::string& home); + OpsLogExport(const std::string& target); void run(); private: std::string target_dir; - std::string home_dir; 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(); void exportAppLogs();