File indexing completed on 2024-04-28 09:21:02

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 <QCommandLineParser>
0007 #include <QCoreApplication>
0008 #include <QDebug>
0009 #include <QFile>
0010 #include <QJsonDocument>
0011 #include <QLocalSocket>
0012 #include <QScopeGuard>
0013 
0014 #include <sys/resource.h>
0015 #include <sys/socket.h>
0016 #include <sys/un.h>
0017 #include <unistd.h>
0018 
0019 #include <coredump.h>
0020 #include <coredumpwatcher.h>
0021 #include <socket.h>
0022 
0023 using namespace Qt::StringLiterals;
0024 
0025 int main(int argc, char **argv)
0026 {
0027     QCoreApplication app(argc, argv);
0028     app.setApplicationName(QStringLiteral("drkonqi-coredump-helper"));
0029     app.setOrganizationDomain(QStringLiteral("kde.org"));
0030 
0031     // This binary is for internal use and intentionally has no i18n!
0032     QCommandLineParser parser;
0033     parser.addHelpOption();
0034     parser.addVersionOption();
0035     QCommandLineOption pickupOption("pickup"_L1, "Picking up old crashes, don't handle them if you can't tell if they were handled."_L1);
0036     parser.addOption(pickupOption);
0037     QCommandLineOption uidOption("uid"_L1, "UID to filter"_L1, "uid"_L1);
0038     parser.addOption(uidOption);
0039     QCommandLineOption bootIdOption("boot-id"_L1, "systemd boot id to filter"_L1, "bootId"_L1);
0040     parser.addOption(bootIdOption);
0041     QCommandLineOption instanceOption("instance"_L1, "systemd-coredump@.service instance to filter"_L1, "instance"_L1);
0042     parser.addOption(instanceOption);
0043     parser.process(app);
0044     const bool pickup = parser.isSet(pickupOption);
0045     const QString uid = parser.value(uidOption);
0046     const QString bootId = parser.value(bootIdOption);
0047     const QString instance = parser.value(instanceOption);
0048 
0049     auto expectedJournal = owning_ptr_call<sd_journal>(sd_journal_open, SD_JOURNAL_LOCAL_ONLY);
0050     Q_ASSERT(expectedJournal.ret == 0);
0051     Q_ASSERT(expectedJournal.value);
0052 
0053     CoredumpWatcher watcher(std::move(expectedJournal.value), bootId, instance, nullptr);
0054     if (!uid.isEmpty()) {
0055         watcher.addMatch(u"COREDUMP_UID=%1"_s.arg(uid));
0056     }
0057     QObject::connect(&watcher, &CoredumpWatcher::newDump, &app, [&watcher, pickup](const Coredump &dump) {
0058         if (pickup && !QFile::exists(dump.filename)) {
0059             // We only ignore missing cores when picking up old crashes. When dealing with new ones we may still wish
0060             // to notify that something has crashed, even when we can't debug it.
0061             return;
0062         }
0063 
0064         // We only try to find the socket file at this point in time because we need to know the UID and on older systemd's we'll not
0065         // be able to figure this out from just the instance information.
0066         // When systemd 245 (Ubuntu 20.04) no longer is out in the wild we can move this into the main and get the
0067         // uid from the instance.
0068         // TODO: move this to main scope as per the comment above
0069         const QByteArray socketPath = QByteArrayLiteral("/run/user/") + QByteArray::number(dump.uid) + QByteArrayLiteral("/drkonqi-coredump-launcher");
0070         if (!QFile::exists(QString::fromUtf8(socketPath))) {
0071             // This is intentionally not an error or fatal, not all users necessarily have
0072             // a socket or drkonqi!
0073             qWarning() << "The socket path doesn't exist @" << socketPath;
0074             Q_EMIT watcher.finished();
0075             return;
0076         }
0077 
0078         sockaddr_un sa{};
0079         sa.sun_family = AF_UNIX;
0080         // size_t is signed, ensure path is too
0081         Q_ASSERT(socketPath.size() >= 0);
0082         const std::make_unsigned<decltype(socketPath.size())>::type pathSize = socketPath.size();
0083         if (pathSize > sizeof(sa.sun_path) /* '>' because we need an extra byte for null */) {
0084             Q_EMIT watcher.error(QStringLiteral("The socket path has too many characters:") + QString::fromLatin1(socketPath));
0085             return;
0086         }
0087         strncpy(static_cast<char *>(sa.sun_path), socketPath.constData(), sizeof(sa.sun_path));
0088 
0089         const int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
0090         if (fd < 0) {
0091             Q_EMIT watcher.error(QString::fromLocal8Bit(strerror(errno)));
0092             Q_UNREACHABLE();
0093         }
0094         QScopeGuard closeFD([fd] {
0095             close(fd);
0096         });
0097 
0098         if (::connect(fd, (sockaddr *)&sa, sizeof(sa)) < 0) { // NOLINT
0099             Q_EMIT watcher.error(QString::fromLocal8Bit(strerror(errno)));
0100             return;
0101         }
0102 
0103         // Convert the raw data to JSON and send that over the socket. This means
0104         // the client side doesn't need to talk to journald again. A tad more efficient,
0105         // and it makes nary a difference in code.
0106         QVariantMap variantMap;
0107         if (pickup) { // forward this into the launcher so it can choose to not have dump trucks handle dumps without metadata
0108             variantMap.insert(QString::fromUtf8(Coredump::keyPickup()), "TRUE"_ba);
0109         }
0110         for (auto it = dump.m_rawData.cbegin(); it != dump.m_rawData.cend(); ++it) {
0111             variantMap.insert(QString::fromUtf8(it.key()), it.value());
0112         }
0113 
0114         QLocalSocket s;
0115         s.setSocketDescriptor(fd, QLocalSocket::ConnectedState, QLocalSocket::WriteOnly);
0116         QByteArray data = QJsonDocument::fromVariant(variantMap).toJson();
0117         while (!data.isEmpty()) {
0118             // NB: we need to constrain the segment size to not run into QLocalSocket::DatagramTooLargeError
0119             const qint64 written = s.write(data.constData(), std::min<int>(data.size(), Socket::DatagramSize));
0120             if (written > 0) {
0121                 data = data.mid(written);
0122                 s.waitForBytesWritten();
0123             } else if (s.state() != QLocalSocket::ConnectedState) {
0124                 qWarning() << "socket state unexpectedly" << s.state() << "aborting crash processing";
0125                 qApp->quit();
0126                 return;
0127             }
0128         }
0129         s.flush();
0130         s.waitForBytesWritten();
0131 
0132         Q_EMIT watcher.finished();
0133         return;
0134     });
0135 
0136     QObject::connect(&watcher, &CoredumpWatcher::finished, &app, &QCoreApplication::quit, Qt::QueuedConnection);
0137     QObject::connect(
0138         &watcher,
0139         &CoredumpWatcher::error,
0140         &app,
0141         [](const QString &msg) {
0142             qWarning() << msg;
0143             qApp->exit(1);
0144         },
0145         Qt::QueuedConnection);
0146     watcher.start();
0147 
0148     return app.exec();
0149 }