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
42using namespace Qt::StringLiterals;
43using namespace KIO;
44
45static 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
52static constexpr int s_workerConnectionTimeoutMax = 10;
53#else
54static constexpr int s_workerConnectionTimeoutMax = 3600;
55#endif
56
57void 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
66void 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
103Worker::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
119Worker::~Worker()
120{
121 // qDebug() << "destructing worker object pid =" << m_pid;
122 delete m_workerConnServer;
123}
124
125QString Worker::protocol() const
126{
127 return m_protocol;
128}
129
130void Worker::setProtocol(const QString &protocol)
131{
132 m_protocol = protocol;
133}
134
135QString Worker::workerProtocol() const
136{
137 return m_workerProtocol;
138}
139
140QString Worker::host() const
141{
142 return m_host;
143}
144
145quint16 Worker::port() const
146{
147 return m_port;
148}
149
150QString Worker::user() const
151{
152 return m_user;
153}
154
155QString Worker::passwd() const
156{
157 return m_passwd;
158}
159
160void Worker::setIdle()
161{
162 m_idleSince.start();
163}
164
165void Worker::ref()
166{
167 m_refCount++;
168}
169
170void 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
192void Worker::aboutToDelete()
193{
194 m_connection->disconnect(receiver: this);
195 this->disconnect();
196}
197
198void Worker::setWorkerThread(WorkerThread *thread)
199{
200 m_workerThread = thread;
201}
202
203int Worker::idleTime() const
204{
205 if (!m_idleSince.isValid()) {
206 return 0;
207 }
208 return m_idleSince.elapsed() / 1000;
209}
210
211void Worker::setPID(qint64 pid)
212{
213 m_pid = pid;
214}
215
216qint64 Worker::worker_pid() const
217{
218 return m_pid;
219}
220
221void Worker::setJob(KIO::SimpleJob *job)
222{
223 m_job = job;
224}
225
226KIO::SimpleJob *Worker::job() const
227{
228 return m_job;
229}
230
231bool Worker::isAlive() const
232{
233 return !m_dead;
234}
235
236void Worker::suspend()
237{
238 m_connection->suspend();
239}
240
241void Worker::resume()
242{
243 m_connection->resume();
244}
245
246bool Worker::suspended()
247{
248 return m_connection->suspended();
249}
250
251void Worker::send(int cmd, const QByteArray &arr)
252{
253 m_connection->send(cmd, arr);
254}
255
256void 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
279void 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
293void 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
306void Worker::resetHost()
307{
308 m_host = QStringLiteral("<reset>");
309}
310
311void 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 */
323bool 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
370Worker *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
482void 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

source code of kio/src/core/worker.cpp