File indexing completed on 2024-04-21 16:12:17

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 "coredumpwatcher.h"
0007 
0008 #include <cerrno>
0009 #include <optional>
0010 #include <utility>
0011 
0012 #include <sys/resource.h>
0013 #include <sys/un.h>
0014 #include <unistd.h>
0015 
0016 #include "coredump.h"
0017 #include "socket.h"
0018 
0019 static std::optional<Coredump> makeDump(sd_journal *context)
0020 {
0021     auto cursorExpected = contextual_owning_ptr_call<char>(sd_journal_get_cursor, context, std::free);
0022     if (cursorExpected.ret != 0) {
0023         qFatal("Failed to get entry cursor");
0024         return std::nullopt;
0025     }
0026 
0027     Coredump::EntriesHash entries;
0028     const void *data = nullptr;
0029     size_t length = 0;
0030     SD_JOURNAL_FOREACH_DATA(context, data, length)
0031     {
0032         // size_t is uint, QBA uses int, make sure we don't overflow the int!
0033         int dataSize = static_cast<int>(length);
0034         Q_ASSERT(dataSize >= 0);
0035         Q_ASSERT(static_cast<quint64>(dataSize) == length);
0036 
0037         QByteArray entry(static_cast<const char *>(data), dataSize);
0038         const int offset = entry.indexOf('=');
0039         if (offset < 0) {
0040             qWarning() << "this entry looks funny it has no separating = character" << entry;
0041             continue;
0042         }
0043 
0044         const QByteArray key = entry.left(offset);
0045         if (key == QByteArrayLiteral("COREDUMP")) {
0046             // The literal COREDUMP= entry is the actual core when configured for journal storage in coredump.conf.
0047             // Synthesize a filename instead so we can use the same validity checks for all storage types.
0048             entries.insert(Coredump::keyFilename(), QByteArrayLiteral("/dev/null"));
0049             continue;
0050         }
0051 
0052         const QByteArray value = entry.mid(offset + 1);
0053 
0054         // Always add to raw data, they get serialized back into the INI file for drkonqi.
0055         entries.insert(key, value);
0056     }
0057 
0058     return std::make_optional<Coredump>(cursorExpected.value.get(), entries);
0059 }
0060 
0061 CoredumpWatcher::CoredumpWatcher(std::unique_ptr<sd_journal> context_, QString bootId_, const QString &instance_, QObject *parent)
0062     : QObject(parent)
0063     , context(std::move(context_))
0064     , bootId(std::move(bootId_))
0065     , instance(instance_)
0066     , instanceFilter(QStringLiteral("systemd-coredump@%1").arg(instance_))
0067 {
0068 }
0069 
0070 void CoredumpWatcher::processLog()
0071 {
0072     int i = 0;
0073     while (sd_journal_next(context.get()) > 0) {
0074         ++i;
0075         const auto optionalDump = makeDump(context.get());
0076         if (!optionalDump.has_value()) {
0077             qWarning() << "Failed to make a dump :O";
0078             continue;
0079         }
0080 
0081         const Coredump &dump = optionalDump.value();
0082         if (!dump.systemd_unit.startsWith(instanceFilter)) {
0083             // Older systemds have trouble templating a correct instance. We only
0084             // perform a startsWith check here, but will filter more aggressively
0085             // whenever possible via the constructor.
0086             continue;
0087         }
0088         if (dump.exe.isEmpty() && dump.filename.isEmpty()) {
0089             qDebug() << "Entry doesn't look like a dump. This may have been a vaccum run. Nothing to process.";
0090             // Do not finish here. Vaccum log entires are created from real coredump processes. We should eventually
0091             // find a dump.
0092             continue;
0093         }
0094 
0095         qDebug() << dump.exe << dump.pid << dump.filename;
0096 
0097         Q_EMIT newDump(dump);
0098 
0099         constexpr int maximumInBatch = 128; // give the event loop a chance to do other stuff as well
0100         if (i >= maximumInBatch) {
0101             // reschedule run
0102             QMetaObject::invokeMethod(this, &CoredumpWatcher::processLog, Qt::QueuedConnection);
0103             return;
0104         }
0105     }
0106     Q_EMIT atLogEnd();
0107 }
0108 
0109 void CoredumpWatcher::errnoError(const QString &msg, int err)
0110 {
0111     Q_EMIT error(msg + QStringLiteral(": (%1) ").arg(QString::number(err)) + QString::fromLocal8Bit(strerror(err)));
0112 }
0113 
0114 void CoredumpWatcher::start()
0115 {
0116     Q_ASSERT(context);
0117 
0118     sd_journal_flush_matches(context.get()); // reset match
0119     if (sd_journal_add_match(context.get(), "SYSLOG_IDENTIFIER=systemd-coredump", 0) != 0) {
0120         Q_EMIT error(QStringLiteral("Failed to install id match"));
0121         return;
0122     }
0123 
0124     if (!bootId.isEmpty()) {
0125         const QString bootIdMatch = QStringLiteral("_BOOT_ID=%1").arg(bootId);
0126         if (sd_journal_add_match(context.get(), qPrintable(bootIdMatch), 0) != 0) {
0127             Q_EMIT error(QStringLiteral("Failed to install boot id match"));
0128             return;
0129         }
0130     }
0131 
0132     if (!instance.isEmpty()) {
0133         if (instance.count(QLatin1Char('-')) >= 2) {
0134             // older systemds have a bug where %I doesn't actually expand correctly and only contains the first element.
0135             // This prevents us from matching through sd API. Instead processLog will filter based on the instance
0136             // information prefix. It's still unique enough.
0137             // Auto-generated instance names are of the form
0138             // $iid-$pid-$uid where iid is a growing instance id number.
0139             // Additionally we'll filter by chrono proximity, iids that are too far in the past will be discarded.
0140             // This is because iid on its own isn't necessarily unique in the event that it wraps around whatever
0141             // integer limit it has.
0142             if (sd_journal_add_match(context.get(), qPrintable(QStringLiteral("_SYSTEMD_UNIT=%1.service").arg(instanceFilter)), 0) != 0) {
0143                 Q_EMIT error(QStringLiteral("Failed to install unit match"));
0144                 return;
0145             }
0146         }
0147     }
0148 
0149     const int fd = sd_journal_get_fd(context.get());
0150     if (fd < 0) {
0151         errnoError(QStringLiteral("Failed to get listening socket"), -fd);
0152         return;
0153     }
0154 
0155     notifier = std::make_unique<QSocketNotifier>(fd, QSocketNotifier::Read);
0156     connect(notifier.get(), &QSocketNotifier::activated, this, [this] {
0157         if (sd_journal_process(context.get()) != SD_JOURNAL_APPEND) {
0158             return;
0159         }
0160         processLog();
0161     });
0162 
0163     if (int ret = sd_journal_seek_head(context.get()); ret != 0) {
0164         errnoError(QStringLiteral("Failed to go to tail"), -fd);
0165         return;
0166     }
0167     // Make sure to read whatever we have pending on next loop.
0168     QMetaObject::invokeMethod(this, &CoredumpWatcher::processLog);
0169 }