File indexing completed on 2024-05-19 05:29:24

0001 /*
0002     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "coredumpbackend.h"
0008 
0009 #include <chrono>
0010 
0011 #include <KCrash>
0012 
0013 #include <QDBusConnection>
0014 #include <QDBusConnectionInterface>
0015 #include <QDBusMessage>
0016 #include <QDBusPendingCallWatcher>
0017 #include <QDBusReply>
0018 #include <QDebug>
0019 #include <QJsonDocument>
0020 #include <QJsonObject>
0021 #include <QProcess>
0022 #include <QScopeGuard>
0023 
0024 #include <unistd.h>
0025 
0026 // TODO: refactor the entire stack such that logs extraction happens in the processor and gets passed along the chain
0027 #include <systemd/sd-journal.h>
0028 
0029 #include <coredump.h>
0030 
0031 #include "bugzillaintegration/reportinterface.h"
0032 #include "crashedapplication.h"
0033 #include "debugger.h"
0034 #include "debuggermanager.h"
0035 #include "drkonqi.h"
0036 #include "drkonqi_debug.h"
0037 #include "linuxprocmapsparser.h"
0038 #include <coredumpexcavator.h>
0039 
0040 using namespace std::chrono_literals;
0041 using namespace Qt::StringLiterals;
0042 
0043 using namespace std::chrono_literals;
0044 using namespace Qt::StringLiterals;
0045 
0046 namespace
0047 {
0048 // Special backend type for core-based debugging. A polkit helper excavates the core file for us and we then
0049 // invoke gdb on it. Side stepping coredumpctl.
0050 constexpr auto CORE_BACKEND_TYPE = "coredump-core"_L1;
0051 
0052 [[nodiscard]] QString errnoError(const QString &msg, int err)
0053 {
0054     return msg + u": (%1) "_s.arg(QString::number(err)) + QString::fromLocal8Bit(strerror(err));
0055 }
0056 
0057 QList<EntriesHash> collectLogs(const QByteArray &cursor, sd_journal *context, const QStringList &matches)
0058 {
0059     Q_ASSERT(context);
0060 
0061     // - Reset all matches
0062     // - seek to the cursor of the crash
0063     // - install all matches to filter only log output from the crashed app
0064     // - grab last N messages
0065 
0066     sd_journal_flush_matches(context);
0067     if (auto err = sd_journal_seek_cursor(context, cursor.constData()); err != 0) {
0068         qCWarning(DRKONQI_LOG) << errnoError(u"Failed to seek to cursor"_s, -err);
0069         return {};
0070     }
0071 
0072     // make the cursor the current entry so we can read its time
0073     if (auto err = sd_journal_next(context); err < 0) {
0074         qCWarning(DRKONQI_LOG) << errnoError(u"Failed to read cursor"_s, -err);
0075         return {};
0076     }
0077 
0078     uint64_t core_time = 0;
0079     sd_id128_t boot_id;
0080     if (auto err = sd_journal_get_monotonic_usec(context, &core_time, &boot_id); err != 0) {
0081         qCWarning(DRKONQI_LOG) << errnoError(u"Failed to get core time"_s, -err);
0082         return {};
0083     }
0084 
0085     for (const auto &match : matches) {
0086         if (sd_journal_add_match(context, qUtf8Printable(match), 0) != 0) {
0087             qCWarning(DRKONQI_LOG) << "Failed to install custom match:" << match;
0088             return {};
0089         }
0090     }
0091 
0092     QList<EntriesHash> blobs;
0093     constexpr auto maxBacklog = 30;
0094     for (int i = 0; i < maxBacklog; i++) {
0095         if (auto err = sd_journal_previous(context); err <= 0) { // end of data
0096             if (err < 0) { // error
0097                 qCWarning(DRKONQI_LOG) << errnoError(u"Failed to seek previous entry"_s, -err);
0098             }
0099             break;
0100         }
0101 
0102         uint64_t time = 0;
0103         if (auto err = sd_journal_get_monotonic_usec(context, &time, &boot_id); err != 0) {
0104             qCWarning(DRKONQI_LOG) << errnoError(u"Failed to get entry time"_s, -err);
0105             break; // don't know when this entry is from, assume it's old
0106         }
0107 
0108         std::chrono::microseconds timeDelta(core_time - time);
0109         if (timeDelta > 8s) {
0110             break;
0111         }
0112 
0113         EntriesHash blob;
0114         const void *data = nullptr;
0115         size_t length = 0;
0116         SD_JOURNAL_FOREACH_DATA(context, data, length)
0117         {
0118             // size_t is uint, QBA uses int, make sure we don't overflow the int!
0119             int dataSize = static_cast<int>(length);
0120             Q_ASSERT(dataSize >= 0);
0121             Q_ASSERT(static_cast<quint64>(dataSize) == length);
0122 
0123             QByteArray entry(static_cast<const char *>(data), dataSize);
0124             const auto offset = entry.indexOf('=');
0125             if (offset < 0) {
0126                 qCWarning(DRKONQI_LOG) << "this entry looks funny it has no separating = character" << entry;
0127                 continue;
0128             }
0129 
0130             const QByteArray key = entry.left(offset);
0131             const QByteArray value = entry.mid(offset + 1);
0132 
0133             blob.emplace(key, value);
0134         }
0135         blobs.push_back(blob);
0136     }
0137 
0138     return blobs;
0139 }
0140 } // namespace
0141 
0142 bool CoredumpBackend::init()
0143 {
0144     Q_ASSERT(!metadataPath().isEmpty());
0145     Q_ASSERT_X(QFile::exists(metadataPath()), static_cast<const char *>(Q_FUNC_INFO), qUtf8Printable(metadataPath()));
0146     qCDebug(DRKONQI_LOG) << "loading metadata" << metadataPath();
0147 
0148     QFile file(metadataPath());
0149     if (!file.open(QFile::ReadOnly)) {
0150         return false;
0151     }
0152     auto document = QJsonDocument::fromJson(file.readAll());
0153     const auto journal = document[u"journal"_s].toObject().toVariantHash();
0154     for (auto [key, value] : journal.asKeyValueRange()) {
0155         m_journalEntry.insert(key.toUtf8(), value.toByteArray());
0156     }
0157     // conceivably the file contains no Journal group for unknown reasons
0158     Q_ASSERT_X(!m_journalEntry.isEmpty(), static_cast<const char *>(Q_FUNC_INFO), qUtf8Printable(metadataPath()));
0159 
0160     AbstractDrKonqiBackend::init(); // calls constructCrashedApplication -> we need to have our members set before calling it
0161 
0162     if (crashedApplication()->pid() <= 0) {
0163         qCWarning(DRKONQI_LOG) << "Invalid pid specified or it wasn't found in journald.";
0164         return false;
0165     }
0166 
0167     connect(ReportInterface::self(), &ReportInterface::crashEventSent, this, [] {
0168         QFile file(metadataPath());
0169         if (!file.open(QFile::ReadWrite)) {
0170             qCWarning(DRKONQI_LOG) << "Failed to open for writing" << metadataPath();
0171             return;
0172         }
0173         auto object = QJsonDocument::fromJson(file.readAll()).object();
0174         constexpr auto DRKONQI_KEY = "drkonqi"_L1;
0175         auto drkonqiObject = object[DRKONQI_KEY].toObject();
0176         drkonqiObject[u"sentryEventId"_s] = ReportInterface::self()->sentryEventId();
0177         object[DRKONQI_KEY] = drkonqiObject;
0178         file.reset();
0179         file.write(QJsonDocument(object).toJson());
0180     });
0181 
0182     return true;
0183 }
0184 
0185 CrashedApplication *CoredumpBackend::constructCrashedApplication()
0186 {
0187     Q_ASSERT(!m_journalEntry.isEmpty());
0188 
0189     bool ok = false;
0190     // Journald timestamps are always micro seconds, coredumpd takes care to make sure this also the case for its timestamp.
0191     const std::chrono::microseconds timestamp(m_journalEntry["COREDUMP_TIMESTAMP"].toLong(&ok));
0192     Q_ASSERT(ok);
0193     const auto datetime = QDateTime::fromMSecsSinceEpoch(std::chrono::duration_cast<std::chrono::milliseconds>(timestamp).count());
0194     Q_ASSERT(ok);
0195     const QFileInfo executable(QString::fromUtf8(m_journalEntry["COREDUMP_EXE"]));
0196     const int signal = m_journalEntry["COREDUMP_SIGNAL"].toInt(&ok);
0197     Q_ASSERT(ok);
0198     const bool hasDeletedFiles = LinuxProc::hasMapsDeletedFiles(executable.filePath(), m_journalEntry["COREDUMP_PROC_MAPS"], LinuxProc::Check::Stat);
0199 
0200     Q_ASSERT_X(m_journalEntry["COREDUMP_PID"].toInt() == DrKonqi::pid(),
0201                static_cast<const char *>(Q_FUNC_INFO),
0202                qPrintable(QStringLiteral("journal: %1, drkonqi: %2").arg(QString::fromUtf8(m_journalEntry["COREDUMP_PID"]), QString::number(DrKonqi::pid()))));
0203 
0204     auto expectedJournal = owning_ptr_call<sd_journal>(sd_journal_open, SD_JOURNAL_LOCAL_ONLY);
0205     Q_ASSERT(expectedJournal.ret == 0);
0206     Q_ASSERT(expectedJournal.value);
0207 
0208     m_crashedApplication = std::make_unique<CrashedApplication>(DrKonqi::pid(),
0209                                                                 DrKonqi::thread(),
0210                                                                 signal,
0211                                                                 executable,
0212                                                                 DrKonqi::appVersion(),
0213                                                                 BugReportAddress(DrKonqi::bugAddress()),
0214                                                                 DrKonqi::programName(),
0215                                                                 DrKonqi::productName(),
0216                                                                 datetime,
0217                                                                 DrKonqi::isRestarted(),
0218                                                                 hasDeletedFiles);
0219 
0220     m_crashedApplication->m_logs = collectLogs(m_journalEntry[Coredump::keyCursor()],
0221                                                expectedJournal.value.get(),
0222                                                {
0223                                                    u"_PID=%1"_s.arg(QString::fromUtf8(m_journalEntry["COREDUMP_PID"])),
0224                                                    u"_UID=%1"_s.arg(QString::fromUtf8(m_journalEntry["COREDUMP_UID"])),
0225                                                    u"_COMM=%1"_s.arg(QString::fromUtf8(m_journalEntry["COREDUMP_COMM"])),
0226                                                    u"_EXE=%1"_s.arg(QString::fromUtf8(m_journalEntry["COREDUMP_EXE"])),
0227                                                    u"_BOOT_ID=%1"_s.arg(QString::fromUtf8(m_journalEntry["_BOOT_ID"])),
0228                                                });
0229 
0230     qCDebug(DRKONQI_LOG) << "Executable is:" << executable.absoluteFilePath();
0231     qCDebug(DRKONQI_LOG) << "Executable exists:" << executable.exists();
0232 
0233     return m_crashedApplication.get();
0234 }
0235 
0236 DebuggerManager *CoredumpBackend::constructDebuggerManager()
0237 {
0238     const QList<Debugger> internalDebuggers = Debugger::availableInternalDebuggers(CORE_BACKEND_TYPE);
0239     const QList<Debugger> externalDebuggers = Debugger::availableExternalDebuggers(CORE_BACKEND_TYPE);
0240 
0241     const Debugger preferredDebugger(Debugger::findDebugger(internalDebuggers, QStringLiteral("gdb")));
0242     qCDebug(DRKONQI_LOG) << "Using debugger:" << preferredDebugger.codeName();
0243 
0244     m_debuggerManager = new DebuggerManager(preferredDebugger, externalDebuggers, this);
0245     return m_debuggerManager;
0246 }
0247 
0248 void CoredumpBackend::prepareForDebugger()
0249 {
0250     if (m_excavator) {
0251         m_excavator->excavateFrom(QString::fromUtf8(m_journalEntry["COREDUMP_FILENAME"]));
0252         return;
0253     }
0254 
0255     m_excavator = std::make_unique<AutomaticCoredumpExcavator>();
0256     connect(m_excavator.get(), &AutomaticCoredumpExcavator::failed, this, [this] {
0257         Q_EMIT failedToPrepare();
0258     });
0259     connect(m_excavator.get(), &AutomaticCoredumpExcavator::excavated, this, [this](const QString &corePath) {
0260         m_crashedApplication->m_coreFile = corePath;
0261         Q_EMIT preparedForDebugger();
0262     });
0263     m_excavator->excavateFrom(QString::fromUtf8(m_journalEntry["COREDUMP_FILENAME"]));
0264 }
0265 
0266 #include "moc_coredumpbackend.cpp"