| 1 | /* |
| 2 | This file is part of the KDE project, module kdesu. |
| 3 | SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org> |
| 4 | |
| 5 | Sudo support added by Jonathan Riddell <jriddell@ ubuntu.com> |
| 6 | SPDX-FileCopyrightText: 2005 Canonical Ltd // krazy:exclude=copyright (no email) |
| 7 | |
| 8 | SPDX-License-Identifier: GPL-2.0-only |
| 9 | |
| 10 | su.cpp: Execute a program as another user with "class SuProcess". |
| 11 | */ |
| 12 | |
| 13 | #include "suprocess.h" |
| 14 | |
| 15 | #include "kcookie_p.h" |
| 16 | #include "stubprocess_p.h" |
| 17 | #include <ksu_debug.h> |
| 18 | |
| 19 | #include <QFile> |
| 20 | #include <QStandardPaths> |
| 21 | #include <qplatformdefs.h> |
| 22 | |
| 23 | #include <KConfig> |
| 24 | #include <KConfigGroup> |
| 25 | #include <KSharedConfig> |
| 26 | #include <kuser.h> |
| 27 | |
| 28 | #if defined(KDESU_USE_SUDO_DEFAULT) |
| 29 | #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("sudo") |
| 30 | #elif defined(KDESU_USE_DOAS_DEFAULT) |
| 31 | #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("doas") |
| 32 | #else |
| 33 | #define DEFAULT_SUPER_USER_COMMAND QStringLiteral("su") |
| 34 | #endif |
| 35 | |
| 36 | namespace KDESu |
| 37 | { |
| 38 | using namespace KDESuPrivate; |
| 39 | |
| 40 | class SuProcessPrivate : public StubProcessPrivate |
| 41 | { |
| 42 | public: |
| 43 | bool isPrivilegeEscalation() const; |
| 44 | QString superUserCommand; |
| 45 | }; |
| 46 | |
| 47 | bool SuProcessPrivate::isPrivilegeEscalation() const |
| 48 | { |
| 49 | return (superUserCommand == QLatin1String("sudo" ) || superUserCommand == QLatin1String("doas" )); |
| 50 | } |
| 51 | |
| 52 | SuProcess::SuProcess(const QByteArray &user, const QByteArray &command) |
| 53 | : StubProcess(*new SuProcessPrivate) |
| 54 | { |
| 55 | Q_D(SuProcess); |
| 56 | |
| 57 | m_user = user; |
| 58 | m_command = command; |
| 59 | |
| 60 | KSharedConfig::Ptr config = KSharedConfig::openConfig(); |
| 61 | KConfigGroup group(config, QStringLiteral("super-user-command" )); |
| 62 | d->superUserCommand = group.readEntry(key: "super-user-command" , DEFAULT_SUPER_USER_COMMAND); |
| 63 | |
| 64 | if (!d->isPrivilegeEscalation() && d->superUserCommand != QLatin1String("su" )) { |
| 65 | qCWarning(KSU_LOG) << "unknown super user command." ; |
| 66 | d->superUserCommand = DEFAULT_SUPER_USER_COMMAND; |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | SuProcess::~SuProcess() = default; |
| 71 | |
| 72 | QString SuProcess::superUserCommand() |
| 73 | { |
| 74 | Q_D(SuProcess); |
| 75 | |
| 76 | return d->superUserCommand; |
| 77 | } |
| 78 | |
| 79 | bool SuProcess::useUsersOwnPassword() |
| 80 | { |
| 81 | Q_D(SuProcess); |
| 82 | |
| 83 | if (d->isPrivilegeEscalation() && m_user == "root" ) { |
| 84 | return true; |
| 85 | } |
| 86 | |
| 87 | KUser user; |
| 88 | return user.loginName() == QString::fromUtf8(ba: m_user); |
| 89 | } |
| 90 | |
| 91 | int SuProcess::checkInstall(const char *password) |
| 92 | { |
| 93 | return exec(password, check: Install); |
| 94 | } |
| 95 | |
| 96 | int SuProcess::checkNeedPassword() |
| 97 | { |
| 98 | return exec(password: nullptr, check: NeedPassword); |
| 99 | } |
| 100 | |
| 101 | /* |
| 102 | * Execute a command with su(1). |
| 103 | */ |
| 104 | int SuProcess::exec(const char *password, int check) |
| 105 | { |
| 106 | Q_D(SuProcess); |
| 107 | |
| 108 | if (check) { |
| 109 | setTerminal(true); |
| 110 | } |
| 111 | |
| 112 | // since user may change after constructor (due to setUser()) |
| 113 | // we need to override sudo with su for non-root here |
| 114 | if (m_user != QByteArray("root" )) { |
| 115 | d->superUserCommand = QStringLiteral("su" ); |
| 116 | } |
| 117 | |
| 118 | QList<QByteArray> args; |
| 119 | if (d->isPrivilegeEscalation()) { |
| 120 | args += "-u" ; |
| 121 | } |
| 122 | |
| 123 | if (m_scheduler != SchedNormal || m_priority > 50) { |
| 124 | args += "root" ; |
| 125 | } else { |
| 126 | args += m_user; |
| 127 | } |
| 128 | |
| 129 | if (d->superUserCommand == QLatin1String("su" )) { |
| 130 | args += "-c" ; |
| 131 | } |
| 132 | // Get the kdesu_stub and su command from a config file if set, used in test |
| 133 | KSharedConfig::Ptr config = KSharedConfig::openConfig(); |
| 134 | KConfigGroup group(config, QStringLiteral("super-user-command" )); |
| 135 | const QString defaultPath = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF) + QStringLiteral("/kdesu_stub" ); |
| 136 | const QString kdesuStubPath = group.readEntry(key: "kdesu_stub_path" , aDefault: defaultPath); |
| 137 | args += kdesuStubPath.toLocal8Bit(); |
| 138 | args += "-" ; // krazy:exclude=doublequote_chars (QList, not QString) |
| 139 | |
| 140 | const QString commandString = group.readEntry(key: "command" , aDefault: QStandardPaths::findExecutable(executableName: d->superUserCommand)); |
| 141 | const QByteArray command = commandString.toLocal8Bit(); |
| 142 | if (command.isEmpty()) { |
| 143 | return check ? SuNotFound : -1; |
| 144 | } |
| 145 | |
| 146 | // Turn echo off for conversion with kdesu_stub. Needs to be done before |
| 147 | // it's started so that sudo copies this option to its internal PTY. |
| 148 | enableLocalEcho(enable: false); |
| 149 | |
| 150 | if (StubProcess::exec(command, args) < 0) { |
| 151 | return check ? SuNotFound : -1; |
| 152 | } |
| 153 | |
| 154 | SuErrors ret = (SuErrors)converseSU(password); |
| 155 | |
| 156 | if (ret == error) { |
| 157 | if (!check) { |
| 158 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
| 159 | << "Conversation with" << d->superUserCommand << "failed." ; |
| 160 | } |
| 161 | return ret; |
| 162 | } |
| 163 | if (check == NeedPassword) { |
| 164 | if (ret == killme) { |
| 165 | if (d->isPrivilegeEscalation()) { |
| 166 | // sudo can not be killed, just return |
| 167 | return ret; |
| 168 | } |
| 169 | if (kill(pid: m_pid, SIGKILL) < 0) { |
| 170 | // FIXME SIGKILL doesn't work for sudo, |
| 171 | // why is this different from su? |
| 172 | // A: because sudo runs as root. Perhaps we could write a Ctrl+C to its stdin, instead? |
| 173 | ret = error; |
| 174 | } else { |
| 175 | int iret = waitForChild(); |
| 176 | if (iret < 0) { |
| 177 | ret = error; |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | return ret; |
| 182 | } |
| 183 | |
| 184 | if (m_erase && password) { |
| 185 | memset(s: const_cast<char *>(password), c: 0, n: qstrlen(str: password)); |
| 186 | } |
| 187 | |
| 188 | if (ret != ok) { |
| 189 | kill(pid: m_pid, SIGKILL); |
| 190 | if (d->isPrivilegeEscalation()) { |
| 191 | waitForChild(); |
| 192 | } |
| 193 | return SuIncorrectPassword; |
| 194 | } |
| 195 | |
| 196 | int iret = converseStub(check); |
| 197 | if (iret < 0) { |
| 198 | if (!check) { |
| 199 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
| 200 | << "Conversation with kdesu_stub failed." ; |
| 201 | } |
| 202 | return iret; |
| 203 | } else if (iret == 1) { |
| 204 | kill(pid: m_pid, SIGKILL); |
| 205 | waitForChild(); |
| 206 | return SuIncorrectPassword; |
| 207 | } |
| 208 | |
| 209 | if (check == Install) { |
| 210 | waitForChild(); |
| 211 | return 0; |
| 212 | } |
| 213 | |
| 214 | iret = waitForChild(); |
| 215 | return iret; |
| 216 | } |
| 217 | |
| 218 | /* |
| 219 | * Conversation with su: feed the password. |
| 220 | * Return values: -1 = error, 0 = ok, 1 = kill me, 2 not authorized |
| 221 | */ |
| 222 | int SuProcess::converseSU(const char *password) |
| 223 | { |
| 224 | enum { |
| 225 | WaitForPrompt, |
| 226 | CheckStar, |
| 227 | HandleStub, |
| 228 | } state = WaitForPrompt; |
| 229 | int colon; |
| 230 | unsigned i; |
| 231 | unsigned j; |
| 232 | |
| 233 | QByteArray line; |
| 234 | while (true) { |
| 235 | line = readLine(); |
| 236 | // return if problem. sudo checks for a second prompt || su gets a blank line |
| 237 | if ((line.contains(c: ':') && state != WaitForPrompt) || line.isNull()) { |
| 238 | return (state == HandleStub ? notauthorized : error); |
| 239 | } |
| 240 | |
| 241 | if (line == "kdesu_stub" ) { |
| 242 | unreadLine(line); |
| 243 | return ok; |
| 244 | } |
| 245 | |
| 246 | switch (state) { |
| 247 | case WaitForPrompt: { |
| 248 | if (waitMS(fd: fd(), ms: 100) > 0) { |
| 249 | // There is more output available, so this line |
| 250 | // couldn't have been a password prompt (the definition |
| 251 | // of prompt being that there's a line of output followed |
| 252 | // by a colon, and then the process waits). |
| 253 | continue; |
| 254 | } |
| 255 | |
| 256 | const uint len = line.length(); |
| 257 | // Match "Password: " with the regex ^[^:]+:[\w]*$. |
| 258 | for (i = 0, j = 0, colon = 0; i < len; ++i) { |
| 259 | if (line[i] == ':') { |
| 260 | j = i; |
| 261 | colon++; |
| 262 | continue; |
| 263 | } |
| 264 | if (!isspace(line[i])) { |
| 265 | j++; |
| 266 | } |
| 267 | } |
| 268 | if (colon == 1 && line[j] == ':') { |
| 269 | if (password == nullptr) { |
| 270 | return killme; |
| 271 | } |
| 272 | if (waitSlave()) { |
| 273 | return error; |
| 274 | } |
| 275 | write(fd: fd(), buf: password, n: strlen(s: password)); |
| 276 | write(fd: fd(), buf: "\n" , n: 1); |
| 277 | state = CheckStar; |
| 278 | } |
| 279 | break; |
| 280 | } |
| 281 | ////////////////////////////////////////////////////////////////////////// |
| 282 | case CheckStar: { |
| 283 | const QByteArray s = line.trimmed(); |
| 284 | if (s.isEmpty()) { |
| 285 | state = HandleStub; |
| 286 | break; |
| 287 | } |
| 288 | const bool starCond = std::any_of(first: s.cbegin(), last: s.cend(), pred: [](const char c) { |
| 289 | return c != '*'; |
| 290 | }); |
| 291 | if (starCond) { |
| 292 | return error; |
| 293 | } |
| 294 | state = HandleStub; |
| 295 | break; |
| 296 | } |
| 297 | ////////////////////////////////////////////////////////////////////////// |
| 298 | case HandleStub: |
| 299 | break; |
| 300 | ////////////////////////////////////////////////////////////////////////// |
| 301 | } // end switch |
| 302 | } // end while (true) |
| 303 | return ok; |
| 304 | } |
| 305 | |
| 306 | void SuProcess::virtual_hook(int id, void *data) |
| 307 | { |
| 308 | StubProcess::virtual_hook(id, data); |
| 309 | } |
| 310 | |
| 311 | } // namespace KDESu |
| 312 | |