summaryrefslogtreecommitdiffstats
path: root/src/manager-lib/sudo.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/manager-lib/sudo.cpp')
-rw-r--r--src/manager-lib/sudo.cpp495
1 files changed, 495 insertions, 0 deletions
diff --git a/src/manager-lib/sudo.cpp b/src/manager-lib/sudo.cpp
new file mode 100644
index 00000000..a9d661fe
--- /dev/null
+++ b/src/manager-lib/sudo.cpp
@@ -0,0 +1,495 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 Luxoft Sweden AB
+** Copyright (C) 2018 Pelagicore AG
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Luxoft Application Manager.
+**
+** $QT_BEGIN_LICENSE:LGPL-QTAS$
+** Commercial License Usage
+** Licensees holding valid commercial Qt Automotive Suite licenses may use
+** this file in accordance with the commercial license agreement provided
+** with the Software or, alternatively, in accordance with the terms
+** contained in a written agreement between you and The Qt Company. For
+** licensing terms and conditions see https://www.qt.io/terms-conditions.
+** For further information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+** SPDX-License-Identifier: LGPL-3.0
+**
+****************************************************************************/
+
+
+#include <QProcess>
+#include <QDir>
+#include <QFile>
+#include <QtEndian>
+#include <QDataStream>
+#include <qplatformdefs.h>
+#include <QDataStream>
+
+#include "logging.h"
+#include "sudo.h"
+#include "utilities.h"
+#include "exception.h"
+#include "global.h"
+
+#include <errno.h>
+
+#if defined(Q_OS_LINUX)
+# include "processtitle.h"
+
+# include <fcntl.h>
+# include <unistd.h>
+# include <sys/socket.h>
+# include <sys/errno.h>
+# include <sys/ioctl.h>
+# include <sys/stat.h>
+# include <sys/prctl.h>
+# include <linux/capability.h>
+
+// These two functions are implemented in glibc, but the header file is
+// in the separate libcap-dev package. Since we want to avoid unnecessary
+// dependencies, we just declare them here
+extern "C" int capset(cap_user_header_t header, cap_user_data_t data);
+extern "C" int capget(cap_user_header_t header, const cap_user_data_t data);
+
+// Support for old/broken C libraries
+# if defined(_LINUX_CAPABILITY_VERSION) && !defined(_LINUX_CAPABILITY_VERSION_1)
+# define _LINUX_CAPABILITY_VERSION_1 _LINUX_CAPABILITY_VERSION
+# define _LINUX_CAPABILITY_U32S_1 1
+# if !defined(CAP_TO_INDEX)
+# define CAP_TO_INDEX(x) ((x) >> 5)
+# endif
+# if !defined(CAP_TO_MASK)
+# define CAP_TO_MASK(x) (1 << ((x) & 31))
+# endif
+# endif
+# if defined(_LINUX_CAPABILITY_VERSION_3) // use 64-bit support, if available
+# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_3
+# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_3
+# else // fallback to 32-bit support
+# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_1
+# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_1
+# endif
+
+// Convenient way to ignore EINTR on any system call
+# define EINTR_LOOP(cmd) __extension__ ({__typeof__(cmd) res = 0; do { res = cmd; } while (res == -1 && errno == EINTR); res; })
+
+
+// Declared as weak symbol here, so we can check at runtime if we were compiled against libgcov
+extern "C" void __gcov_init() __attribute__((weak));
+
+
+QT_BEGIN_NAMESPACE_AM
+
+static void sigHupHandler(int sig)
+{
+ if (sig == SIGHUP)
+ _exit(0);
+}
+
+QT_END_NAMESPACE_AM
+
+#endif // Q_OS_LINUX
+
+
+QT_BEGIN_NAMESPACE_AM
+
+void Sudo::forkServer(DropPrivileges dropPrivileges, QStringList *warnings)
+{
+ bool canSudo = false;
+
+#if defined(Q_OS_LINUX)
+ uid_t realUid = getuid();
+ uid_t effectiveUid = geteuid();
+ canSudo = (realUid == 0) || (effectiveUid == 0);
+#else
+ Q_UNUSED(warnings)
+ Q_UNUSED(dropPrivileges)
+#endif
+
+ if (!canSudo) {
+ SudoServer::createInstance(-1);
+ SudoClient::createInstance(-1, SudoServer::instance());
+ if (warnings) {
+ *warnings << qSL("For the installer to work correctly, the executable needs to be run either as root via sudo or SUID (preferred)");
+ *warnings << qSL("(using fallback implementation - you might experience permission errors on installer operations)");
+ }
+ return;
+ }
+
+#if defined(Q_OS_LINUX)
+ gid_t realGid = getgid();
+ uid_t sudoUid = static_cast<uid_t>(qEnvironmentVariableIntValue("SUDO_UID"));
+
+ // run as normal user (e.g. 1000): uid == 1000 euid == 1000
+ // run with binary suid-root: uid == 1000 euid == 0
+ // run with sudo (no suid-root): uid == 0 euid == 0 $SUDO_UID == 1000
+
+ // treat sudo as special variant of a SUID executable
+ if (realUid == 0 && effectiveUid == 0 && sudoUid != 0) {
+ realUid = sudoUid;
+ realGid = static_cast<gid_t>(qEnvironmentVariableIntValue("SUDO_GID"));
+
+ if (setresgid(realGid, 0, 0) || setresuid(realUid, 0, 0))
+ throw Exception(errno, "Could not set real user or group ID");
+ }
+
+ int socketFds[2];
+ if (EINTR_LOOP(socketpair(AF_UNIX, SOCK_DGRAM, 0, socketFds)) != 0)
+ throw Exception(errno, "Could not create a pair of sockets");
+
+ // We need to make the gcda files generated by the root process writable by the normal user.
+ // There is no way to detect a compilation with -ftest-coverage, but we can check for gcov
+ // symbols at runtime. GCov will open all gcda files at fork() time, so we can get away with
+ // switching umasks around the fork() call.
+
+ mode_t realUmask = 0;
+ if (__gcov_init)
+ realUmask = umask(0);
+
+ pid_t pid = fork();
+ if (pid < 0) {
+ throw Exception(errno, "Could not fork process");
+ } else if (pid == 0) {
+ // child
+ close(0);
+ setsid();
+
+ // reset umask
+ if (realUmask)
+ umask(realUmask);
+
+ // This call is Linux only, but it makes it so easy to detect a dying parent process.
+ // We would have a big problem otherwise, since the main process drops its privileges,
+ // which prevents it from sending SIGHUP to the child process, which still runs with
+ // root privileges.
+ prctl(PR_SET_PDEATHSIG, SIGHUP);
+ signal(SIGHUP, sigHupHandler);
+
+ // Drop as many capabilities as possible, just to be on the safe side
+ static const quint32 neededCapabilities[] = {
+ CAP_SYS_ADMIN,
+ CAP_CHOWN,
+ CAP_FOWNER,
+ CAP_DAC_OVERRIDE
+ };
+
+ bool capSetOk = false;
+ __user_cap_header_struct capHeader { AM_CAP_VERSION, getpid() };
+ __user_cap_data_struct capData[AM_CAP_SIZE];
+ if (capget(&capHeader, capData) == 0) {
+ quint32 capNeeded[AM_CAP_SIZE];
+ memset(&capNeeded, 0, sizeof(capNeeded));
+ for (quint32 cap : neededCapabilities) {
+ int idx = CAP_TO_INDEX(cap);
+ Q_ASSERT(idx < AM_CAP_SIZE);
+ capNeeded[idx] |= CAP_TO_MASK(cap);
+ }
+ for (int i = 0; i < AM_CAP_SIZE; ++i)
+ capData[i].effective = capData[i].permitted = capData[i].inheritable = capNeeded[i];
+ if (capset(&capHeader, capData) == 0)
+ capSetOk = true;
+ }
+ if (!capSetOk)
+ qCCritical(LogSystem) << "could not drop privileges in the SudoServer process -- continuing with full root privileges";
+
+ SudoServer::createInstance(socketFds[0]);
+ ProcessTitle::setTitle("%s", "sudo helper");
+ SudoServer::instance()->run();
+ }
+ // parent
+
+ // reset umask
+ if (realUmask)
+ umask(realUmask);
+
+ SudoClient::createInstance(socketFds[1]);
+
+ if (realUid != effectiveUid) {
+ // drop all root privileges
+ if (dropPrivileges == DropPrivilegesPermanently) {
+ if (setresgid(realGid, realGid, realGid) || setresuid(realUid, realUid, realUid)) {
+ kill(pid, SIGKILL);
+ throw Exception(errno, "Could not set real user or group ID");
+ }
+ } else {
+ qCCritical(LogSystem) << "\nSudo was instructed to NOT drop root privileges permanently.\nThis is dangerous and should only be used in auto-tests!\n";
+ if (setresgid(realGid, realGid, 0) || setresuid(realUid, realUid, 0)) {
+ kill(pid, 9);
+ throw Exception(errno, "Could not set real user or group ID");
+ }
+ }
+ }
+ ::atexit([]() { SudoClient::instance()->stopServer(); });
+#endif
+}
+
+SudoInterface::SudoInterface()
+{ }
+
+#ifdef Q_OS_LINUX
+bool SudoInterface::sendMessage(int socket, const QByteArray &msg, MessageType type, const QString &errorString)
+{
+ QByteArray packet;
+ QDataStream ds(&packet, QIODevice::WriteOnly);
+ ds << errorString << msg;
+ packet.prepend((type == Request) ? "RQST" : "RPLY");
+
+ auto bytesWritten = EINTR_LOOP(write(socket, packet.constData(), static_cast<size_t>(packet.size())));
+ return bytesWritten == packet.size();
+}
+
+
+QByteArray SudoInterface::receiveMessage(int socket, MessageType type, QString *errorString)
+{
+ const int headerSize = 4;
+ char recvBuffer[8*1024];
+ auto bytesReceived = EINTR_LOOP(recv(socket, recvBuffer, sizeof(recvBuffer), 0));
+
+ if ((bytesReceived < headerSize) || qstrncmp(recvBuffer, (type == Request ? "RQST" : "RPLY"), 4)) {
+ *errorString = qL1S("failed to receive command from the SudoClient process");
+ //qCCritical(LogSystem) << *errorString;
+ return QByteArray();
+ }
+
+ QByteArray packet(recvBuffer + headerSize, int(bytesReceived) - headerSize);
+
+ QDataStream ds(&packet, QIODevice::ReadOnly);
+ QByteArray msg;
+ ds >> *errorString >> msg;
+ return msg;
+}
+#endif // Q_OS_LINUX
+
+
+SudoClient *SudoClient::s_instance = nullptr;
+
+SudoClient *SudoClient::instance()
+{
+ return s_instance;
+}
+
+bool SudoClient::isFallbackImplementation() const
+{
+ return m_socket < 0;
+}
+
+SudoClient::SudoClient(int socketFd)
+ : m_socket(socketFd)
+{ }
+
+SudoClient *SudoClient::createInstance(int socketFd, SudoServer *shortCircuit)
+{
+ if (!s_instance) {
+ s_instance = new SudoClient(socketFd);
+ s_instance->m_shortCircuit = shortCircuit;
+ }
+ return s_instance;
+}
+
+
+// this is not nice, but it prevents a lot of copy/paste errors. (the C++ variadic template version
+// would be equally ugly, since it needs a friend declaration in the public header)
+template <typename R, typename C, typename ...Ps> R returnType(R (C::*)(Ps...));
+
+#define CALL(FUNC_NAME, PARAM) \
+ QByteArray msg; \
+ QDataStream(&msg, QIODevice::WriteOnly) << #FUNC_NAME << PARAM; \
+ QByteArray reply = call(msg); \
+ QDataStream result(&reply, QIODevice::ReadOnly); \
+ decltype(returnType(&SudoClient::FUNC_NAME)) r; \
+ result >> r; \
+ return r
+
+bool SudoClient::removeRecursive(const QString &fileOrDir)
+{
+ CALL(removeRecursive, fileOrDir);
+}
+
+bool SudoClient::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions)
+{
+ CALL(setOwnerAndPermissionsRecursive, fileOrDir << user << group << permissions);
+}
+
+void SudoClient::stopServer()
+{
+#ifdef Q_OS_LINUX
+ if (!m_shortCircuit && m_socket >= 0) {
+ QByteArray msg;
+ QDataStream(&msg, QIODevice::WriteOnly) << "stopServer";
+ sendMessage(m_socket, msg, Request);
+ }
+#endif
+}
+
+QByteArray SudoClient::call(const QByteArray &msg)
+{
+ QMutexLocker locker(&m_mutex);
+
+ if (m_shortCircuit)
+ return m_shortCircuit->receive(msg);
+
+#ifdef Q_OS_LINUX
+ if (m_socket >= 0) {
+ if (sendMessage(m_socket, msg, Request))
+ return receiveMessage(m_socket, Reply, &m_errorString);
+ }
+#else
+ Q_UNUSED(m_socket)
+#endif
+
+ //qCCritical(LogSystem) << "failed to send command to the SudoServer process";
+ m_errorString = qL1S("failed to send command to the SudoServer process");
+ return QByteArray();
+}
+
+
+
+SudoServer *SudoServer::s_instance = nullptr;
+
+SudoServer *SudoServer::instance()
+{
+ return s_instance;
+}
+
+SudoServer::SudoServer(int socketFd)
+ : m_socket(socketFd)
+{ }
+
+SudoServer *SudoServer::createInstance(int socketFd)
+{
+ if (!s_instance)
+ s_instance = new SudoServer(socketFd);
+ return s_instance;
+}
+
+void SudoServer::run()
+{
+#ifdef Q_OS_LINUX
+ QString dummy;
+
+ forever {
+ QByteArray msg = receiveMessage(m_socket, Request, &dummy);
+ QByteArray reply = receive(msg);
+
+ if (m_stop)
+ exit(0);
+
+ sendMessage(m_socket, reply, Reply, m_errorString);
+ }
+#else
+ Q_UNUSED(m_socket)
+ Q_ASSERT(false);
+ exit(0);
+#endif
+}
+
+QByteArray SudoServer::receive(const QByteArray &msg)
+{
+ QDataStream params(msg);
+ char *functionArray;
+ params >> functionArray;
+ QByteArray function(functionArray);
+ delete [] functionArray;
+ QByteArray reply;
+ QDataStream result(&reply, QIODevice::WriteOnly);
+ m_errorString.clear();
+
+ if (function == "removeRecursive") {
+ QString fileOrDir;
+ params >> fileOrDir;
+ result << removeRecursive(fileOrDir);
+ } else if (function == "setOwnerAndPermissionsRecursive") {
+ QString fileOrDir;
+ uid_t user;
+ gid_t group;
+ mode_t permissions;
+ params >> fileOrDir >> user >> group >> permissions;
+ result << setOwnerAndPermissionsRecursive(fileOrDir, user, group, permissions);
+ } else if (function == "stopServer") {
+ m_stop = true;
+ } else {
+ reply.truncate(0);
+ m_errorString = QString::fromLatin1("unknown function '%1' called in SudoServer").arg(qL1S(function));
+ }
+ return reply;
+}
+
+bool SudoServer::removeRecursive(const QString &fileOrDir)
+{
+ try {
+ if (!recursiveOperation(fileOrDir, safeRemove))
+ throw Exception(errno, "could not recursively remove %1").arg(fileOrDir);
+ return true;
+ } catch (const Exception &e) {
+ m_errorString = e.errorString();
+ return false;
+ }
+}
+
+bool SudoServer::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions)
+{
+#if defined(Q_OS_LINUX)
+ static auto setOwnerAndPermissions =
+ [user, group, permissions](const QString &path, RecursiveOperationType type) -> bool {
+ if (type == RecursiveOperationType::EnterDirectory)
+ return true;
+
+ const QByteArray localPath = path.toLocal8Bit();
+ mode_t mode = permissions;
+
+ if (type == RecursiveOperationType::LeaveDirectory) {
+ // set the x bit for directories, but only where it makes sense
+ if (mode & 06)
+ mode |= 01;
+ if (mode & 060)
+ mode |= 010;
+ if (mode & 0600)
+ mode |= 0100;
+ }
+
+ return ((chmod(localPath, mode) == 0) && (chown(localPath, user, group) == 0));
+ };
+
+ try {
+ if (!recursiveOperation(fileOrDir, setOwnerAndPermissions)) {
+ throw Exception(errno, "could not recursively set owner and permission on %1 to %2:%3 / %4")
+ .arg(fileOrDir).arg(user).arg(group).arg(permissions, 4, 8, QLatin1Char('0'));
+ }
+ } catch (const Exception &e) {
+ m_errorString = e.errorString();
+ return false;
+ }
+#else
+ Q_UNUSED(fileOrDir)
+ Q_UNUSED(user)
+ Q_UNUSED(group)
+ Q_UNUSED(permissions)
+#endif // Q_OS_LINUX
+ return false;
+}
+
+QT_END_NAMESPACE_AM