File indexing completed on 2024-12-08 10:52:35

0001 /*
0002     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #include <QCoreApplication>
0007 #include <QDebug>
0008 #include <QFile>
0009 #include <QJsonDocument>
0010 #include <QLibraryInfo>
0011 #include <QPluginLoader>
0012 #include <QProcess>
0013 #include <QScopeGuard>
0014 #include <QSettings>
0015 #include <QStandardPaths>
0016 
0017 #include <cerrno>
0018 #include <chrono>
0019 #include <memory>
0020 #include <tuple>
0021 #include <utility>
0022 
0023 #include <poll.h>
0024 #include <systemd/sd-daemon.h>
0025 #include <unistd.h>
0026 
0027 #include "../coredump.h"
0028 #include "../metadata.h"
0029 #include "../socket.h"
0030 #include "DumpTruckInterface.h"
0031 
0032 using namespace std::chrono_literals;
0033 
0034 static QString drkonqiExe()
0035 {
0036     // Borrowed from kcrash.cpp
0037     static QStringList paths = QFile::decodeName(qgetenv("LIBEXEC_PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts)
0038         + QStringList{
0039             QCoreApplication::applicationDirPath(), // then look where our application binary is located
0040             QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath), // look where libexec path is (can be set in qt.conf)
0041             QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR), // look at our installation location
0042         };
0043     static QString exec = QStandardPaths::findExecutable(QStringLiteral("drkonqi"), paths);
0044     return exec;
0045 }
0046 
0047 using ArgumentsPidTuple = std::tuple<QStringList, bool>;
0048 
0049 static ArgumentsPidTuple metadataArguments(const Coredump &dump, const QString &metadataPath)
0050 {
0051     QStringList arguments;
0052     bool foundPID = false;
0053 
0054     // Parse the metadata file. Ideally we'd should even stop passing a gazillion options
0055     // and instead rely on this file, then drkonqi
0056     // would also be in charge of removing it instead of us here.
0057     QSettings metadata(metadataPath, QSettings::IniFormat);
0058     metadata.beginGroup(QStringLiteral("KCrash"));
0059     const QStringList keys = metadata.allKeys();
0060     for (const QString &key : keys) {
0061         const QString value = metadata.value(key).toString();
0062 
0063         if (key == QLatin1String("exe")) {
0064             if (value.endsWith(QStringLiteral("/drkonqi"))) {
0065                 qWarning() << "drkonqi crashed, we aren't going to invoke it again, we might be the reason it crashd :O";
0066                 return {};
0067             }
0068             if (value != dump.exe) {
0069                 qWarning() << "the exe in the metadata file doesn't match the exe in the journal entry! aborting" << value << dump.exe;
0070                 return {};
0071             }
0072             // exe purely exists for our benefit, don't forward it to drkonqi.
0073             continue;
0074         }
0075 
0076         if (key == QLatin1String("pid")) {
0077             foundPID = true;
0078         }
0079 
0080         arguments << QStringLiteral("--%1").arg(key);
0081         if (value != QLatin1String("true") && value != QLatin1String("false")) { // not a bool value, append as arg
0082             arguments << value;
0083         }
0084     }
0085     metadata.endGroup();
0086 
0087     return {arguments, foundPID};
0088 }
0089 
0090 static ArgumentsPidTuple includeAllArguments(const Coredump &dump)
0091 {
0092     // Synthesize drkonqi arguments from the dump alone, this has limited functionality and is meant for use with
0093     // non-KDE apps.
0094     QStringList arguments;
0095     arguments << QStringLiteral("--signal") << QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_SIGNAL")));
0096     arguments << QStringLiteral("--pid") << QString::fromUtf8(dump.m_rawData.value(QByteArrayLiteral("COREDUMP_PID")));
0097     arguments << QStringLiteral("--restarted"); // Cannot restart foreign apps!
0098     return {arguments, true};
0099 }
0100 
0101 static ArgumentsPidTuple includeKWinWaylandArguments(const Coredump &dump)
0102 {
0103     auto [arguments, foundPID] = includeAllArguments(dump);
0104     arguments << QStringLiteral("--bugaddress") << QStringLiteral("submit@bugs.kde.org") << QStringLiteral("--appname") << dump.exe;
0105     return {arguments, foundPID};
0106 }
0107 
0108 static bool tryDrkonqi(const Coredump &dump)
0109 {
0110     const QString metadataPath = Metadata::resolveMetadataPath(dump.pid);
0111     const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("drkonqirc"));
0112 
0113     // Arm removal. If we return early this will ensure clean up, otherwise we dismiss it later
0114     auto deleteFile = qScopeGuard([metadataPath] {
0115         QFile::remove(metadataPath);
0116     });
0117 
0118     if (qEnvironmentVariableIsSet("KDE_DEBUG")) {
0119         qWarning() << "KDE_DEBUG set. Not invoking DrKonqi.";
0120         return false;
0121     }
0122 
0123     if (drkonqiExe().isEmpty()) {
0124         qWarning() << "Couldn't find drkonqi exe";
0125         return false;
0126     }
0127 
0128     QStringList arguments;
0129     bool foundPID = false;
0130 
0131     if (!metadataPath.isEmpty()) {
0132         // A KDE app crash has metadata, build arguments from that
0133         std::tie(arguments, foundPID) = metadataArguments(dump, metadataPath);
0134     } else if (dump.exe.endsWith(QLatin1String("/kwin_wayland"))) {
0135         // When the compositor goes down it may not have time to store metadata, in that case we'll fake them.
0136         std::tie(arguments, foundPID) = includeKWinWaylandArguments(dump);
0137     } else if (QSettings(configFile, QSettings::IniFormat).value(QStringLiteral("IncludeAll")).toBool()) {
0138         // Handle non-KDE apps when in IncludeAll mode.
0139         std::tie(arguments, foundPID) = includeAllArguments(dump);
0140     } else {
0141         return false;
0142     }
0143 
0144     if (arguments.isEmpty() || !foundPID) {
0145         // There is a chance that somehow the metadata file writing failed or is incomplete. Do some trivial
0146         // checks to catch and ignore such cases. Otherwise we risk drkonqi crashing due to internally failed
0147         // assertions.
0148         qWarning() << "Failed to read metadata from crash" << arguments << foundPID;
0149         return false;
0150     }
0151 
0152     // Append Coredump data. This allow us to not have to talk to journald again on the drkonqi side.
0153     QSettings metadata(Metadata::metadataPath(dump.pid), QSettings::IniFormat);
0154     metadata.beginGroup(QStringLiteral("Journal"));
0155     for (auto it = dump.m_rawData.cbegin(); it != dump.m_rawData.cend(); ++it) {
0156         metadata.setValue(QString::fromUtf8(it.key()), it.value());
0157     }
0158     metadata.endGroup();
0159     metadata.sync();
0160 
0161     setenv("DRKONQI_BACKEND", "COREDUMPD", 1);
0162     setenv("DRKONQI_METADATA_FILE", qPrintable(metadataPath), 1);
0163 
0164     deleteFile.dismiss(); // let drkonqi handle cleanup if we get here
0165 
0166     // We must start drkonqi in a new slice. This launcher will want to terminate quickly and we enforce that
0167     // through maximum run time in the unit configuration. If drkonqi wasn't in a new slice it'd get killed with us.
0168     QProcess::execute(drkonqiExe(), arguments);
0169 
0170     return true; // always considered handled, even if drkonqi crashes or something
0171 }
0172 
0173 class DrKonqiTruck : public DumpTruckInterface
0174 {
0175 public:
0176     DrKonqiTruck() = default;
0177     ~DrKonqiTruck() override = default;
0178 
0179     bool handle(const Coredump &dump) override
0180     {
0181         if (!QFile::exists(dump.filename)) {
0182             QFile::remove(Metadata::resolveMetadataPath(dump.pid)); // without trace we'll never run drkonqi as it can't file a bug anyway
0183             return false;
0184         }
0185 
0186         return tryDrkonqi(dump);
0187     }
0188 
0189 private:
0190     Q_DISABLE_COPY_MOVE(DrKonqiTruck)
0191 };
0192 
0193 static void onNewDump(const Coredump &dump)
0194 {
0195     static DrKonqiTruck drkonqi;
0196     if (drkonqi.handle(dump)) {
0197         return;
0198     }
0199 
0200     if (qEnvironmentVariableIntValue("KDE_COREDUMP_NOTIFY") >= 1) {
0201         // Developers need to explicitly opt into the notifications. They
0202         // have no l10n and are also uniquely useless to users.
0203         static QPluginLoader loader(QStringLiteral("drkonqi/KDECoredumpNotifierTruck"));
0204         if (!loader.load()) {
0205             qWarning() << "failed to load" << loader.fileName() << loader.errorString();
0206             return;
0207         }
0208         auto notifier = qobject_cast<DumpTruckInterface *>(loader.instance());
0209         Q_ASSERT(notifier);
0210         if (notifier->handle(dump)) {
0211             return;
0212         }
0213     }
0214 
0215     qWarning() << "Nothing handled the dump :O";
0216 }
0217 
0218 int main(int argc, char **argv)
0219 {
0220     QCoreApplication app(argc, argv);
0221     app.setApplicationName(QStringLiteral("drkonqi-coredump-launcher"));
0222     app.setOrganizationDomain(QStringLiteral("kde.org"));
0223 
0224     if (sd_listen_fds(false) != 1) {
0225         qFatal("Not exactly one fd passed by systemd. Quel malheur!");
0226         return 1;
0227     }
0228 
0229     // Reading is very awkward and I'm not even sure why.
0230     // It seems like QLocalSocket doesn't manage to model stream sockets properly or at least not SOCK_SEQPACKET.
0231     // The socket on our end never notices that the remote has closed and even terminated already.
0232     //
0233     // Since we don't really need to do anything fancy we'll simply poll on our own instead of relying on QLS.
0234 
0235     QByteArray json;
0236     QByteArray segment;
0237     segment.resize(Socket::DatagramSize);
0238     while (true) {
0239         struct pollfd poll {
0240         };
0241         poll.fd = SD_LISTEN_FDS_START;
0242         poll.events = POLLIN;
0243         struct timespec time {
0244         };
0245         time.tv_nsec = (2500ns).count();
0246         ppoll(&poll, 1, &time, nullptr);
0247 
0248         if (poll.revents & POLLERR) {
0249             qFatal("Socket had an error");
0250             break;
0251         }
0252 
0253         if (poll.revents & POLLNVAL) {
0254             qFatal("Socket was invalid");
0255             break;
0256         }
0257 
0258         if (poll.revents & POLLIN) {
0259             const size_t size = read(poll.fd, segment.data(), segment.size());
0260             if (size == 0 && poll.revents & POLLHUP) {
0261                 break; // zero read + POLLHUP = EOS says the manpage
0262             }
0263             json.append(segment.data(), size);
0264         }
0265     }
0266     close(SD_LISTEN_FDS_START);
0267 
0268     QJsonParseError error{};
0269     const QJsonDocument document = QJsonDocument::fromJson(json, &error);
0270     if (error.error != QJsonParseError::NoError) {
0271         qWarning() << "json parse error" << error.errorString();
0272         return 1;
0273     }
0274 
0275     // Unset a slew of systemd variables.
0276     unsetenv("JOURNAL_STREAM");
0277     unsetenv("INVOCATION_ID");
0278     unsetenv("LISTEN_FDNAMES");
0279     unsetenv("LISTEN_FDS");
0280     unsetenv("LISTEN_PID");
0281     unsetenv("MANAGERPID");
0282 
0283     onNewDump(Coredump(document));
0284 
0285     return 0;
0286 }