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"