File indexing completed on 2024-04-21 09:17:08

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