File indexing completed on 2024-04-21 05:26:43

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #include "linuxprocmapsparser.h"
0007 
0008 #include <sys/stat.h>
0009 #include <sys/types.h>
0010 #include <unistd.h>
0011 
0012 #include <cerrno>
0013 #include <cstring>
0014 
0015 #include <QByteArrayList>
0016 #include <QDebug>
0017 #include <QFile>
0018 #include <QRegularExpression>
0019 #include <QStorageInfo>
0020 
0021 #include "drkonqi_debug.h"
0022 
0023 using namespace Qt::StringLiterals;
0024 
0025 bool LinuxProc::isLibraryPath(const QString &path)
0026 {
0027     // This intentionally matches potential suffixes, i.e. "/usr/lib/foo.so.0" but also "foo.so (deleted)"
0028     static QRegularExpression soExpression(QStringLiteral("(?<path>.+\\.(so|py)([^/]*))$"));
0029     const auto soMatch = soExpression.match(path);
0030     return soMatch.isValid() && soMatch.hasMatch() && !soMatch.captured(u"path").isEmpty();
0031 }
0032 
0033 bool LinuxProc::hasMapsDeletedFiles(const QString &exePathString, const QByteArray &maps, Check check)
0034 {
0035     const QByteArray exePath = QFile::encodeName(exePathString);
0036     const QByteArrayList lines = maps.split('\n');
0037     for (const auto &line : lines) {
0038         if (line.isEmpty()) {
0039             continue;
0040         }
0041         // Walk string by tokens. This is by far the easiest way to parse the format as anything after
0042         // the first 5 fields (minus the tokens) is the pathname. The pathname may be nothing, or contain more
0043         // spaces in turn. Qt has no convenient API for this, use strtok.
0044 
0045         QByteArray mutableLine = line;
0046         // address
0047         std::ignore = strtok(mutableLine.data(), " ");
0048         // perms
0049         std::ignore = strtok(nullptr, " ");
0050         // offset
0051         std::ignore = strtok(nullptr, " ");
0052         // dev
0053         std::ignore = strtok(nullptr, " ");
0054         // inode
0055         const QByteArray inodeString(strtok(nullptr, " "));
0056         bool ok = false;
0057         const ino_t inode = inodeString.toULongLong(&ok);
0058         if (!ok) {
0059             qCWarning(DRKONQI_LOG) << "inode appears to not be a ulonglong";
0060         }
0061         // remainder is the pathname
0062         const QByteArray pathname = QByteArray(strtok(nullptr, "\n")).simplified(); // simplify to make evaluation easier
0063 
0064         if (pathname.isEmpty() || pathname.at(0) != QLatin1Char('/')) {
0065             // Could be pseudo entry like [heap] or anonymous region.
0066             continue;
0067         }
0068 
0069         qCDebug(DRKONQI_LOG) << "Checking for file status" << pathname;
0070 
0071         if (pathname.startsWith(QByteArrayLiteral("/memfd"))) {
0072             // Qml.so's JIT shows up under memfd. This is a false positive as it isn't a real path in the
0073             // file system. Skip over it.
0074             continue;
0075         }
0076 
0077         const QByteArray deletedMarker = QByteArrayLiteral(" (deleted)");
0078         // We filter only .so files to ensure that we don't trip over cache files or the like.
0079         // NB: includes .so* and .py* since we also implicitly support snakes to
0080         //   a degree
0081         // As a result we need to explicitly look for the main executable.
0082         if (pathname == exePath + deletedMarker) {
0083             qCWarning(DRKONQI_LOG) << "Found deleted exe marker" << pathname;
0084             return true;
0085         }
0086 
0087         if (pathname != exePath && !isLibraryPath(QFile::decodeName(pathname))) {
0088             continue; // not exe and not a library.
0089         }
0090 
0091         // Deleted marker always declares something missing. Even when we perform additional stat checks on it.
0092         if (pathname.endsWith(deletedMarker)) {
0093             qCWarning(DRKONQI_LOG) << "Found deleted library marker" << pathname;
0094             return true;
0095         }
0096 
0097         switch (check) {
0098         case Check::DeletedMarker: {
0099             // If we get here the file hasn't been marked deleted.
0100             break;
0101         }
0102         case Check::Stat: {
0103             // Bit unfortunate this but when the file is on an overlayFS the entire stat validation system is unreliable.
0104             // The inode in the /maps is the actual underlying inode, not the st_ino we'll get when stating through
0105             // the overlay.
0106             if (QStorageInfo storageInfo(QString::fromUtf8(pathname)); storageInfo.fileSystemType().contains("overlay"_L1)) {
0107                 qCDebug(DRKONQI_LOG) << "Couldn't perform stat check because file is on overlayfs" << pathname;
0108                 break;
0109             }
0110 
0111             struct stat info {
0112             };
0113             const int ret = stat(pathname.constData(), &info);
0114             if (ret == -1) {
0115                 qCWarning(DRKONQI_LOG) << "Couldn't stat file, assuming it was deleted" << pathname << strerror(errno);
0116                 return true;
0117                 break;
0118             }
0119 
0120             if (info.st_ino != inode) {
0121                 qCWarning(DRKONQI_LOG) << "Found mismatching inode on" << pathname << info.st_ino << inode;
0122                 return true;
0123                 break;
0124             }
0125 
0126             // It's very awkward but st_dev seems dodgy at least with btrfs. The dev_t the kernel has is not the one
0127             // stat has and what's more the kernel has one that solid doesn't know about either. That may simply be
0128             // because btrfs makes up fake dev_ts since multiple btrfs subvolumes may be on the same block device.
0129             // Anyway, it's unfortunate but I guess we had best not look at the device.
0130         } break;
0131         }
0132     }
0133 
0134     return false;
0135 }