KIO

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

KDE's Doxygen guidelines are available online.