| 1 | /* |
| 2 | This file is part of the KDE libraries |
| 3 | SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> |
| 4 | SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> |
| 5 | SPDX-FileCopyrightText: 2023-2025 Harald Sitter <sitter@kde.org> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.0-only |
| 8 | */ |
| 9 | |
| 10 | #include "worker_p.h" |
| 11 | |
| 12 | #include <config-kiocore.h> |
| 13 | #include <qplatformdefs.h> |
| 14 | #include <stdio.h> |
| 15 | |
| 16 | #include <QCoreApplication> |
| 17 | #include <QDataStream> |
| 18 | #include <QDir> |
| 19 | #include <QFile> |
| 20 | #include <QLibraryInfo> |
| 21 | #include <QPluginLoader> |
| 22 | #include <QProcess> |
| 23 | #include <QStandardPaths> |
| 24 | #include <QTimer> |
| 25 | |
| 26 | #include <KLibexec> |
| 27 | #include <KLocalizedString> |
| 28 | |
| 29 | #include "commands_p.h" |
| 30 | #include "connection_p.h" |
| 31 | #include "connectionserver.h" |
| 32 | #include "dataprotocol_p.h" |
| 33 | #include "kioglobal_p.h" |
| 34 | #include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF |
| 35 | #include <kprotocolinfo.h> |
| 36 | |
| 37 | #include "kiocoredebug.h" |
| 38 | #include "workerbase.h" |
| 39 | #include "workerfactory.h" |
| 40 | #include "workerthread_p.h" |
| 41 | |
| 42 | using namespace Qt::StringLiterals; |
| 43 | using namespace KIO; |
| 44 | |
| 45 | static constexpr int s_workerConnectionTimeoutMin = 2; |
| 46 | |
| 47 | // Without debug info we consider it an error if the worker doesn't connect |
| 48 | // within 10 seconds. |
| 49 | // With debug info we give the worker an hour so that developers have a chance |
| 50 | // to debug their worker. |
| 51 | #ifdef NDEBUG |
| 52 | static constexpr int s_workerConnectionTimeoutMax = 10; |
| 53 | #else |
| 54 | static constexpr int s_workerConnectionTimeoutMax = 3600; |
| 55 | #endif |
| 56 | |
| 57 | void Worker::accept() |
| 58 | { |
| 59 | m_workerConnServer->setNextPendingConnection(m_connection); |
| 60 | m_workerConnServer->deleteLater(); |
| 61 | m_workerConnServer = nullptr; |
| 62 | |
| 63 | connect(sender: m_connection, signal: &Connection::readyRead, context: this, slot: &Worker::gotInput); |
| 64 | } |
| 65 | |
| 66 | void Worker::timeout() |
| 67 | { |
| 68 | if (m_dead) { // already dead? then workerDied was emitted and we are done |
| 69 | return; |
| 70 | } |
| 71 | if (m_connection->isConnected()) { |
| 72 | return; |
| 73 | } |
| 74 | |
| 75 | /*qDebug() << "worker failed to connect to application pid=" << m_pid |
| 76 | << " protocol=" << m_protocol;*/ |
| 77 | if (m_pid && KIOPrivate::isProcessAlive(pid: m_pid)) { |
| 78 | int delta_t = m_contact_started.elapsed() / 1000; |
| 79 | // qDebug() << "worker is slow... pid=" << m_pid << " t=" << delta_t; |
| 80 | if (delta_t < s_workerConnectionTimeoutMax) { |
| 81 | QTimer::singleShot(interval: 1000 * s_workerConnectionTimeoutMin, receiver: this, slot: &Worker::timeout); |
| 82 | return; |
| 83 | } |
| 84 | } |
| 85 | // qDebug() << "Houston, we lost our worker, pid=" << m_pid; |
| 86 | m_connection->close(); |
| 87 | m_dead = true; |
| 88 | QString arg = m_protocol; |
| 89 | if (!m_host.isEmpty()) { |
| 90 | arg += QLatin1String("://" ) + m_host; |
| 91 | } |
| 92 | // qDebug() << "worker failed to connect pid =" << m_pid << arg; |
| 93 | |
| 94 | ref(); |
| 95 | // Tell the job about the problem. |
| 96 | Q_EMIT error(ERR_WORKER_DIED, arg); |
| 97 | // Tell the scheduler about the problem. |
| 98 | Q_EMIT workerDied(worker: this); |
| 99 | // After the above signal we're dead!! |
| 100 | deref(); |
| 101 | } |
| 102 | |
| 103 | Worker::Worker(const QString &protocol, QObject *parent) |
| 104 | : WorkerInterface(parent) |
| 105 | , m_protocol(protocol) |
| 106 | , m_workerProtocol(protocol) |
| 107 | , m_workerConnServer(new KIO::ConnectionServer) |
| 108 | { |
| 109 | m_contact_started.start(); |
| 110 | m_workerConnServer->setParent(this); |
| 111 | m_workerConnServer->listenForRemote(); |
| 112 | if (!m_workerConnServer->isListening()) { |
| 113 | qCWarning(KIO_CORE) << "KIO Connection server not listening, could not connect" ; |
| 114 | } |
| 115 | m_connection = new Connection(Connection::Type::Application, this); |
| 116 | connect(sender: m_workerConnServer, signal: &ConnectionServer::newConnection, context: this, slot: &Worker::accept); |
| 117 | } |
| 118 | |
| 119 | Worker::~Worker() |
| 120 | { |
| 121 | // qDebug() << "destructing worker object pid =" << m_pid; |
| 122 | delete m_workerConnServer; |
| 123 | } |
| 124 | |
| 125 | QString Worker::protocol() const |
| 126 | { |
| 127 | return m_protocol; |
| 128 | } |
| 129 | |
| 130 | void Worker::setProtocol(const QString &protocol) |
| 131 | { |
| 132 | m_protocol = protocol; |
| 133 | } |
| 134 | |
| 135 | QString Worker::workerProtocol() const |
| 136 | { |
| 137 | return m_workerProtocol; |
| 138 | } |
| 139 | |
| 140 | QString Worker::host() const |
| 141 | { |
| 142 | return m_host; |
| 143 | } |
| 144 | |
| 145 | quint16 Worker::port() const |
| 146 | { |
| 147 | return m_port; |
| 148 | } |
| 149 | |
| 150 | QString Worker::user() const |
| 151 | { |
| 152 | return m_user; |
| 153 | } |
| 154 | |
| 155 | QString Worker::passwd() const |
| 156 | { |
| 157 | return m_passwd; |
| 158 | } |
| 159 | |
| 160 | void Worker::setIdle() |
| 161 | { |
| 162 | m_idleSince.start(); |
| 163 | } |
| 164 | |
| 165 | void Worker::ref() |
| 166 | { |
| 167 | m_refCount++; |
| 168 | } |
| 169 | |
| 170 | void Worker::deref() |
| 171 | { |
| 172 | m_refCount--; |
| 173 | if (!m_refCount) { |
| 174 | aboutToDelete(); |
| 175 | if (m_workerThread) { |
| 176 | // When on a thread, delete in a thread to prevent deadlocks between the main thread and the worker thread. |
| 177 | // This most notably can happen when the worker thread uses QDBus, because traffic will generally be routed |
| 178 | // through the main loop. |
| 179 | // Generally speaking we'd want to avoid waiting in the main thread anyway, the worker stopping isn't really |
| 180 | // useful for anything but delaying deletion. |
| 181 | // https://bugs.kde.org/show_bug.cgi?id=468673 |
| 182 | WorkerThread *workerThread = nullptr; |
| 183 | std::swap(a&: workerThread, b&: m_workerThread); |
| 184 | workerThread->setParent(nullptr); |
| 185 | connect(sender: workerThread, signal: &QThread::finished, context: workerThread, slot: &QThread::deleteLater); |
| 186 | workerThread->quit(); |
| 187 | } |
| 188 | delete this; // yes it reads funny, but it's too late for a deleteLater() here, no event loop anymore |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | void Worker::aboutToDelete() |
| 193 | { |
| 194 | m_connection->disconnect(receiver: this); |
| 195 | this->disconnect(); |
| 196 | } |
| 197 | |
| 198 | void Worker::setWorkerThread(WorkerThread *thread) |
| 199 | { |
| 200 | m_workerThread = thread; |
| 201 | } |
| 202 | |
| 203 | int Worker::idleTime() const |
| 204 | { |
| 205 | if (!m_idleSince.isValid()) { |
| 206 | return 0; |
| 207 | } |
| 208 | return m_idleSince.elapsed() / 1000; |
| 209 | } |
| 210 | |
| 211 | void Worker::setPID(qint64 pid) |
| 212 | { |
| 213 | m_pid = pid; |
| 214 | } |
| 215 | |
| 216 | qint64 Worker::worker_pid() const |
| 217 | { |
| 218 | return m_pid; |
| 219 | } |
| 220 | |
| 221 | void Worker::setJob(KIO::SimpleJob *job) |
| 222 | { |
| 223 | m_job = job; |
| 224 | } |
| 225 | |
| 226 | KIO::SimpleJob *Worker::job() const |
| 227 | { |
| 228 | return m_job; |
| 229 | } |
| 230 | |
| 231 | bool Worker::isAlive() const |
| 232 | { |
| 233 | return !m_dead; |
| 234 | } |
| 235 | |
| 236 | void Worker::suspend() |
| 237 | { |
| 238 | m_connection->suspend(); |
| 239 | } |
| 240 | |
| 241 | void Worker::resume() |
| 242 | { |
| 243 | m_connection->resume(); |
| 244 | } |
| 245 | |
| 246 | bool Worker::suspended() |
| 247 | { |
| 248 | return m_connection->suspended(); |
| 249 | } |
| 250 | |
| 251 | void Worker::send(int cmd, const QByteArray &arr) |
| 252 | { |
| 253 | m_connection->send(cmd, arr); |
| 254 | } |
| 255 | |
| 256 | void Worker::gotInput() |
| 257 | { |
| 258 | if (m_dead) { // already dead? then workerDied was emitted and we are done |
| 259 | return; |
| 260 | } |
| 261 | ref(); |
| 262 | if (!dispatch()) { |
| 263 | m_connection->close(); |
| 264 | m_dead = true; |
| 265 | QString arg = m_protocol; |
| 266 | if (!m_host.isEmpty()) { |
| 267 | arg += QLatin1String("://" ) + m_host; |
| 268 | } |
| 269 | // qDebug() << "worker died pid =" << m_pid << arg; |
| 270 | // Tell the job about the problem. |
| 271 | Q_EMIT error(ERR_WORKER_DIED, arg); |
| 272 | // Tell the scheduler about the problem. |
| 273 | Q_EMIT workerDied(worker: this); |
| 274 | } |
| 275 | deref(); |
| 276 | // Here we might be dead!! |
| 277 | } |
| 278 | |
| 279 | void Worker::kill() |
| 280 | { |
| 281 | m_dead = true; // OO can be such simple. |
| 282 | if (m_pid) { |
| 283 | qCDebug(KIO_CORE) << "killing worker process pid" << m_pid << "(" << m_protocol + QLatin1String("://" ) + m_host << ")" ; |
| 284 | KIOPrivate::sendTerminateSignal(pid: m_pid); |
| 285 | m_pid = 0; |
| 286 | } else if (m_workerThread) { |
| 287 | qCDebug(KIO_CORE) << "aborting worker thread for " << m_protocol + QLatin1String("://" ) + m_host; |
| 288 | m_workerThread->abort(); |
| 289 | } |
| 290 | deref(); |
| 291 | } |
| 292 | |
| 293 | void Worker::setHost(const QString &host, quint16 port, const QString &user, const QString &passwd) |
| 294 | { |
| 295 | m_host = host; |
| 296 | m_port = port; |
| 297 | m_user = user; |
| 298 | m_passwd = passwd; |
| 299 | |
| 300 | QByteArray data; |
| 301 | QDataStream stream(&data, QIODevice::WriteOnly); |
| 302 | stream << m_host << m_port << m_user << m_passwd; |
| 303 | m_connection->send(cmd: CMD_HOST, arr: data); |
| 304 | } |
| 305 | |
| 306 | void Worker::resetHost() |
| 307 | { |
| 308 | m_host = QStringLiteral("<reset>" ); |
| 309 | } |
| 310 | |
| 311 | void Worker::setConfig(const MetaData &config) |
| 312 | { |
| 313 | QByteArray data; |
| 314 | QDataStream stream(&data, QIODevice::WriteOnly); |
| 315 | stream << config; |
| 316 | m_connection->send(cmd: CMD_CONFIG, arr: data); |
| 317 | } |
| 318 | |
| 319 | /* |
| 320 | * Returns true if the worker should not be created because it would insecurely ask users for a password. |
| 321 | * false is returned when the worker is either safe because only the root user can write to it, or if this kio binary is already not secure. |
| 322 | */ |
| 323 | bool isWorkerSecurityCompromised(const QString &workerPath, const QString &protocolName, int &error, QString &error_text) |
| 324 | { |
| 325 | #ifdef Q_OS_WIN |
| 326 | return false; // This security check is not (yet?) implemented on Windows. |
| 327 | #endif |
| 328 | auto onlyRootHasWriteAccess = [](const QString &filePath) { |
| 329 | QFileInfo file(filePath); |
| 330 | return file.ownerId() == 0 && (file.groupId() == 0 || !file.permission(permissions: QFileDevice::WriteGroup)) && !file.permission(permissions: QFileDevice::WriteOther); |
| 331 | }; |
| 332 | if (onlyRootHasWriteAccess(workerPath)) { |
| 333 | return false; |
| 334 | } |
| 335 | |
| 336 | // The worker can be modified by non-privileged processes! If it ever asks for elevated privileges, this could lead to a privilege escalation! |
| 337 | // We will only let this slide if we are e.g. in a development environment. In a development environment the binaries are not system-installed, |
| 338 | // so this KIO library itself would also be writable by non-privileged processes. We check if this KIO library is safe from unprivileged tampering. |
| 339 | // If it is not, the security is already compromised anyway, so we ignore that the security of the worker binary is compromised as well. |
| 340 | std::optional<bool> kioCoreSecurityCompromised; |
| 341 | |
| 342 | QDir folderOfKioBinary{KLibexec::path(relativePath: QString{})}; |
| 343 | const QFileInfoList kioBinariesAndSymlinks = folderOfKioBinary.entryInfoList(nameFilters: {QLatin1String{"*KIOCore.so*" }}, filters: QDir::Files); |
| 344 | for (const QFileInfo &kioFile : kioBinariesAndSymlinks) { |
| 345 | if (onlyRootHasWriteAccess(kioFile.absoluteFilePath())) { |
| 346 | kioCoreSecurityCompromised = false; |
| 347 | break; // As long as there is at least one library which appears to be secure, we assume that the whole execution is supposed to be secure. |
| 348 | } else { |
| 349 | kioCoreSecurityCompromised = true; |
| 350 | // We have found a library that is compromised. We continue searching in case this library was only placed here to circumvent this security check. |
| 351 | } |
| 352 | } |
| 353 | const auto adminWorkerSecurityWarning{i18nc("@info %2 is a path" , |
| 354 | "The security of the KIO worker for protocol ’%1’, which typically asks for elevated permissions, " |
| 355 | "can not be guaranteed because users other than root have permission to modify it at %2." , |
| 356 | protocolName, |
| 357 | workerPath)}; |
| 358 | if (!kioCoreSecurityCompromised.has_value() || !kioCoreSecurityCompromised.value()) { |
| 359 | error_text = adminWorkerSecurityWarning; |
| 360 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 361 | return true; |
| 362 | } |
| 363 | // Both KIO as well as the worker can be written to by non-root objects, so there is no protection against these binaries being compromised. |
| 364 | // Notwithstanding, we let everything continue as normal because we assume this is a development environment. |
| 365 | qCInfo(KIO_CORE) << adminWorkerSecurityWarning; |
| 366 | return false; |
| 367 | } |
| 368 | |
| 369 | // TODO KF6: return std::unique_ptr |
| 370 | Worker *Worker::createWorker(const QString &protocol, const QUrl &url, int &error, QString &error_text) |
| 371 | { |
| 372 | Q_UNUSED(url) |
| 373 | // qDebug() << "createWorker" << protocol << "for" << url; |
| 374 | // Firstly take into account all special workers |
| 375 | if (protocol == QLatin1String("data" )) { |
| 376 | return new DataProtocol(); |
| 377 | } |
| 378 | |
| 379 | #ifdef BUILD_TESTING |
| 380 | if (protocol.startsWith("kio-test"_L1 )) { |
| 381 | auto *worker = new Worker(protocol); |
| 382 | const QUrl workerAddress = worker->m_workerConnServer->address(); |
| 383 | auto *thread = new WorkerThread(worker, s_testFactory.lock().get(), workerAddress.toString().toLocal8Bit()); |
| 384 | thread->start(); |
| 385 | worker->setWorkerThread(thread); |
| 386 | return worker; |
| 387 | } |
| 388 | #endif |
| 389 | |
| 390 | const QString _name = KProtocolInfo::exec(protocol); |
| 391 | if (_name.isEmpty()) { |
| 392 | error_text = i18n("Unknown protocol '%1'." , protocol); |
| 393 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 394 | return nullptr; |
| 395 | } |
| 396 | |
| 397 | // find the KIO worker using QPluginLoader; kioworker would do this |
| 398 | // anyway, but if it doesn't exist, we want to be able to return |
| 399 | // a useful error message immediately |
| 400 | QPluginLoader loader(_name); |
| 401 | const QString lib_path = loader.fileName(); |
| 402 | if (lib_path.isEmpty()) { |
| 403 | error_text = i18n("Can not find a KIO worker for protocol '%1'." , protocol); |
| 404 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 405 | return nullptr; |
| 406 | } |
| 407 | |
| 408 | if (protocol == QLatin1String("admin" ) && !lib_path.startsWith(s: QLatin1String{KDE_INSTALL_FULL_KIO_PLUGINDIR})) { |
| 409 | error_text = i18nc("@info %2 and %3 are paths" , |
| 410 | "The KIO worker for protocol “%1” in %2 was not loaded because all KIO workers which are located outside of %3 and ask for elevated " |
| 411 | "privileges are considered insecure." , |
| 412 | protocol, |
| 413 | lib_path, |
| 414 | QLatin1String{KDE_INSTALL_FULL_KIO_PLUGINDIR}); |
| 415 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 416 | return nullptr; |
| 417 | } |
| 418 | |
| 419 | auto *worker = new Worker(protocol); |
| 420 | const QUrl workerAddress = worker->m_workerConnServer->address(); |
| 421 | if (workerAddress.isEmpty()) { |
| 422 | error_text = i18n("Can not create a socket for launching a KIO worker for protocol '%1'." , protocol); |
| 423 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 424 | delete worker; |
| 425 | return nullptr; |
| 426 | } |
| 427 | |
| 428 | // Threads are enabled by default, set KIO_ENABLE_WORKER_THREADS=0 to disable them |
| 429 | const auto useThreads = []() { |
| 430 | return qgetenv(varName: "KIO_ENABLE_WORKER_THREADS" ) != "0" ; |
| 431 | }; |
| 432 | static bool bUseThreads = useThreads(); |
| 433 | |
| 434 | // Threads have performance benefits, but degrade robustness |
| 435 | // (a worker crashing kills the app). So let's only enable the feature for kio_file, for now. |
| 436 | if (protocol == QLatin1String("admin" ) || (bUseThreads && protocol == QLatin1String("file" ))) { |
| 437 | auto *factory = qobject_cast<WorkerFactory *>(object: loader.instance()); |
| 438 | if (factory) { |
| 439 | auto *thread = new WorkerThread(worker, factory, workerAddress.toString().toLocal8Bit()); |
| 440 | thread->start(); |
| 441 | worker->setWorkerThread(thread); |
| 442 | return worker; |
| 443 | } else { |
| 444 | qCWarning(KIO_CORE) << lib_path << "doesn't implement WorkerFactory?" ; |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | const QStringList args = QStringList{lib_path, protocol, QString(), workerAddress.toString()}; |
| 449 | // qDebug() << "kioworker" << ", " << lib_path << ", " << protocol << ", " << QString() << ", " << workerAddress; |
| 450 | |
| 451 | // search paths |
| 452 | QStringList searchPaths = KLibexec::kdeFrameworksPaths(QStringLiteral("libexec/kf6" )); |
| 453 | searchPaths.append(t: QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF)); // look at our installation location |
| 454 | QString kioworkerExecutable = QStandardPaths::findExecutable(QStringLiteral("kioworker" ), paths: searchPaths); |
| 455 | if (kioworkerExecutable.isEmpty()) { |
| 456 | // Fallback to PATH. On win32 we install to bin/ which tests outside |
| 457 | // KIO cannot not find at the time ctest is run because it |
| 458 | // isn't the same as applicationDirPath(). |
| 459 | kioworkerExecutable = QStandardPaths::findExecutable(QStringLiteral("kioworker" )); |
| 460 | } |
| 461 | if (kioworkerExecutable.isEmpty()) { |
| 462 | error_text = i18n("Can not find 'kioworker' executable at '%1'" , searchPaths.join(QLatin1String(", " ))); |
| 463 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
| 464 | delete worker; |
| 465 | return nullptr; |
| 466 | } |
| 467 | |
| 468 | qint64 pid = 0; |
| 469 | QProcess process; |
| 470 | process.setProgram(kioworkerExecutable); |
| 471 | process.setArguments(args); |
| 472 | #ifdef Q_OS_UNIX |
| 473 | process.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); |
| 474 | #endif |
| 475 | process.startDetached(pid: &pid); |
| 476 | worker->setPID(pid); |
| 477 | |
| 478 | return worker; |
| 479 | } |
| 480 | |
| 481 | #ifdef BUILD_TESTING |
| 482 | void KIO::Worker::setTestWorkerFactory(const std::weak_ptr<KIO::WorkerFactory> &factory) |
| 483 | { |
| 484 | s_testFactory = factory; |
| 485 | } |
| 486 | #endif |
| 487 | |
| 488 | #include "moc_worker_p.cpp" |
| 489 | |