File indexing completed on 2024-04-28 05:45:58

0001 /*
0002     SPDX-FileCopyrightText: 2017-2022 Andrius Štikonas <andrius@stikonas.eu>
0003     SPDX-FileCopyrightText: 2018 Huzaifa Faruqui <huzaifafaruqui@gmail.com>
0004     SPDX-FileCopyrightText: 2018 Caio Jordão Carvalho <caiojcarvalho@gmail.com>
0005     SPDX-FileCopyrightText: 2018-2019 Harald Sitter <sitter@kde.org>
0006     SPDX-FileCopyrightText: 2018 Simon Depiets <sdepiets@gmail.com>
0007     SPDX-FileCopyrightText: 2019 Shubham Jangra <aryan100jangid@gmail.com>
0008     SPDX-FileCopyrightText: 2020 David Edmundson <kde@davidedmundson.co.uk>
0009 
0010     SPDX-License-Identifier: GPL-3.0-or-later
0011 */
0012 
0013 #include "externalcommandhelper.h"
0014 #include "externalcommand_whitelist.h"
0015 
0016 #include <filesystem>
0017 
0018 #include <fcntl.h>
0019 
0020 #include <QtDBus>
0021 
0022 #include <QCoreApplication>
0023 #include <QDebug>
0024 #include <QDir>
0025 #include <QElapsedTimer>
0026 #include <QFile>
0027 #include <QFileInfo>
0028 #include <QString>
0029 #include <QVariant>
0030 
0031 #include <KLocalizedString>
0032 #include <PolkitQt1/Authority>
0033 #include <PolkitQt1/Subject>
0034 
0035 #include <polkitqt1-version.h>
0036 
0037 /** Initialize ExternalCommandHelper Daemon and prepare DBus interface
0038  *
0039  * This helper runs in the background until all applications using it exit.
0040  * If helper is not busy then it exits when the client services gets
0041  * unregistered. In case the client crashes, the helper waits
0042  * for the current job to finish before exiting, to avoid leaving partially moved data.
0043  *
0044  * This helper starts DBus interface where it listens to command execution requests.
0045  * New clients connecting to the helper have to authenticate using Polkit.
0046 */
0047 
0048 ExternalCommandHelper::ExternalCommandHelper()
0049 {
0050     if (!QDBusConnection::systemBus().registerObject(QStringLiteral("/Helper"), this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
0051         exit(-1);
0052     }
0053 
0054     if (!QDBusConnection::systemBus().registerService(QStringLiteral("org.kde.kpmcore.helperinterface"))) {
0055         exit(-1);
0056     }
0057 
0058     // we know this service must be registered already as DBus policy blocks calls from anyone else
0059     m_serviceWatcher = new QDBusServiceWatcher(this);
0060     m_serviceWatcher->setConnection(QDBusConnection ::systemBus());
0061     m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
0062 
0063     connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, qApp, [this](const QString &service) {
0064         m_serviceWatcher->removeWatchedService(service);
0065         if (m_serviceWatcher->watchedServices().isEmpty()) {
0066             qApp->quit();
0067         }
0068     });
0069 }
0070 
0071 /** Reads the given number of bytes from the sourceDevice into the given buffer.
0072     @param sourceDevice device or file to read from
0073     @param buffer buffer to store the bytes read in
0074     @param offset offset where to begin reading
0075     @param size the number of bytes to read
0076     @return true on success
0077 */
0078 bool ExternalCommandHelper::readData(QFile& device, QByteArray& buffer, const qint64 offset, const qint64 size)
0079 {
0080     if (!device.isOpen()) {
0081         if (!device.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) {
0082             qCritical() << xi18n("Could not open device <filename>%1</filename> for reading.", device.fileName());
0083             return false;
0084         }
0085     }
0086 
0087     // Sequential devices such as /dev/zero or /dev/urandom return false on seek().
0088     if (!device.isSequential() && !device.seek(offset)) {
0089         qCritical() << xi18n("Could not seek position %1 on device <filename>%2</filename>.", offset, device.fileName());
0090         return false;
0091     }
0092 
0093     buffer = device.read(size);
0094 
0095     if (size != buffer.size()) {
0096         qCritical() << xi18n("Could not read from device <filename>%1</filename>.", device.fileName());
0097         return false;
0098     }
0099 
0100     return true;
0101 }
0102 
0103 /** Writes the data from buffer to a given device.
0104     @param device device or file to write to
0105     @param buffer the data that we write
0106     @param offset offset where to begin writing
0107     @return true on success
0108 */
0109 bool ExternalCommandHelper::writeData(QFile& device, const QByteArray& buffer, const qint64 offset)
0110 {
0111     auto flags = QIODevice::WriteOnly | QIODevice::Unbuffered;
0112     if (!device.isOpen()) {
0113         if (!device.open(flags)) {
0114             qCritical() << xi18n("Could not open device <filename>%1</filename> for writing.", device.fileName());
0115             return false;
0116         }
0117     }
0118 
0119     if (!device.seek(offset)) {
0120         qCritical() << xi18n("Could not seek position %1 on device <filename>%2</filename>.", offset, device.fileName());
0121         return false;
0122     }
0123 
0124     if (device.write(buffer) != buffer.size()) {
0125         qCritical() << xi18n("Could not write to device <filename>%1</filename>.", device.fileName());
0126         return false;
0127     }
0128 
0129     return true;
0130 }
0131 
0132 /** Creates a new fstab file with given contents.
0133     @param Contents the data that we write
0134     @return true on success
0135 */
0136 bool ExternalCommandHelper::WriteFstab(const QByteArray& fstabContents)
0137 {
0138     if (!isCallerAuthorized()) {
0139         return false;
0140     }
0141     if (fstabContents.size() > MiB) {
0142         qCritical() << QStringLiteral("/etc/fstab size limit exceeded.");
0143         return false;
0144     }
0145     QString fstabPath = QStringLiteral("/etc/fstab");
0146     QFile fstabFile(fstabPath);
0147 
0148     // WriteOnly implies O_TRUNC
0149     auto flags = QIODevice::WriteOnly | QIODevice::Unbuffered;
0150     if (!fstabFile.open(flags)) {
0151         qCritical() << xi18n("Could not open file <filename>%1</filename> for writing.", fstabPath);
0152         return false;
0153     }
0154 
0155     if (fstabFile.write(fstabContents) != fstabContents.size()) {
0156         qCritical() << xi18n("Could not write to file <filename>%1</filename>.", fstabPath);
0157         return false;
0158     }
0159 
0160     return true;
0161 }
0162 
0163 // If targetDevice is empty then return QByteArray with data that was read from disk.
0164 QVariantMap ExternalCommandHelper::CopyFileData(const QString& sourceDevice, const qint64 sourceOffset, const qint64 sourceLength, const QString& targetDevice, const qint64 targetOffset, const qint64 chunkSize)
0165 {
0166     if (!isCallerAuthorized()) {
0167         return {};
0168     }
0169 
0170     // Avoid division by zero further down
0171     if (!chunkSize) {
0172         return {};
0173     }
0174 
0175     // Prevent some out of memory situations
0176     if (chunkSize > 100 * MiB) {
0177         return {};
0178     }
0179 
0180     // Check for relative paths
0181     std::filesystem::path sourcePath(sourceDevice.toStdU16String());
0182     std::filesystem::path targetPath(targetDevice.toStdU16String());
0183     if(sourcePath.is_relative() || targetPath.is_relative()) {
0184         return {};
0185     }
0186 
0187     // Only allow writing to existing files.
0188     if(!std::filesystem::exists(targetPath)) {
0189         return {};
0190     }
0191 
0192     QVariantMap reply;
0193     reply[QStringLiteral("success")] = true;
0194 
0195     // This enum specified whether individual data chunks are moved left or right
0196     // When source and target devices are the same we have to be careful not to overwrite
0197     // source data with newly written data. We don't have to do this if sourceDevice is not
0198     // targetDevice but there are no disadvantages in applying the same scheme.
0199     // When partition is moved to the left, we start with the leftmost chunk,
0200     // and move it further left, then second leftmost chunk and so on.
0201     // But when we move partition to the right, we start with rightmost chunk.
0202     // To account for this difference, we introduce CopyDirection variable which takes
0203     // care of some of the differences in offset calculation between these two cases.
0204     enum CopyDirection : qint8 {
0205         Left = 1,
0206         Right = -1,
0207     };
0208     qint8 copyDirection = targetOffset > sourceOffset ? CopyDirection::Right : CopyDirection::Left;
0209 
0210     // Let readOffset (r) and writeOffset (w) be the offsets of the first chunk that we move.
0211     // When we move data to the left:
0212     // ______target______         ______source______
0213     // r                     <-   w=================
0214     qint64 readOffset = sourceOffset;
0215     qint64 writeOffset = targetOffset;
0216 
0217     // When we move data to the right, we start moving data from the last chunk
0218     // ______source______         ______target______
0219     // =================r    ->                    w
0220     if (copyDirection == CopyDirection::Right) {
0221         readOffset = sourceOffset + sourceLength - chunkSize;
0222         writeOffset = targetOffset + sourceLength - chunkSize;
0223     }
0224 
0225     const qint64 chunksToCopy = sourceLength / chunkSize;
0226     const qint64 lastBlock = sourceLength % chunkSize;
0227 
0228     qint64 bytesWritten = 0;
0229     qint64 chunksCopied = 0;
0230 
0231     QByteArray buffer;
0232     int percent = 0;
0233     QElapsedTimer timer;
0234 
0235     timer.start();
0236 
0237     QString reportText = xi18nc("@info:progress", "Copying %1 chunks (%2 bytes) from %3 to %4, direction: %5.", chunksToCopy,
0238                                               sourceLength, readOffset, writeOffset, copyDirection == CopyDirection::Left ? i18nc("direction: left", "left")
0239                                               : i18nc("direction: right", "right"));
0240     Q_EMIT report(reportText);
0241 
0242     bool rval = true;
0243 
0244     QFile target(targetDevice);
0245     QFile source(sourceDevice);
0246     while (chunksCopied < chunksToCopy) {
0247         if (!(rval = readData(source, buffer, readOffset + chunkSize * chunksCopied * copyDirection, chunkSize)))
0248             break;
0249 
0250         if (!(rval = writeData(target, buffer, writeOffset + chunkSize * chunksCopied * copyDirection)))
0251             break;
0252 
0253         bytesWritten += buffer.size();
0254 
0255         if (++chunksCopied * 100 / chunksToCopy != percent) {
0256             percent = chunksCopied * 100 / chunksToCopy;
0257 
0258             if (percent % 5 == 0 && timer.elapsed() > 1000) {
0259                 const qint64 mibsPerSec = (chunksCopied * chunkSize / 1024 / 1024) / (timer.elapsed() / 1000);
0260                 const qint64 estSecsLeft = (100 - percent) * timer.elapsed() / percent / 1000;
0261                 reportText = xi18nc("@info:progress", "Copying %1 MiB/second, estimated time left: %2", mibsPerSec, QTime(0, 0).addSecs(estSecsLeft).toString());
0262                 Q_EMIT report(reportText);
0263             }
0264             Q_EMIT progress(percent);
0265         }
0266     }
0267 
0268     // copy the remainder
0269     if (rval && lastBlock > 0) {
0270         Q_ASSERT(lastBlock < chunkSize);
0271 
0272         const qint64 lastBlockReadOffset = copyDirection == CopyDirection::Left ? readOffset + chunkSize * chunksCopied : sourceOffset;
0273         const qint64 lastBlockWriteOffset = copyDirection == CopyDirection::Left ? writeOffset + chunkSize * chunksCopied : targetOffset;
0274         reportText = xi18nc("@info:progress", "Copying remainder of chunk size %1 from %2 to %3.", lastBlock, lastBlockReadOffset, lastBlockWriteOffset);
0275         Q_EMIT report(reportText);
0276         rval = readData(source, buffer, lastBlockReadOffset, lastBlock);
0277 
0278         if (rval) {
0279             rval = writeData(target, buffer, lastBlockWriteOffset);
0280         }
0281 
0282         if (rval) {
0283             Q_EMIT progress(100);
0284             bytesWritten += buffer.size();
0285         }
0286     }
0287 
0288     reportText = xi18ncp("@info:progress argument 2 is a string such as 7 bytes (localized accordingly)", "Copying 1 chunk (%2) finished.", "Copying %1 chunks (%2) finished.", chunksCopied, i18np("1 byte", "%1 bytes", bytesWritten));
0289     Q_EMIT report(reportText);
0290 
0291     reply[QStringLiteral("success")] = rval;
0292     return reply;
0293 }
0294 
0295 QByteArray ExternalCommandHelper::ReadData(const QString& device, const qint64 offset, const qint64 length)
0296 {
0297     if (!isCallerAuthorized()) {
0298         return {};
0299     }
0300 
0301     if (length > MiB) {
0302         return {};
0303     }
0304     if (!std::filesystem::is_block_file(device.toStdU16String())) {
0305         qWarning() << "Not a block device";
0306         return {};
0307     }
0308 
0309     // Do not follow symlinks
0310     QFileInfo info(device);
0311     if (info.isSymbolicLink()) {
0312         qWarning() << "ReadData: device should not be symbolic link";
0313         return {};
0314     }
0315     if (device.left(5) != QStringLiteral("/dev/") || device.left(9) != QStringLiteral("/dev/shm/")) {
0316         qWarning() << "Error: trying to read data from device not in /dev";
0317         return {};
0318     }
0319 
0320     QByteArray buffer;
0321     QFile sourceDevice;
0322     int fd = open(device.toLocal8Bit().constData(), O_NOFOLLOW);
0323     // Negative numbers are error codes
0324     if (fd < 0) {
0325         qWarning() << "Error: failed to open device " << device;
0326         return QByteArray();
0327     }
0328     bool rval = sourceDevice.open(fd, QIODevice::ReadOnly | QIODevice::Unbuffered);
0329     rval = rval && readData(sourceDevice, buffer, offset, length);
0330     close(fd);
0331     if (rval) {
0332         return buffer;
0333     }
0334     return QByteArray();
0335 }
0336 
0337 bool ExternalCommandHelper::WriteData(const QByteArray& buffer, const QString& targetDevice, const qint64 targetOffset)
0338 {
0339     if (!isCallerAuthorized()) {
0340         return false;
0341     }
0342     // Do not allow using this helper for writing to arbitrary location
0343     if ( targetDevice.left(5) != QStringLiteral("/dev/") )
0344         return false;
0345 
0346     auto targetPath = std::filesystem::path(targetDevice.toStdU16String());
0347     if (!std::filesystem::is_block_file(targetDevice.toStdU16String())) {
0348         qWarning() << "Not a block device";
0349         return {};
0350     }
0351 
0352     auto canonicalTargetPath = std::filesystem::canonical(targetPath);
0353     // TODO: Qt6 supports std::filesystem::path
0354     QFile device(QLatin1String(canonicalTargetPath.c_str()));
0355     return writeData(device, buffer, targetOffset);
0356 }
0357 
0358 QVariantMap ExternalCommandHelper::RunCommand(const QString& command, const QStringList& arguments, const QByteArray& input, const int processChannelMode)
0359 {
0360     if (!isCallerAuthorized()) {
0361         return {};
0362     }
0363 
0364     QVariantMap reply;
0365     reply[QStringLiteral("success")] = false;
0366 
0367     if (command.isEmpty()) {
0368         return reply;
0369     }
0370 
0371     // Compare with command whitelist
0372     QFileInfo fileInfo(command);
0373     QString basename = fileInfo.fileName();
0374     if (allowedCommands.find(basename) == allowedCommands.end()) { // TODO: C++20: replace with contains
0375         qInfo() << command << "command is not one of the whitelisted commands";
0376         reply[QStringLiteral("success")] = false;
0377         return reply;
0378     }
0379 
0380     // Make sure command is located in the trusted prefix
0381     QDir prefix = fileInfo.absoluteDir();
0382     QString dirname = prefix.dirName();
0383     if (dirname == QStringLiteral("bin") || dirname == QStringLiteral("sbin")) {
0384         prefix.cdUp();
0385     }
0386     if (trustedPrefixes.find(prefix.path()) == trustedPrefixes.end()) { // TODO: C++20: replace with contains
0387         qInfo() << prefix.path() << "prefix is not one of the trusted command prefixes";
0388         reply[QStringLiteral("success")] = false;
0389         return reply;
0390     }
0391 
0392 //  connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput);
0393 
0394     QProcess cmd;
0395     cmd.setEnvironment( { QStringLiteral("LVM_SUPPRESS_FD_WARNINGS=1") } );
0396 
0397     if((processChannelMode != QProcess::SeparateChannels) && (processChannelMode != QProcess::MergedChannels)) {
0398         return reply;
0399     }
0400     cmd.setProcessChannelMode(static_cast<QProcess::ProcessChannelMode>(processChannelMode));
0401     cmd.start(command, arguments);
0402     cmd.write(input);
0403     cmd.closeWriteChannel();
0404     cmd.waitForFinished(-1);
0405     QByteArray output = cmd.readAllStandardOutput();
0406     reply[QStringLiteral("output")] = output;
0407     reply[QStringLiteral("exitCode")] = cmd.exitCode();
0408 
0409     reply[QStringLiteral("success")] = true;
0410     return reply;
0411 }
0412 
0413 void ExternalCommandHelper::onReadOutput()
0414 {
0415 /*    const QByteArray s = cmd.readAllStandardOutput();
0416 
0417       if(output.length() > 10*1024*1024) { // prevent memory overflow for badly corrupted file systems
0418         if (report())
0419             report()->line() << xi18nc("@info:status", "(Command is printing too much output)");
0420             return;
0421      }
0422 
0423      output += s;
0424 
0425      if (report())
0426          *report() << QString::fromLocal8Bit(s);*/
0427 }
0428 
0429 bool ExternalCommandHelper::isCallerAuthorized()
0430 {
0431     if (!calledFromDBus()) {
0432         return false;
0433     }
0434 
0435     // Cache successful authentication requests, so that clients don't need
0436     // to authenticate multiple times during long partitioning operations.
0437     // auth_admin_keep is not used intentionally because with current architecture
0438     // it might lead to data loss if user cancels sfdisk partition boundary adjustment
0439     // after partition data was moved.
0440     if (m_serviceWatcher->watchedServices().contains(message().service())) {
0441         return true;
0442     }
0443 
0444     PolkitQt1::SystemBusNameSubject subject(message().service());
0445     PolkitQt1::Authority *authority = PolkitQt1::Authority::instance();
0446 
0447     PolkitQt1::Authority::Result result;
0448     QEventLoop e;
0449     connect(authority, &PolkitQt1::Authority::checkAuthorizationFinished, &e, [&e, &result](PolkitQt1::Authority::Result _result) {
0450         result = _result;
0451         e.quit();
0452     });
0453 
0454     authority->checkAuthorization(QStringLiteral("org.kde.kpmcore.externalcommand.init"), subject, PolkitQt1::Authority::AllowUserInteraction);
0455     e.exec();
0456 
0457     if (authority->hasError()) {
0458         qDebug() << "Encountered error while checking authorization, error code:" << authority->lastError() << authority->errorDetails();
0459         authority->clearError();
0460     }
0461 
0462     switch (result) {
0463     case PolkitQt1::Authority::Yes:
0464         // track who called into us so we can close when all callers have gone away
0465         m_serviceWatcher->addWatchedService(message().service());
0466         return true;
0467     default:
0468         sendErrorReply(QDBusError::AccessDenied);
0469         if (m_serviceWatcher->watchedServices().isEmpty())
0470             qApp->quit();
0471         return false;
0472     }
0473 }
0474 
0475 int main(int argc, char ** argv)
0476 {
0477     QCoreApplication app(argc, argv);
0478     ExternalCommandHelper helper;
0479     app.exec();
0480 }
0481 
0482 #include "moc_externalcommandhelper.cpp"