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 | |