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