File indexing completed on 2024-04-21 03:53:59

0001 /*
0002     This file is part of the KDE project, module kdesu.
0003     SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org>
0004 
0005     Sudo support added by Jonathan Riddell <jriddell@ ubuntu.com>
0006     SPDX-FileCopyrightText: 2005 Canonical Ltd // krazy:exclude=copyright (no email)
0007 
0008     SPDX-License-Identifier: GPL-2.0-only
0009 
0010     su.cpp: Execute a program as another user with "class SuProcess".
0011 */
0012 
0013 #include "suprocess.h"
0014 
0015 #include "kcookie_p.h"
0016 #include "stubprocess_p.h"
0017 #include <ksu_debug.h>
0018 
0019 #include <QFile>
0020 #include <QStandardPaths>
0021 #include <qplatformdefs.h>
0022 
0023 #include <KConfig>
0024 #include <KConfigGroup>
0025 #include <KSharedConfig>
0026 #include <kuser.h>
0027 
0028 #if defined(KDESU_USE_SUDO_DEFAULT)
0029 #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("sudo")
0030 #elif defined(KDESU_USE_DOAS_DEFAULT)
0031 #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("doas")
0032 #else
0033 #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("su")
0034 #endif
0035 
0036 namespace KDESu
0037 {
0038 using namespace KDESuPrivate;
0039 
0040 class SuProcessPrivate : public StubProcessPrivate
0041 {
0042 public:
0043     bool isPrivilegeEscalation() const;
0044     QString superUserCommand;
0045 };
0046 
0047 bool SuProcessPrivate::isPrivilegeEscalation() const
0048 {
0049     return (superUserCommand == QLatin1String("sudo") || superUserCommand == QLatin1String("doas"));
0050 }
0051 
0052 SuProcess::SuProcess(const QByteArray &user, const QByteArray &command)
0053     : StubProcess(*new SuProcessPrivate)
0054 {
0055     Q_D(SuProcess);
0056 
0057     m_user = user;
0058     m_command = command;
0059 
0060     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0061     KConfigGroup group(config, QStringLiteral("super-user-command"));
0062     d->superUserCommand = group.readEntry("super-user-command", DEFAULT_SUPER_USER_COMMAND);
0063 
0064     if (!d->isPrivilegeEscalation() && d->superUserCommand != QLatin1String("su")) {
0065         qCWarning(KSU_LOG) << "unknown super user command.";
0066         d->superUserCommand = DEFAULT_SUPER_USER_COMMAND;
0067     }
0068 }
0069 
0070 SuProcess::~SuProcess() = default;
0071 
0072 QString SuProcess::superUserCommand()
0073 {
0074     Q_D(SuProcess);
0075 
0076     return d->superUserCommand;
0077 }
0078 
0079 bool SuProcess::useUsersOwnPassword()
0080 {
0081     Q_D(SuProcess);
0082 
0083     if (d->isPrivilegeEscalation() && m_user == "root") {
0084         return true;
0085     }
0086 
0087     KUser user;
0088     return user.loginName() == QString::fromUtf8(m_user);
0089 }
0090 
0091 int SuProcess::checkInstall(const char *password)
0092 {
0093     return exec(password, Install);
0094 }
0095 
0096 int SuProcess::checkNeedPassword()
0097 {
0098     return exec(nullptr, NeedPassword);
0099 }
0100 
0101 /*
0102  * Execute a command with su(1).
0103  */
0104 int SuProcess::exec(const char *password, int check)
0105 {
0106     Q_D(SuProcess);
0107 
0108     if (check) {
0109         setTerminal(true);
0110     }
0111 
0112     // since user may change after constructor (due to setUser())
0113     // we need to override sudo with su for non-root here
0114     if (m_user != QByteArray("root")) {
0115         d->superUserCommand = QStringLiteral("su");
0116     }
0117 
0118     QList<QByteArray> args;
0119     if (d->isPrivilegeEscalation()) {
0120         args += "-u";
0121     }
0122 
0123     if (m_scheduler != SchedNormal || m_priority > 50) {
0124         args += "root";
0125     } else {
0126         args += m_user;
0127     }
0128 
0129     if (d->superUserCommand == QLatin1String("su")) {
0130         args += "-c";
0131     }
0132     // Get the kdesu_stub and su command from a config file if set, used in test
0133     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0134     KConfigGroup group(config, QStringLiteral("super-user-command"));
0135     const QString defaultPath = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF) + QStringLiteral("/kdesu_stub");
0136     const QString kdesuStubPath = group.readEntry("kdesu_stub_path", defaultPath);
0137     args += kdesuStubPath.toLocal8Bit();
0138     args += "-"; // krazy:exclude=doublequote_chars (QList, not QString)
0139 
0140     const QString commandString = group.readEntry("command", QStandardPaths::findExecutable(d->superUserCommand));
0141     const QByteArray command = commandString.toLocal8Bit();
0142     if (command.isEmpty()) {
0143         return check ? SuNotFound : -1;
0144     }
0145 
0146     // Turn echo off for conversion with kdesu_stub. Needs to be done before
0147     // it's started so that sudo copies this option to its internal PTY.
0148     enableLocalEcho(false);
0149 
0150     if (StubProcess::exec(command, args) < 0) {
0151         return check ? SuNotFound : -1;
0152     }
0153 
0154     SuErrors ret = (SuErrors)converseSU(password);
0155 
0156     if (ret == error) {
0157         if (!check) {
0158             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0159                                 << "Conversation with" << d->superUserCommand << "failed.";
0160         }
0161         return ret;
0162     }
0163     if (check == NeedPassword) {
0164         if (ret == killme) {
0165             if (d->isPrivilegeEscalation()) {
0166                 // sudo can not be killed, just return
0167                 return ret;
0168             }
0169             if (kill(m_pid, SIGKILL) < 0) {
0170                 // FIXME SIGKILL doesn't work for sudo,
0171                 // why is this different from su?
0172                 // A: because sudo runs as root. Perhaps we could write a Ctrl+C to its stdin, instead?
0173                 ret = error;
0174             } else {
0175                 int iret = waitForChild();
0176                 if (iret < 0) {
0177                     ret = error;
0178                 }
0179             }
0180         }
0181         return ret;
0182     }
0183 
0184     if (m_erase && password) {
0185         memset(const_cast<char *>(password), 0, qstrlen(password));
0186     }
0187 
0188     if (ret != ok) {
0189         kill(m_pid, SIGKILL);
0190         if (d->isPrivilegeEscalation()) {
0191             waitForChild();
0192         }
0193         return SuIncorrectPassword;
0194     }
0195 
0196     int iret = converseStub(check);
0197     if (iret < 0) {
0198         if (!check) {
0199             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0200                                 << "Conversation with kdesu_stub failed.";
0201         }
0202         return iret;
0203     } else if (iret == 1) {
0204         kill(m_pid, SIGKILL);
0205         waitForChild();
0206         return SuIncorrectPassword;
0207     }
0208 
0209     if (check == Install) {
0210         waitForChild();
0211         return 0;
0212     }
0213 
0214     iret = waitForChild();
0215     return iret;
0216 }
0217 
0218 /*
0219  * Conversation with su: feed the password.
0220  * Return values: -1 = error, 0 = ok, 1 = kill me, 2 not authorized
0221  */
0222 int SuProcess::converseSU(const char *password)
0223 {
0224     enum {
0225         WaitForPrompt,
0226         CheckStar,
0227         HandleStub,
0228     } state = WaitForPrompt;
0229     int colon;
0230     unsigned i;
0231     unsigned j;
0232 
0233     QByteArray line;
0234     while (true) {
0235         line = readLine();
0236         // return if problem. sudo checks for a second prompt || su gets a blank line
0237         if ((line.contains(':') && state != WaitForPrompt) || line.isNull()) {
0238             return (state == HandleStub ? notauthorized : error);
0239         }
0240 
0241         if (line == "kdesu_stub") {
0242             unreadLine(line);
0243             return ok;
0244         }
0245 
0246         switch (state) {
0247         case WaitForPrompt: {
0248             if (waitMS(fd(), 100) > 0) {
0249                 // There is more output available, so this line
0250                 // couldn't have been a password prompt (the definition
0251                 // of prompt being that  there's a line of output followed
0252                 // by a colon, and then the process waits).
0253                 continue;
0254             }
0255 
0256             const uint len = line.length();
0257             // Match "Password: " with the regex ^[^:]+:[\w]*$.
0258             for (i = 0, j = 0, colon = 0; i < len; ++i) {
0259                 if (line[i] == ':') {
0260                     j = i;
0261                     colon++;
0262                     continue;
0263                 }
0264                 if (!isspace(line[i])) {
0265                     j++;
0266                 }
0267             }
0268             if (colon == 1 && line[j] == ':') {
0269                 if (password == nullptr) {
0270                     return killme;
0271                 }
0272                 if (waitSlave()) {
0273                     return error;
0274                 }
0275                 write(fd(), password, strlen(password));
0276                 write(fd(), "\n", 1);
0277                 state = CheckStar;
0278             }
0279             break;
0280         }
0281         //////////////////////////////////////////////////////////////////////////
0282         case CheckStar: {
0283             const QByteArray s = line.trimmed();
0284             if (s.isEmpty()) {
0285                 state = HandleStub;
0286                 break;
0287             }
0288             const bool starCond = std::any_of(s.cbegin(), s.cend(), [](const char c) {
0289                 return c != '*';
0290             });
0291             if (starCond) {
0292                 return error;
0293             }
0294             state = HandleStub;
0295             break;
0296         }
0297         //////////////////////////////////////////////////////////////////////////
0298         case HandleStub:
0299             break;
0300             //////////////////////////////////////////////////////////////////////////
0301         } // end switch
0302     } // end while (true)
0303     return ok;
0304 }
0305 
0306 void SuProcess::virtual_hook(int id, void *data)
0307 {
0308     StubProcess::virtual_hook(id, data);
0309 }
0310 
0311 } // namespace KDESu