File indexing completed on 2024-04-14 15:39:43

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0003 
0004 #include "helper.h"
0005 
0006 #include <QDebug>
0007 #include <QFileInfo>
0008 #include <QProcess>
0009 
0010 #include <chrono>
0011 
0012 #include <errno.h>
0013 #include <fcntl.h>
0014 #include <string.h>
0015 #include <sys/stat.h>
0016 #include <sys/types.h>
0017 #include <unistd.h>
0018 
0019 // Append name to /dev/ and ensure it is a trustable block device.
0020 static QString nameToPath(const QString &name)
0021 {
0022     if (name.isEmpty()) {
0023         return {};
0024     }
0025 
0026     // This also excludes relative path shenanigans as they'd all need to contain a separator.
0027     if (name.contains(QLatin1Char('/'))) {
0028         qWarning() << "Device names must not contain slashes";
0029         return {};
0030     }
0031 
0032     const QString path = QStringLiteral("/dev/%1").arg(name);
0033 
0034     struct stat sb;
0035     if (lstat(QFile::encodeName(path), &sb) == -1) {
0036         const int err = errno;
0037         qWarning() << "Failed to stat block device" << name << strerror(err);
0038         return {};
0039     }
0040 
0041 #ifdef Q_OS_FREEBSD // There are only character devices
0042     if (!S_ISCHR(sb.st_mode)) {
0043 #else // On others assume they are block devices (e.g. linux)
0044     if (!S_ISBLK(sb.st_mode)) {
0045 #endif
0046         qWarning() << "Device is not actually a block device" << name;
0047         return {};
0048     }
0049 
0050     if (sb.st_uid != 0) {
0051         qWarning() << "Device is not owned by root" << name;
0052         return {};
0053     }
0054 
0055     return path;
0056 }
0057 
0058 ActionReply SMARTHelper::smartctl(const QVariantMap &args)
0059 {
0060     // For security reasons we only accept fully resolved device names which
0061     // we use to construct the final /dev/$name path.
0062     const QString name = args.value(QStringLiteral("deviceName")).toString();
0063     const QString devicePath = nameToPath(name);
0064     if (devicePath.isEmpty()) {
0065         return ActionReply::HelperErrorReply();
0066     }
0067 
0068     // PATH is super minimal when invoked through dbus
0069     setenv("PATH", "/usr/sbin:/sbin:/usr/local/sbin", 1);
0070 
0071     const QString command = QStringLiteral("smartctl");
0072     const QString all = QStringLiteral("--all");
0073 
0074     // JSON output.
0075     QProcess jsonProcess;
0076     // json=c is badly documented and means "gimme json but don't pretty print"
0077     jsonProcess.start(command, {all, QStringLiteral("--json=c"), devicePath}, QProcess::ReadOnly);
0078 
0079     // Diagnostic CLI output for advanced users.
0080     QProcess cliProcess;
0081     cliProcess.start(command, {all, devicePath}, QProcess::ReadOnly);
0082 
0083     // Wait for 120 seconds + 5 seconds leeway.
0084     // This allows us to ideally let smartctl time out internally and still
0085     // construct a json blob if possible. The kernel ioctl timeout for nvme
0086     // apparently is 60 seconds and internal smartctl timeouts also largely
0087     // appear to be in that range but there may be more than one :|
0088     // https://bugs.kde.org/show_bug.cgi?id=428844
0089     using namespace std::chrono_literals;
0090     if (!jsonProcess.waitForFinished(std::chrono::milliseconds(125s).count())) {
0091         qDebug() << "jsonProcess has not finished in time";
0092     }
0093     // CLI was also running. Give it a bit of extra time still in case there was resource contention.
0094     if (!cliProcess.waitForFinished(std::chrono::milliseconds(10s).count())) {
0095         qDebug() << "cliProcess has not finished in time";
0096     }
0097 
0098     ActionReply reply;
0099     reply.addData(QStringLiteral("exitCode"), jsonProcess.exitCode());
0100     reply.addData(QStringLiteral("data"), jsonProcess.readAllStandardOutput());
0101     reply.addData(QStringLiteral("cliExitCode"), cliProcess.exitCode());
0102     reply.addData(QStringLiteral("cliData"), cliProcess.readAllStandardOutput());
0103     return reply;
0104 }
0105 
0106 KAUTH_HELPER_MAIN("org.kde.kded.smart", SMARTHelper)