// Copyright (C) 2021 The Qt Company Ltd. // Copyright (C) 2019 Luxoft Sweden AB // Copyright (C) 2018 Pelagicore AG // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // Qt-Security score:critical reason:privilege-management #include #include #include #include #include #include #include #include "logging.h" #include "sudo.h" #include "utilities.h" #include "exception.h" #include "global.h" #include using namespace Qt::StringLiterals; #if defined(Q_OS_LINUX) # include "processtitle.h" # include # include # include # include # include # include # include # include # include # include # include // 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)); // NOLINT(reserved-identifier) #ifndef OPEN_TREE_CLONE # define OPEN_TREE_CLONE 1 #endif #ifndef OPEN_TREE_CLOEXEC # define OPEN_TREE_CLOEXEC O_CLOEXEC #endif #ifndef SYS_open_tree # define SYS_open_tree 428 #endif #ifndef MOVE_MOUNT_F_EMPTY_PATH # define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 #endif #ifndef SYS_move_mount # define SYS_move_mount 429 #endif #ifndef MOUNT_ATTR_RDONLY # define MOUNT_ATTR_RDONLY 0x00000001 #endif #ifndef SYS_mount_setattr # define SYS_mount_setattr 442 #endif #ifndef MOUNT_ATTR_SIZE_VER0 # define MOUNT_ATTR_SIZE_VER0 32 struct mount_attr { __u64 attr_set; __u64 attr_clr; __u64 propagation; __u64 userns_fd; }; #endif #ifndef AT_RECURSIVE # define AT_RECURSIVE 0x8000 #endif #ifndef AT_EMPTY_PATH # define AT_EMPTY_PATH 0x1000 #endif #ifndef SYS_mount_setattr # define SYS_mount_setattr 442 #endif #ifndef SYS_pidfd_open # define SYS_pidfd_open 434 #endif 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) { bool canSudo = false; #if defined(Q_OS_LINUX) uid_t realUid = getuid(); uid_t effectiveUid = geteuid(); canSudo = (realUid == 0) || (effectiveUid == 0); #else Q_UNUSED(dropPrivileges) #endif if (!canSudo) { SudoServer::createInstance(-1); SudoClient::createInstance(-1, SudoServer::instance()); return; } #if defined(Q_OS_LINUX) gid_t realGid = getgid(); uid_t sudoUid = static_cast(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(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_SYS_CHROOT, CAP_SYS_PTRACE, 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, QDataStream::WriteOnly); ds << errorString << msg; packet.prepend((type == Request) ? "RQST" : "RPLY"); auto bytesWritten = EINTR_LOOP(write(socket, packet.constData(), static_cast(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 = u"failed to receive command from the SudoClient process"_s; //qCCritical(LogSystem) << *errorString; return QByteArray(); } QByteArray packet(recvBuffer + headerSize, int(bytesReceived) - headerSize); QDataStream ds(&packet, QDataStream::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 R returnType(R (C::*)(Ps...)); #define CALL(FUNC_NAME, PARAM) \ QByteArray msg; \ QDataStream(&msg, QDataStream::WriteOnly) << #FUNC_NAME << PARAM; \ QByteArray reply = call(msg); \ QDataStream result(&reply, QDataStream::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); } bool SudoClient::bindMountFileSystem(const QString &from, const QString &to, bool readOnly, quint64 namespacePid) { CALL(bindMountFileSystem, from << to << readOnly << namespacePid); } void SudoClient::stopServer() { #ifdef Q_OS_LINUX if (!m_shortCircuit && m_socket >= 0) { QByteArray msg; QDataStream(&msg, QDataStream::WriteOnly) << "stopServer"; sendMessage(m_socket, msg, Request); } #endif } QByteArray SudoClient::call(const QByteArray &msg) { QMutexLocker locker(&m_mutex); if (m_shortCircuit) { const QByteArray res = m_shortCircuit->receive(msg); m_errorString = m_shortCircuit->lastError(); return res; } #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 = u"failed to send command to the SudoServer process"_s; 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, QDataStream::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 == "bindMountFileSystem") { QString from; QString to; bool readOnly; quint64 namespacePid; params >> from >> to >> readOnly >> namespacePid; result << bindMountFileSystem(from, to, readOnly, namespacePid); } else if (function == "stopServer") { m_stop = true; } else { reply.truncate(0); m_errorString = u"unknown function '%1' called in SudoServer"_s.arg(QString::fromLatin1(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(); bool noModeChange = (permissions == static_cast(-1)); 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 ((noModeChange ? true : (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(int(permissions), 4, 8, QChar(u'0')); } return true; } catch (const Exception &e) { m_errorString = e.errorString(); return false; } #else Q_UNUSED(fileOrDir) Q_UNUSED(user) Q_UNUSED(group) Q_UNUSED(permissions) return false; #endif // Q_OS_LINUX } bool SudoServer::bindMountFileSystem(const QString &from, const QString &to, bool readOnly, quint64 namespacePid) { #if defined(Q_OS_LINUX) bool result = true; int oldNsFd = -1; try { // Create a detached mount point for our source location int fromFd = int(::syscall(SYS_open_tree, -EBADF, from.toLocal8Bit().constData(), OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE)); if (fromFd < 0) throw Exception(errno, "could not create a detached mount point for %1").arg(from); if (readOnly) { ::mount_attr mountAttr { MOUNT_ATTR_RDONLY, 0, 0, 0 }; if (::syscall(SYS_mount_setattr, fromFd, "", AT_EMPTY_PATH | AT_RECURSIVE, &mountAttr, sizeof(mountAttr)) < 0) throw Exception(errno, "could not set the detached mount point for %1 to read-only").arg(from); } if (namespacePid) { // Save our current mount namespace to be able to restore it later oldNsFd = open("/proc/self/ns/mnt", O_RDONLY); if (oldNsFd < 0) throw Exception(errno, "could not open our own mount namespace"); int pidFd = int(::syscall(SYS_pidfd_open, pid_t(namespacePid), 0)); if (pidFd < 0) throw Exception(errno, "process %1 is not available").arg(namespacePid); if (::setns(pidFd, CLONE_NEWNS) < 0) throw Exception(errno, "could not enter the mount namespace of process %1").arg(namespacePid); } // Mount the detached mount point to the final location within the mount namespace if (::syscall(SYS_move_mount, fromFd, "", -EBADF, to.toLocal8Bit().constData(), MOVE_MOUNT_F_EMPTY_PATH) < 0) throw Exception(errno, "could not move the detached mount point to %1").arg(to); } catch (const Exception &e) { result = false; m_errorString = e.errorString(); } if ((oldNsFd >= 0) && namespacePid) { // Restore our old mount namespace if (::setns(oldNsFd, CLONE_NEWNS) < 0) qFatal() << "SudoHelper process is halted: could not reset the mount namespace:" << strerror(errno); } return result; #else Q_UNUSED(from) Q_UNUSED(to) Q_UNUSED(readOnly) Q_UNUSED(namespacePid) m_errorString = u"bindMountFileSystem is only available on Linux"_s; return false; #endif // Q_OS_LINUX } QT_END_NAMESPACE_AM